Fix #206: Make internal string type a template parameter

Add a StringType template parameter (defaulting to std::string) to
ChaiScript_Parser, Bootstrap::bootstrap(), and Std_Lib::library(),
flowing through to the ChaiScript_Impl convenience class. This allows
users to instantiate ChaiScript with std::wstring (via ChaiScript_WString)
or other string types. String literals, escape sequences (including
unicode for wide chars via if constexpr), to_string conversions, and
string operations all respect the parameterized type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-13 19:30:57 -06:00
parent d4c5bdb3e4
commit f1e0fb35da
6 changed files with 172 additions and 47 deletions

View File

@ -444,6 +444,10 @@ if(BUILD_TESTING)
target_link_libraries(threading_config_test ${LIBS})
add_test(NAME Threading_Config_Test COMMAND threading_config_test)
add_executable(string_type_param_test unittests/string_type_param_test.cpp)
target_link_libraries(string_type_param_test ${LIBS})
add_test(NAME String_Type_Param_Test COMMAND string_type_param_test)
install(TARGETS test_module RUNTIME DESTINATION bin LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}/chaiscript")
endif()
endif()

View File

@ -820,20 +820,24 @@
#include "language/chaiscript_parser.hpp"
namespace chaiscript {
class ChaiScript : public ChaiScript_Basic {
template<typename StringType = std::string>
class ChaiScript_Impl : public ChaiScript_Basic {
public:
ChaiScript(std::vector<std::string> t_modulepaths = {},
ChaiScript_Impl(std::vector<std::string> t_modulepaths = {},
std::vector<std::string> t_usepaths = {},
std::vector<Options> t_opts = chaiscript::default_options(),
std::vector<Library_Options> t_lib_opts = {})
: ChaiScript_Basic(chaiscript::Std_Lib::library(t_lib_opts),
std::make_unique<parser::ChaiScript_Parser<eval::Noop_Tracer, optimizer::Optimizer_Default>>(),
: ChaiScript_Basic(chaiscript::Std_Lib::library<StringType>(t_lib_opts),
std::make_unique<parser::ChaiScript_Parser<eval::Noop_Tracer, optimizer::Optimizer_Default, StringType>>(),
std::move(t_modulepaths),
std::move(t_usepaths),
std::move(t_opts),
std::find(t_lib_opts.begin(), t_lib_opts.end(), Library_Options::No_IO) != t_lib_opts.end()) {
}
};
using ChaiScript = ChaiScript_Impl<std::string>;
using ChaiScript_WString = ChaiScript_Impl<std::wstring>;
} // namespace chaiscript
#endif /* CHAISCRIPT_HPP_ */

View File

@ -38,6 +38,7 @@
namespace chaiscript {
class Std_Lib {
public:
template<typename StringType = std::string>
[[nodiscard]] static ModulePtr library(const std::vector<Library_Options> &t_opts = {}) {
if (std::find(t_opts.begin(), t_opts.end(), Library_Options::No_Stdlib) != t_opts.end()) {
return std::make_shared<Module>();
@ -49,10 +50,10 @@ namespace chaiscript {
const bool no_prelude = std::find(t_opts.begin(), t_opts.end(), Library_Options::No_Prelude) != t_opts.end();
const bool no_json = std::find(t_opts.begin(), t_opts.end(), Library_Options::No_JSON) != t_opts.end();
bootstrap::Bootstrap::bootstrap(*lib, no_io);
bootstrap::Bootstrap::bootstrap<StringType>(*lib, no_io);
bootstrap::standard_library::vector_type<std::vector<Boxed_Value>>("Vector", *lib);
bootstrap::standard_library::string_type<std::string>("string", *lib);
bootstrap::standard_library::string_type<StringType>("string", *lib);
bootstrap::standard_library::map_type<std::map<std::string, Boxed_Value>>("Map", *lib);
bootstrap::standard_library::pair_type<std::pair<Boxed_Value, Boxed_Value>>("Pair", *lib);

View File

@ -10,6 +10,8 @@
#ifndef CHAISCRIPT_BOOTSTRAP_HPP_
#define CHAISCRIPT_BOOTSTRAP_HPP_
#include <type_traits>
#include "../utility/utility.hpp"
#include "register_function.hpp"
@ -269,6 +271,7 @@ namespace chaiscript::bootstrap {
/// \brief perform all common bootstrap functions for std::string, void and POD types
/// \param[in,out] m Module to add bootstrapped functions to
/// \param[in] t_no_io If true, skip registering print_string and println_string
template<typename StringType = std::string>
static void bootstrap(Module &m, const bool t_no_io = false) {
m.add(user_type<void>(), "void");
m.add(user_type<bool>(), "bool");
@ -393,13 +396,27 @@ namespace chaiscript::bootstrap {
operators::equal<bool>(m);
operators::not_equal<bool>(m);
m.add(fun([](const std::string &s) { return s; }), "to_string");
m.add(fun([](const bool b) { return std::string(b ? "true" : "false"); }), "to_string");
m.add(fun([](const StringType &s) { return s; }), "to_string");
m.add(fun([](const bool b) -> StringType {
if constexpr (std::is_same_v<StringType, std::string>) {
return b ? "true" : "false";
} else {
const auto s = std::string(b ? "true" : "false");
return StringType(s.begin(), s.end());
}
}), "to_string");
m.add(fun(&unknown_assign), "=");
m.add(fun([](const Boxed_Value &bv) { throw bv; }), "throw");
m.add(fun([](const char c) { return std::string(1, c); }), "to_string");
m.add(fun(&Boxed_Number::to_string), "to_string");
m.add(fun([](const typename StringType::value_type c) -> StringType { return StringType(1, c); }), "to_string");
if constexpr (std::is_same_v<StringType, std::string>) {
m.add(fun(&Boxed_Number::to_string), "to_string");
} else {
m.add(fun([](const Boxed_Number &n) -> StringType {
const auto s = n.to_string();
return StringType(s.begin(), s.end());
}), "to_string");
}
bootstrap_pod_type<double>("double", m);
bootstrap_pod_type<long double>("long_double", m);

View File

@ -17,6 +17,7 @@
#include <memory>
#include <sstream>
#include <string>
#include <type_traits>
#include <vector>
#include "../dispatchkit/boxed_value.hpp"
@ -101,9 +102,31 @@ namespace chaiscript {
return Char_Parser_Helper<std::true_type>::u8str_from_ll(val);
}
};
template<typename S>
int stoi_for_string(const S &s, std::size_t *pos, int base) {
if constexpr (std::is_same_v<S, std::string>) {
return std::stoi(s, pos, base);
} else if constexpr (std::is_same_v<S, std::wstring>) {
return std::stoi(std::wstring(s.begin(), s.end()), pos, base);
} else {
return std::stoi(std::string(s.begin(), s.end()), pos, base);
}
}
template<typename S>
long long stoll_for_string(const S &s, std::size_t *pos, int base) {
if constexpr (std::is_same_v<S, std::string>) {
return std::stoll(s, pos, base);
} else if constexpr (std::is_same_v<S, std::wstring>) {
return std::stoll(std::wstring(s.begin(), s.end()), pos, base);
} else {
return std::stoll(std::string(s.begin(), s.end()), pos, base);
}
}
} // namespace detail
template<typename Tracer, typename Optimizer, std::size_t Parse_Depth = 512>
template<typename Tracer, typename Optimizer, typename StringType = std::string, std::size_t Parse_Depth = 512>
class ChaiScript_Parser final : public ChaiScript_Parser_Base {
void *get_tracer_ptr() noexcept override { return &m_tracer; }
@ -812,6 +835,17 @@ namespace chaiscript {
#endif
}
template<typename S>
static std::string to_narrow(const S &s) {
if constexpr (std::is_same_v<S, std::string>) {
return s;
} else if constexpr (std::is_convertible_v<S, std::string_view>) {
return std::string(s);
} else {
return std::string(s.begin(), s.end());
}
}
template<typename T, typename... Param>
std::unique_ptr<eval::AST_Node_Impl<Tracer>>
make_node(std::string_view t_match, const int t_prev_line, const int t_prev_col, Param &&...param) {
@ -1100,8 +1134,8 @@ namespace chaiscript {
void process_hex() {
if (!hex_matches.empty()) {
auto val = stoll(hex_matches, nullptr, 16);
match.push_back(char_type(val));
const auto val = detail::stoll_for_string(hex_matches, nullptr, 16);
match.push_back(static_cast<char_type>(val));
}
hex_matches.clear();
is_escaped = false;
@ -1110,8 +1144,8 @@ namespace chaiscript {
void process_octal() {
if (!octal_matches.empty()) {
auto val = stoll(octal_matches, nullptr, 8);
match.push_back(char_type(val));
const auto val = detail::stoll_for_string(octal_matches, nullptr, 8);
match.push_back(static_cast<char_type>(val));
}
octal_matches.clear();
is_escaped = false;
@ -1119,14 +1153,13 @@ namespace chaiscript {
}
void process_unicode() {
const auto ch = static_cast<uint32_t>(std::stoi(hex_matches, nullptr, 16));
const auto ch = static_cast<uint32_t>(detail::stoi_for_string<string_type>(hex_matches, nullptr, 16));
const auto match_size = hex_matches.size();
hex_matches.clear();
is_escaped = false;
const auto u_size = unicode_size;
unicode_size = 0;
char buf[4];
if (u_size != match_size) {
throw exception::eval_error("Incomplete unicode escape sequence");
}
@ -1134,26 +1167,44 @@ namespace chaiscript {
throw exception::eval_error("Invalid 16 bit universal character");
}
if (ch < 0x80) {
match += static_cast<char>(ch);
} else if (ch < 0x800) {
buf[0] = static_cast<char>(0xC0 | (ch >> 6));
buf[1] = static_cast<char>(0x80 | (ch & 0x3F));
match.append(buf, 2);
} else if (ch < 0x10000) {
buf[0] = static_cast<char>(0xE0 | (ch >> 12));
buf[1] = static_cast<char>(0x80 | ((ch >> 6) & 0x3F));
buf[2] = static_cast<char>(0x80 | (ch & 0x3F));
match.append(buf, 3);
} else if (ch < 0x200000) {
buf[0] = static_cast<char>(0xF0 | (ch >> 18));
buf[1] = static_cast<char>(0x80 | ((ch >> 12) & 0x3F));
buf[2] = static_cast<char>(0x80 | ((ch >> 6) & 0x3F));
buf[3] = static_cast<char>(0x80 | (ch & 0x3F));
match.append(buf, 4);
if constexpr (sizeof(char_type) >= 4) {
if (ch < 0x200000) {
match.push_back(static_cast<char_type>(ch));
} else {
throw exception::eval_error("Invalid 32 bit universal character");
}
} else if constexpr (sizeof(char_type) >= 2) {
if (ch < 0x10000) {
match.push_back(static_cast<char_type>(ch));
} else if (ch < 0x110000) {
const auto adjusted = ch - 0x10000;
match.push_back(static_cast<char_type>(0xD800 + (adjusted >> 10)));
match.push_back(static_cast<char_type>(0xDC00 + (adjusted & 0x3FF)));
} else {
throw exception::eval_error("Invalid 32 bit universal character");
}
} else {
// this must be an invalid escape sequence?
throw exception::eval_error("Invalid 32 bit universal character");
char buf[4];
if (ch < 0x80) {
match += static_cast<char>(ch);
} else if (ch < 0x800) {
buf[0] = static_cast<char>(0xC0 | (ch >> 6));
buf[1] = static_cast<char>(0x80 | (ch & 0x3F));
match.append(buf, 2);
} else if (ch < 0x10000) {
buf[0] = static_cast<char>(0xE0 | (ch >> 12));
buf[1] = static_cast<char>(0x80 | ((ch >> 6) & 0x3F));
buf[2] = static_cast<char>(0x80 | (ch & 0x3F));
match.append(buf, 3);
} else if (ch < 0x200000) {
buf[0] = static_cast<char>(0xF0 | (ch >> 18));
buf[1] = static_cast<char>(0x80 | ((ch >> 12) & 0x3F));
buf[2] = static_cast<char>(0x80 | ((ch >> 6) & 0x3F));
buf[3] = static_cast<char>(0x80 | (ch & 0x3F));
match.append(buf, 4);
} else {
throw exception::eval_error("Invalid 32 bit universal character");
}
}
}
@ -1280,11 +1331,11 @@ namespace chaiscript {
const auto start = m_position;
if (Quoted_String_()) {
std::string match;
StringType match;
const auto prev_stack_top = m_match_stack.size();
bool is_interpolated = [&]() -> bool {
Char_Parser<std::string> cparser(match, true);
Char_Parser<StringType> cparser(match, true);
auto s = start + 1, end = m_position - 1;
@ -1293,7 +1344,7 @@ namespace chaiscript {
if (*s == '{') {
// We've found an interpolation point
m_match_stack.push_back(make_node<eval::Constant_AST_Node<Tracer>>(match, start.line, start.col, const_var(match)));
m_match_stack.push_back(make_node<eval::Constant_AST_Node<Tracer>>(to_narrow(match), start.line, start.col, const_var(match)));
if (cparser.is_interpolated) {
// If we've seen previous interpolation, add on instead of making a new one
@ -1351,7 +1402,7 @@ namespace chaiscript {
return cparser.is_interpolated;
}();
m_match_stack.push_back(make_node<eval::Constant_AST_Node<Tracer>>(match, start.line, start.col, const_var(match)));
m_match_stack.push_back(make_node<eval::Constant_AST_Node<Tracer>>(to_narrow(match), start.line, start.col, const_var(match)));
if (is_interpolated) {
build_match<eval::Binary_Operator_AST_Node<Tracer>>(prev_stack_top, "+");
@ -1439,7 +1490,7 @@ namespace chaiscript {
close_seq += '"';
// Extract raw content up to closing sequence
std::string match;
StringType match;
auto end = m_position; // m_position is already past the closing sequence
// Content is from s up to (end - close_seq.size())
@ -1456,11 +1507,11 @@ namespace chaiscript {
break;
}
}
match.push_back(*s);
match.push_back(static_cast<typename StringType::value_type>(*s));
++s;
}
m_match_stack.push_back(make_node<eval::Constant_AST_Node<Tracer>>(match, start.line, start.col, const_var(match)));
m_match_stack.push_back(make_node<eval::Constant_AST_Node<Tracer>>(to_narrow(match), start.line, start.col, const_var(match)));
return true;
}
return false;
@ -1501,11 +1552,11 @@ namespace chaiscript {
const auto start = m_position;
if (Single_Quoted_String_()) {
std::string match;
StringType match;
{
// scope for cparser destructor
Char_Parser<std::string> cparser(match, false);
Char_Parser<StringType> cparser(match, false);
for (auto s = start + 1, end = m_position - 1; s != end; ++s) {
cparser.parse(*s, start.line, start.col, *m_filename);
@ -1518,7 +1569,8 @@ namespace chaiscript {
*m_filename);
}
m_match_stack.push_back(make_node<eval::Constant_AST_Node<Tracer>>(match, start.line, start.col, const_var(char(match.at(0)))));
m_match_stack.push_back(make_node<eval::Constant_AST_Node<Tracer>>(to_narrow(match), start.line, start.col,
const_var(static_cast<typename StringType::value_type>(match.at(0)))));
return true;
} else {
return false;
@ -2805,7 +2857,7 @@ namespace chaiscript {
}
AST_NodePtr parse(const std::string &t_input, const std::string &t_fname) override {
ChaiScript_Parser<Tracer, Optimizer> parser(m_tracer, m_optimizer);
ChaiScript_Parser<Tracer, Optimizer, StringType> parser(m_tracer, m_optimizer);
return parser.parse_internal(t_input, t_fname);
}

View File

@ -0,0 +1,47 @@
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4062 4242 4566 4640 4702 6330 28251)
#endif
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunknown-pragmas"
#pragma GCC diagnostic ignored "-Wparentheses"
#pragma GCC diagnostic ignored "-Wignored-qualifiers"
#endif
#include <chaiscript/chaiscript.hpp>
#include <chaiscript/chaiscript_basic.hpp>
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
TEST_CASE("String type can be parameterized to wstring") {
chaiscript::ChaiScript_WString chai;
SECTION("String literals produce std::wstring") {
auto result = chai.eval<std::wstring>("\"hello\"");
CHECK(result == L"hello");
}
SECTION("String concatenation works with wstring") {
auto result = chai.eval<std::wstring>("\"hello\" + \" world\"");
CHECK(result == L"hello world");
}
SECTION("to_string works for numbers with wstring") {
auto result = chai.eval<std::wstring>("to_string(42)");
CHECK(result == L"42");
}
SECTION("String interpolation works with wstring") {
auto result = chai.eval<std::wstring>("var x = 5; \"value: ${x}\"");
CHECK(result == L"value: 5");
}
SECTION("Default ChaiScript still uses std::string") {
chaiscript::ChaiScript default_chai;
auto result = default_chai.eval<std::string>("\"hello\"");
CHECK(result == "hello");
}
}