diff --git a/.github/workflows/emscripten.yml b/.github/workflows/emscripten.yml new file mode 100644 index 00000000..4be8931f --- /dev/null +++ b/.github/workflows/emscripten.yml @@ -0,0 +1,71 @@ +name: Emscripten + +on: + push: + branches: [develop, main] + pull_request: + branches: [develop, main] + workflow_dispatch: + +jobs: + emscripten: + name: Emscripten WebAssembly Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@v14 + + - name: Configure + run: emcmake cmake -B build-em -S emscripten -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build-em -j + + - name: Verify artifacts + run: | + test -f build-em/chaiscript.js + test -f build-em/chaiscript.wasm + test -f build-em/chaiscript.html + echo "All expected artifacts present" + ls -lh build-em/chaiscript.* + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: chaiscript-web + path: | + build-em/chaiscript.js + build-em/chaiscript.wasm + build-em/chaiscript.html + retention-days: 90 + + publish: + name: Publish WASM Release + needs: emscripten + if: github.ref == 'refs/heads/develop' && github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: chaiscript-web + path: artifacts + + - name: Publish to wasm-latest release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Flatten: artifacts may contain build-em/ subdirectory + find artifacts -type f \( -name 'chaiscript.js' -o -name 'chaiscript.wasm' -o -name 'chaiscript.html' \) -exec cp {} . \; + + # Delete existing release if present, then recreate + gh release delete wasm-latest --repo "$GITHUB_REPOSITORY" -y 2>/dev/null || true + gh release create wasm-latest \ + --repo "$GITHUB_REPOSITORY" \ + --title "ChaiScript WASM Build (latest)" \ + --notes "Auto-published from develop branch. Built with Emscripten for browser use." \ + --prerelease \ + chaiscript.js chaiscript.wasm chaiscript.html diff --git a/CMakeLists.txt b/CMakeLists.txt index ad3e79a0..7fcb4bc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -413,6 +413,9 @@ if(BUILD_TESTING) "CHAI_USE_PATH=${CMAKE_CURRENT_SOURCE_DIR}/unittests/" "CHAI_MODULE_PATH=${CMAKE_CURRENT_BINARY_DIR}/" ) + add_executable(async_engine_lifetime_test unittests/async_engine_lifetime_test.cpp) + target_link_libraries(async_engine_lifetime_test ${LIBS}) + add_test(NAME Async_Engine_Lifetime_Test COMMAND async_engine_lifetime_test) endif() add_executable(multifile_test @@ -423,6 +426,10 @@ if(BUILD_TESTING) target_link_libraries(multifile_test ${LIBS}) add_test(NAME MultiFile_Test COMMAND multifile_test) + add_executable(emscripten_eval_test unittests/emscripten_eval_test.cpp) + target_link_libraries(emscripten_eval_test ${LIBS}) + add_test(NAME Emscripten_Eval_Test COMMAND emscripten_eval_test) + install(TARGETS test_module RUNTIME DESTINATION bin LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}/chaiscript") endif() endif() diff --git a/emscripten/CMakeLists.txt b/emscripten/CMakeLists.txt new file mode 100644 index 00000000..7751631f --- /dev/null +++ b/emscripten/CMakeLists.txt @@ -0,0 +1,35 @@ +# Emscripten/WebAssembly build for ChaiScript +# Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js +# +# Usage: +# emcmake cmake -B build-em -S emscripten +# cmake --build build-em + +cmake_minimum_required(VERSION 3.12) +project(chaiscript_em) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Emscripten-specific compiler/linker flags +add_definitions(-DCHAISCRIPT_NO_THREADS -DCHAISCRIPT_NO_DYNLOAD) + +add_executable(chaiscript chaiscript_em.cpp) +target_include_directories(chaiscript PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) + +# Emscripten link flags: enable embind, allow memory growth, export as ES module-compatible +target_link_options(chaiscript PRIVATE + --bind + -sALLOW_MEMORY_GROWTH=1 + -sEXPORT_ES6=0 + -sMODULARIZE=0 + -sINVOKE_RUN=0 +) + +# Copy the HTML shell to the build output directory +add_custom_command(TARGET chaiscript POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/chaiscript.html + ${CMAKE_CURRENT_BINARY_DIR}/chaiscript.html + COMMENT "Copying HTML frontend to build directory" +) diff --git a/emscripten/chaiscript.html b/emscripten/chaiscript.html new file mode 100644 index 00000000..29e32252 --- /dev/null +++ b/emscripten/chaiscript.html @@ -0,0 +1,237 @@ + + + + + + + ChaiScript + + + +
+

