Address review: fix merge conflicts with develop

Requested by @lefticus in PR #657 review.
This commit is contained in:
leftibot 2026-04-11 16:16:25 -06:00
commit 973f9c554c
16 changed files with 705 additions and 3 deletions

71
.github/workflows/emscripten.yml vendored Normal file
View File

@ -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

View File

@ -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()

35
emscripten/CMakeLists.txt Normal file
View File

@ -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"
)

237
emscripten/chaiscript.html Normal file
View File

@ -0,0 +1,237 @@
<!doctype html>
<!--
ChaiScript Emscripten Frontend
Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js
Copyright 2019, Rob Loach
Copyright 2009-2018, Jason Turner
Licensed under the BSD License. See "license.txt" for details.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ChaiScript</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: #16213e;
padding: 1rem 2rem;
border-bottom: 2px solid #0f3460;
display: flex;
align-items: center;
gap: 1rem;
}
header h1 {
font-size: 1.5rem;
color: #e94560;
font-weight: 700;
}
header span {
font-size: 0.85rem;
color: #888;
}
#status {
margin-left: auto;
font-size: 0.85rem;
padding: 0.3rem 0.8rem;
border-radius: 4px;
background: #0f3460;
}
#status.ready { color: #4ecca3; }
#status.loading { color: #e9c46a; }
#status.error { color: #e94560; }
main {
flex: 1;
display: flex;
gap: 0;
}
.panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.panel-header {
background: #16213e;
padding: 0.5rem 1rem;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #888;
border-bottom: 1px solid #0f3460;
}
#input {
flex: 1;
background: #0d1117;
color: #c9d1d9;
border: none;
padding: 1rem;
font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
font-size: 0.9rem;
line-height: 1.6;
resize: none;
outline: none;
tab-size: 2;
}
.divider {
width: 2px;
background: #0f3460;
}
#output {
flex: 1;
background: #0d1117;
padding: 1rem;
font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
font-size: 0.9rem;
line-height: 1.6;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.output-line { color: #c9d1d9; }
.output-error { color: #e94560; }
footer {
background: #16213e;
padding: 0.5rem 1rem;
display: flex;
gap: 0.5rem;
align-items: center;
border-top: 2px solid #0f3460;
}
button {
background: #e94560;
color: #fff;
border: none;
padding: 0.4rem 1.2rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
}
button:hover { background: #c73652; }
button:disabled { background: #555; cursor: not-allowed; }
#btn-clear {
background: #0f3460;
}
#btn-clear:hover { background: #1a4a8a; }
.hint {
margin-left: auto;
font-size: 0.75rem;
color: #555;
}
</style>
</head>
<body>
<header>
<h1>ChaiScript</h1>
<span>Interactive Playground</span>
<div id="status" class="loading">Loading...</div>
</header>
<main>
<div class="panel">
<div class="panel-header">Input</div>
<textarea id="input" spellcheck="false">// Welcome to ChaiScript!
// Write your code here and click Run (or press Ctrl+Enter).
def greet(name) {
return "Hello, " + name + "!"
}
print(greet("World"))
print(greet("ChaiScript"))
// Math example
def factorial(n) {
if (n <= 1) { return 1 }
return n * factorial(n - 1)
}
print("5! = " + to_string(factorial(5)))
print("10! = " + to_string(factorial(10)))
</textarea>
</div>
<div class="divider"></div>
<div class="panel">
<div class="panel-header">Output</div>
<div id="output"></div>
</div>
</main>
<footer>
<button id="btn-run" disabled>Run</button>
<button id="btn-clear">Clear</button>
<span class="hint">Ctrl+Enter to run</span>
</footer>
<script>
var outputEl = document.getElementById('output');
var inputEl = document.getElementById('input');
var btnRun = document.getElementById('btn-run');
var btnClear = document.getElementById('btn-clear');
var statusEl = document.getElementById('status');
function appendOutput(text, className) {
var line = document.createElement('div');
line.className = className || 'output-line';
line.textContent = text;
outputEl.appendChild(line);
outputEl.scrollTop = outputEl.scrollHeight;
}
var Module = {
print: function(text) {
appendOutput(text);
},
printErr: function(text) {
appendOutput(text, 'output-error');
},
onRuntimeInitialized: function() {
statusEl.textContent = 'Ready';
statusEl.className = 'ready';
btnRun.disabled = false;
}
};
function runCode() {
var code = inputEl.value;
if (!code.trim()) return;
appendOutput('> Running...', 'output-line');
try {
Module.eval(code);
} catch (e) {
appendOutput('Error: ' + e.message, 'output-error');
}
appendOutput('', 'output-line');
}
btnRun.addEventListener('click', runCode);
btnClear.addEventListener('click', function() {
outputEl.innerHTML = '';
});
inputEl.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
runCode();
}
// Tab key inserts spaces
if (e.key === 'Tab') {
e.preventDefault();
var start = this.selectionStart;
var end = this.selectionEnd;
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 2;
}
});
</script>
<script src="chaiscript.js"></script>
</body>
</html>