ChaiScript

+ Interactive Playground +
Loading...
+
+
+
+
Input
+ +
+
+
+
Output
+
+
+
+ + + + + + diff --git a/emscripten/chaiscript_em.cpp b/emscripten/chaiscript_em.cpp new file mode 100644 index 00000000..06806797 --- /dev/null +++ b/emscripten/chaiscript_em.cpp @@ -0,0 +1,23 @@ +// This file is distributed under the BSD License. +// See "license.txt" for details. +// Copyright 2019, Rob Loach (https://github.com/RobLoach/ChaiScript.js) +// Copyright 2009-2018, Jason Turner (jason@emptycrate.com) +// http://www.chaiscript.com +// +// Emscripten/WebAssembly wrapper for ChaiScript. +// Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js + +#include "chaiscript_eval.hpp" + +#ifdef __EMSCRIPTEN__ +#include + +EMSCRIPTEN_BINDINGS(chaiscript) { + emscripten::function("eval", &chaiscript_eval); + emscripten::function("evalString", &chaiscript_eval_string); + emscripten::function("evalBool", &chaiscript_eval_bool); + emscripten::function("evalInt", &chaiscript_eval_int); + emscripten::function("evalFloat", &chaiscript_eval_float); + emscripten::function("evalDouble", &chaiscript_eval_double); +} +#endif diff --git a/emscripten/chaiscript_eval.hpp b/emscripten/chaiscript_eval.hpp new file mode 100644 index 00000000..52791ab3 --- /dev/null +++ b/emscripten/chaiscript_eval.hpp @@ -0,0 +1,48 @@ +// This file is distributed under the BSD License. +// See "license.txt" for details. +// Copyright 2019, Rob Loach (https://github.com/RobLoach/ChaiScript.js) +// Copyright 2009-2018, Jason Turner (jason@emptycrate.com) +// http://www.chaiscript.com + +// Shared eval helper functions for the ChaiScript Emscripten wrapper. +// These functions provide typed evaluation of ChaiScript expressions, +// used by both the Emscripten/WebAssembly build and native tests. + +#ifndef CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ +#define CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ + +#include +#include + +namespace detail { +inline chaiscript::ChaiScript &get_chai_instance() { + static chaiscript::ChaiScript chai; + return chai; +} +} // namespace detail + +inline void chaiscript_eval(const std::string &input) { + detail::get_chai_instance().eval(input); +} + +inline std::string chaiscript_eval_string(const std::string &input) { + return detail::get_chai_instance().eval(input); +} + +inline bool chaiscript_eval_bool(const std::string &input) { + return detail::get_chai_instance().eval(input); +} + +inline int chaiscript_eval_int(const std::string &input) { + return detail::get_chai_instance().eval(input); +} + +inline float chaiscript_eval_float(const std::string &input) { + return detail::get_chai_instance().eval(input); +} + +inline double chaiscript_eval_double(const std::string &input) { + return detail::get_chai_instance().eval(input); +} + +#endif /* CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ */ diff --git a/include/chaiscript/chaiscript_stdlib.hpp b/include/chaiscript/chaiscript_stdlib.hpp index 5cb4fa68..0daac71e 100644 --- a/include/chaiscript/chaiscript_stdlib.hpp +++ b/include/chaiscript/chaiscript_stdlib.hpp @@ -49,9 +49,8 @@ namespace chaiscript { #ifndef CHAISCRIPT_NO_THREADS bootstrap::standard_library::future_type>("future", *lib); - lib->add(chaiscript::fun( - [](const std::function &t_func) { return std::async(std::launch::async, t_func); }), - "async"); + // Note: async() is registered in ChaiScript_Basic::build_eval_system() + // with thread tracking to prevent heap-use-after-free on engine destruction. #endif json_wrap::library(*lib); diff --git a/include/chaiscript/dispatchkit/bootstrap.hpp b/include/chaiscript/dispatchkit/bootstrap.hpp index ba1de5ad..d9b3e23e 100644 --- a/include/chaiscript/dispatchkit/bootstrap.hpp +++ b/include/chaiscript/dispatchkit/bootstrap.hpp @@ -115,6 +115,8 @@ namespace chaiscript::bootstrap { m.add(fun(&parse_string), "to_" + name); m.add(fun([](const T t) { return t; }), "to_" + name); + m.add(fun([](const Boxed_Number &bn) { return bn.get_as(); }), "to_" + name); + m.add(fun(&parse_string), name); } /// "clone" function for a shared_ptr type. This is used in the case diff --git a/include/chaiscript/dispatchkit/dispatchkit.hpp b/include/chaiscript/dispatchkit/dispatchkit.hpp index b54d42b4..be43905d 100644 --- a/include/chaiscript/dispatchkit/dispatchkit.hpp +++ b/include/chaiscript/dispatchkit/dispatchkit.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -370,6 +371,28 @@ namespace chaiscript { , m_parser(parser) { } + ~Dispatch_Engine() { + join_async_threads(); + } + + Dispatch_Engine(const Dispatch_Engine &) = delete; + Dispatch_Engine &operator=(const Dispatch_Engine &) = delete; + Dispatch_Engine(Dispatch_Engine &&) = default; + Dispatch_Engine &operator=(Dispatch_Engine &&) = default; + +#ifndef CHAISCRIPT_NO_THREADS + /// Track an async thread so it can be joined during destruction + void track_async_thread(std::thread t_thread) { + chaiscript::detail::threading::unique_lock l(m_async_mutex); + // Clean up already-finished threads to avoid unbounded growth + m_async_threads.erase( + std::remove_if(m_async_threads.begin(), m_async_threads.end(), + [](std::thread &t) { return !t.joinable(); }), + m_async_threads.end()); + m_async_threads.push_back(std::move(t_thread)); + } +#endif + /// \brief casts an object while applying any Dynamic_Conversion available template decltype(auto) boxed_cast(const Boxed_Value &bv) const { @@ -1064,13 +1087,21 @@ namespace chaiscript { // overridden methods in derived classes are tried first during dispatch const auto &lhs_dotn = lhs->dynamic_object_type_name(); const auto &rhs_dotn = rhs->dynamic_object_type_name(); - if (!lhs_dotn.empty() && !rhs_dotn.empty() && lhs_dotn != rhs_dotn) { - if (dispatch::Dynamic_Object::type_matches(lhs_dotn, rhs_dotn)) { - return true; // lhs is derived from rhs, so lhs is more specific + if (lhs_dotn != rhs_dotn) { + if (!lhs_dotn.empty() && !rhs_dotn.empty()) { + if (dispatch::Dynamic_Object::type_matches(lhs_dotn, rhs_dotn)) { + return true; // lhs is derived from rhs, so lhs is more specific + } + if (dispatch::Dynamic_Object::type_matches(rhs_dotn, lhs_dotn)) { + return false; // rhs is derived from lhs, so rhs is more specific + } } - if (dispatch::Dynamic_Object::type_matches(rhs_dotn, lhs_dotn)) { - return false; // rhs is derived from lhs, so rhs is more specific + // Impose a total order on type names to maintain strict-weak ordering: + // non-empty names sort before empty names, then lexicographically + if (lhs_dotn.empty() != rhs_dotn.empty()) { + return !lhs_dotn.empty(); } + return lhs_dotn < rhs_dotn; } const auto &lhsparamtypes = lhs->get_param_types(); @@ -1120,7 +1151,9 @@ namespace chaiscript { return lt < rt; } - return false; + // When all overlapping parameters match, order by arity to maintain + // strict-weak ordering (transitivity of equivalence) + return lhssize < rhssize; } /// Implementation detail for adding a function. @@ -1165,6 +1198,21 @@ namespace chaiscript { get_function_objects_int().insert_or_assign(t_name, std::move(new_func)); } + void join_async_threads() { +#ifndef CHAISCRIPT_NO_THREADS + std::vector threads; + { + chaiscript::detail::threading::unique_lock l(m_async_mutex); + threads = std::move(m_async_threads); + } + for (auto &t : threads) { + if (t.joinable()) { + t.join(); + } + } +#endif + } + mutable chaiscript::detail::threading::shared_mutex m_mutex; Type_Conversions m_conversions; @@ -1174,6 +1222,11 @@ namespace chaiscript { mutable std::atomic_uint_fast32_t m_method_missing_loc = {0}; State m_state; + +#ifndef CHAISCRIPT_NO_THREADS + mutable chaiscript::detail::threading::shared_mutex m_async_mutex; + std::vector m_async_threads; +#endif }; class Dispatch_State { diff --git a/include/chaiscript/dispatchkit/proxy_functions.hpp b/include/chaiscript/dispatchkit/proxy_functions.hpp index fe65ee5a..002e473b 100644 --- a/include/chaiscript/dispatchkit/proxy_functions.hpp +++ b/include/chaiscript/dispatchkit/proxy_functions.hpp @@ -798,6 +798,14 @@ namespace chaiscript { ++numdiffs; } } + + // Deprioritize functions whose first parameter (object/receiver) requires + // type conversion: conversions create temporaries, so mutations on the + // converted object are silently lost. + if (plist.size() > 1 && !func->get_param_types()[1].bare_equal(plist[0].get_type_info())) { + numdiffs = plist.size(); + } + ordered_funcs.emplace_back(numdiffs, func.get()); } } diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index c9f22e58..50dfb3c7 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -179,6 +180,25 @@ namespace chaiscript { }), "namespace"); m_engine.add(fun([this](const std::string &t_namespace_name) { import(t_namespace_name); }), "import"); + +#ifndef CHAISCRIPT_NO_THREADS + // Register async() with thread tracking so the engine can join all + // async threads before destroying shared state (issues #632, #636). + m_engine.add(chaiscript::fun( + [this](const std::function &t_func) { + auto promise_ptr = std::make_shared>(); + auto future = promise_ptr->get_future(); + m_engine.track_async_thread(std::thread([promise_ptr, t_func]() { + try { + promise_ptr->set_value(t_func()); + } catch (...) { + promise_ptr->set_exception(std::current_exception()); + } + })); + return future; + }), + "async"); +#endif } /// Skip BOM at the beginning of file diff --git a/unittests/async_engine_lifetime.chai b/unittests/async_engine_lifetime.chai new file mode 100644 index 00000000..94ee7066 --- /dev/null +++ b/unittests/async_engine_lifetime.chai @@ -0,0 +1,17 @@ +// Regression test for #632 and #636: Heap-use-after-free in async threads +// Async threads must complete before the engine is destroyed. + +var func = fun(){ + var ret = 0; + for (var i = 0; i < 1000; ++i) { + ret += i; + } + return ret; +} + +var fut1 = async(func); +var fut2 = async(func); + +// Wait for results to verify correctness +assert_equal(fut1.get(), 499500); +assert_equal(fut2.get(), 499500); diff --git a/unittests/async_engine_lifetime_test.cpp b/unittests/async_engine_lifetime_test.cpp new file mode 100644 index 00000000..698f64c0 --- /dev/null +++ b/unittests/async_engine_lifetime_test.cpp @@ -0,0 +1,46 @@ +// Regression test for #632 and #636: +// Heap-use-after-free when async threads outlive the ChaiScript engine. +// The engine must join all outstanding async threads before destroying shared state. + +#include + +int main() { + // Test 1: Async threads still running when engine is destroyed. + // Without the fix, this triggers heap-use-after-free under ASAN/TSAN. + for (int iter = 0; iter < 3; ++iter) { + { + chaiscript::ChaiScript chai; + try { + chai.eval(R"( + var func = fun(){ + var ret = 0; + for (var i = 0; i < 5000; ++i) { + ret += i; + } + return ret; + } + + var fut1 = async(func); + var fut2 = async(func); + )"); + // Engine destroyed here without waiting for futures. + } catch (const std::exception &) { + // Exceptions are fine + } + } + } + + // Test 2: Verify async still works correctly (results are accessible). + { + chaiscript::ChaiScript chai; + auto result = chai.eval(R"( + var f = async(fun() { return 42; }); + f.get(); + )"); + if (result != 42) { + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index b788b070..0a2d252b 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -1462,6 +1462,87 @@ TEST_CASE("Issue #524 - vector of unique_ptr can be registered") { CHECK(chai.eval("var v2 = UniqueVec(); v2.empty()") == true); } +// Issue #625: function_less_than comparator must satisfy strict-weak ordering. +// Registering overloaded functions with different arities triggered a +// std::stable_sort assertion on macOS 15.2 (hardened libc++) because the +// comparator violated transitivity of equivalence. +TEST_CASE("Issue 625: function_less_than strict-weak ordering with different arities") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + // Register overloaded functions with varying arities under the same name. + // If the comparator doesn't order by arity when overlapping params match, + // std::stable_sort may exhibit undefined behavior. + CHECK_NOTHROW(chai.add(chaiscript::fun([](int x, const chaiscript::Boxed_Value &) { return x; }), "overloaded")); + CHECK_NOTHROW(chai.add(chaiscript::fun([](int x, double y) { return x + static_cast(y); }), "overloaded")); + CHECK_NOTHROW(chai.add(chaiscript::fun([](int x) { return x; }), "overloaded")); + + // Verify dispatch still works correctly + CHECK(chai.eval("overloaded(5)") == 5); + CHECK(chai.eval("overloaded(3, 2.0)") == 5); +} + +// Regression test: push_back() on script-created vector has no effect when +// vector_conversion is in effect. The bug occurs because dispatch selects +// the C++ push_back for the converted type over the built-in one, operating +// on a temporary copy of the vector. +TEST_CASE("push_back on script vector with vector_conversion") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + auto m = std::make_shared(); + chaiscript::bootstrap::standard_library::vector_type>("VectorString", *m); + m->add(chaiscript::vector_conversion>()); + chai.add(m); + + // Register a C++ function that accepts the converted type, so we can + // verify that vector_conversion actually works for passing vectors + chai.add(chaiscript::fun([](const std::vector &v) -> std::string { + std::string result; + for (const auto &s : v) { + if (!result.empty()) { result += ","; } + result += s; + } + return result; + }), "join_strings"); + + // push_back on an empty script-created vector must be visible + CHECK(chai.eval( + "auto x = [];" + "x.push_back(\"Hello\");" + "x.size() == 1" + )); + + // push_back on a vector with initial elements must grow correctly + CHECK(chai.eval( + "auto y = [\"a\", \"b\"];" + "y.push_back(\"c\");" + "y.push_back(\"d\");" + "y.size() == 4" + )); + + // Verify the actual content is preserved after push_back + CHECK(chai.eval( + "auto z = [];" + "z.push_back(\"World\");" + "z[0]" + ) == "World"); + + // Round-trip: build a vector in script, push_back elements, then pass it + // to a C++ function via vector_conversion and verify the contents + CHECK(chai.eval( + "auto v = [\"one\", \"two\"];" + "v.push_back(\"three\");" + "join_strings(v)" + ) == "one,two,three"); + + // Verify conversion works on a freshly created vector too + CHECK(chai.eval( + "auto w = [];" + "w.push_back(\"hello\");" + "w.push_back(\"world\");" + "join_strings(w)" + ) == "hello,world"); +} + // Regression test for issue #607: AST_Node_Trace must be a complete type // when used in eval_error's std::vector call_stack member. // This failed to compile with C++20 on clang/libc++ when AST_Node_Trace diff --git a/unittests/conversion_functions.chai b/unittests/conversion_functions.chai new file mode 100644 index 00000000..4f51e101 --- /dev/null +++ b/unittests/conversion_functions.chai @@ -0,0 +1,33 @@ +// Test that all conversion functions work across char, int, and string types + +// char() constructor +assert_equal('A', char('A')) // char from char (identity via Boxed_Number) +assert_equal('A', char(65)) // char from int (via Boxed_Number) +assert_equal('A', char("A")) // char from string (new) + +// to_char() conversions +assert_equal('A', to_char('A')) // to_char from char (identity) +assert_equal('A', to_char(65)) // to_char from int (new, via Boxed_Number) +assert_equal('A', to_char("A")) // to_char from string (existing) + +// int() constructor +assert_equal(65, int('A')) // int from char (via Boxed_Number) +assert_equal(65, int(65)) // int from int (identity via Boxed_Number) +assert_equal(65, int("65")) // int from string (new) + +// to_int() conversions +assert_equal(65, to_int('A')) // to_int from char (new, via Boxed_Number) +assert_equal(65, to_int(65)) // to_int from int (identity) +assert_equal(65, to_int("65")) // to_int from string (existing) + +// to_string() conversions +assert_equal("A", to_string('A')) +assert_equal("65", to_string(65)) +assert_equal("A", to_string("A")) + +// double conversions +assert_equal(3.5, double("3.5")) // double from string (new) +assert_equal(3.5, to_double("3.5")) // to_double from string (existing) +assert_equal(65.0, to_double('A')) // to_double from char (new, via Boxed_Number) +assert_equal(65.0, to_double(65)) // to_double from int (new, via Boxed_Number) +assert_equal("3.5", to_string(3.5)) // to_string from double (existing) diff --git a/unittests/emscripten_eval_test.cpp b/unittests/emscripten_eval_test.cpp new file mode 100644 index 00000000..6084c9d3 --- /dev/null +++ b/unittests/emscripten_eval_test.cpp @@ -0,0 +1,51 @@ +// Test that validates the eval patterns used by the Emscripten wrapper. +// Based on work by Rob Loach (https://github.com/RobLoach/ChaiScript.js) + +#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 + +int main() { + // Test eval (void return) - same as Emscripten eval() + chaiscript_eval("var x = 42"); + + // Test evalString - same as Emscripten evalString() + std::string s = chaiscript_eval_string("to_string(x)"); + assert(s == "42"); + + // Test evalInt - same as Emscripten evalInt() + int i = chaiscript_eval_int("1 + 2"); + assert(i == 3); + + // Test evalBool - same as Emscripten evalBool() + bool b = chaiscript_eval_bool("true"); + assert(b == true); + + b = chaiscript_eval_bool("false"); + assert(b == false); + + // Test evalFloat - same as Emscripten evalFloat() + float f = chaiscript_eval_float("1.5f"); + assert(std::abs(f - 1.5f) < 0.001f); + + // Test evalDouble - same as Emscripten evalDouble() + double d = chaiscript_eval_double("3.14"); + assert(std::abs(d - 3.14) < 0.001); + + // Test a more complex expression + chaiscript_eval("def square(n) { return n * n; }"); + int sq = chaiscript_eval_int("square(7)"); + assert(sq == 49); + + return 0; +}