View File

@ -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/bind.h>
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

View File

@ -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 <string>
#include <chaiscript/chaiscript.hpp>
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<std::string>(input);
}
inline bool chaiscript_eval_bool(const std::string &input) {
return detail::get_chai_instance().eval<bool>(input);
}
inline int chaiscript_eval_int(const std::string &input) {
return detail::get_chai_instance().eval<int>(input);
}
inline float chaiscript_eval_float(const std::string &input) {
return detail::get_chai_instance().eval<float>(input);
}
inline double chaiscript_eval_double(const std::string &input) {
return detail::get_chai_instance().eval<double>(input);
}
#endif /* CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ */

View File

@ -49,9 +49,8 @@ namespace chaiscript {
#ifndef CHAISCRIPT_NO_THREADS
bootstrap::standard_library::future_type<std::future<chaiscript::Boxed_Value>>("future", *lib);
lib->add(chaiscript::fun(
[](const std::function<chaiscript::Boxed_Value()> &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);

View File

@ -115,6 +115,8 @@ namespace chaiscript::bootstrap {
m.add(fun(&parse_string<T>), "to_" + name);
m.add(fun([](const T t) { return t; }), "to_" + name);
m.add(fun([](const Boxed_Number &bn) { return bn.get_as<T>(); }), "to_" + name);
m.add(fun(&parse_string<T>), name);
}
/// "clone" function for a shared_ptr type. This is used in the case

View File

@ -19,6 +19,7 @@
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <typeinfo>
#include <utility>
#include <vector>
@ -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<chaiscript::detail::threading::shared_mutex> 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<typename Type>
decltype(auto) boxed_cast(const Boxed_Value &bv) const {
@ -1165,6 +1188,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<std::thread> threads;
{
chaiscript::detail::threading::unique_lock<chaiscript::detail::threading::shared_mutex> 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 +1212,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<std::thread> m_async_threads;
#endif
};
class Dispatch_State {

View File

@ -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());
}
}

View File

@ -15,6 +15,7 @@
#include <exception>
#include <fstream>
#include <functional>
#include <future>
#include <map>
#include <memory>
#include <mutex>
@ -190,6 +191,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<chaiscript::Boxed_Value()> &t_func) {
auto promise_ptr = std::make_shared<std::promise<chaiscript::Boxed_Value>>();
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

View File

@ -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);

View File

@ -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 <chaiscript/chaiscript.hpp>
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<int>(R"(
var f = async(fun() { return 42; });
f.get();
)");
if (result != 42) {
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}

View File

@ -1502,6 +1502,68 @@ TEST_CASE("IO redirection different instances are independent") {
CHECK(output2 == "from2");
}
// 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::Module>();
chaiscript::bootstrap::standard_library::vector_type<std::vector<std::string>>("VectorString", *m);
m->add(chaiscript::vector_conversion<std::vector<std::string>>());
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<std::string> &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<bool>(
"auto x = [];"
"x.push_back(\"Hello\");"
"x.size() == 1"
));
// push_back on a vector with initial elements must grow correctly
CHECK(chai.eval<bool>(
"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<std::string>(
"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<std::string>(
"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<std::string>(
"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<AST_Node_Trace> call_stack member.
// This failed to compile with C++20 on clang/libc++ when AST_Node_Trace

View File

@ -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)

View File

@ -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 <chaiscript/chaiscript.hpp>
#include "../emscripten/chaiscript_eval.hpp"
#include <cassert>
#include <cmath>
#include <string>
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;
}