diff --git a/include/chaiscript/chaiscript_stdlib.hpp b/include/chaiscript/chaiscript_stdlib.hpp index 5c1e7a48..f2034b6c 100644 --- a/include/chaiscript/chaiscript_stdlib.hpp +++ b/include/chaiscript/chaiscript_stdlib.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -52,9 +53,14 @@ namespace chaiscript { bootstrap::standard_library::vector_type>("Vector", *lib); bootstrap::standard_library::string_type("string", *lib); + bootstrap::standard_library::string_view_type("string_view", *lib); bootstrap::standard_library::map_type>("Map", *lib); bootstrap::standard_library::pair_type>("Pair", *lib); + // Allow explicit conversion from std::string_view back to std::string, + // e.g. `string(sv)` in ChaiScript. + lib->add(fun([](const std::string_view sv) { return std::string{sv}; }), "string"); + #ifndef CHAISCRIPT_NO_THREADS bootstrap::standard_library::future_type>("future", *lib); // Note: async() is registered in ChaiScript_Basic::build_eval_system() diff --git a/include/chaiscript/dispatchkit/bootstrap_stl.hpp b/include/chaiscript/dispatchkit/bootstrap_stl.hpp index 967344d7..6ac2939c 100644 --- a/include/chaiscript/dispatchkit/bootstrap_stl.hpp +++ b/include/chaiscript/dispatchkit/bootstrap_stl.hpp @@ -539,6 +539,61 @@ namespace chaiscript::bootstrap::standard_library { m.add(fun([](const String *s, size_t pos, size_t len) { return s->substr(pos, len); }), "substr"); } + /// Add a String_View type (e.g. std::string_view), with conversions to and from + /// the matching owning String type (e.g. std::string). + /// + /// Mirrors the search/substring surface of string_type so that scripts can + /// traverse a buffer through StringView without allocating. Sharing method + /// names with the owning String type is safe: dispatch deprioritizes any + /// candidate whose first/receiver argument requires a type conversion (see + /// dispatch() in proxy_functions.hpp), so myString.substr(1, 2) still + /// resolves to String::substr while mySV.substr(1, 2) resolves to + /// StringView::substr (returning a StringView). + /// + /// \note A String_View is a non-owning reference. Constructing one from a + /// temporary owning String yields a dangling reference once that temporary + /// is destroyed; the same lifetime caveats as in C++ apply here. + template + void string_view_type(const std::string &type, Module &m) { + m.add(user_type(), type); + m.add(constructor(), type); + m.add(constructor(), type); + m.add(fun([](const String &s) { return StringView{s}; }), type); + + opers_comparison(m); + + m.add(fun([](const StringView *s) { return s->size(); }), "size"); + m.add(fun([](const StringView *s) { return s->length(); }), "length"); + m.add(fun([](const StringView *s) { return s->empty(); }), "empty"); + m.add(fun([](const StringView *s) { return s->data(); }), "data"); + + // Random-access via [] / at, returning const_reference; StringView is read-only. + m.add(fun([](const StringView &sv, int index) -> typename StringView::const_reference { + return sv.at(static_cast(index)); + }), + "[]"); + + m.add(fun([](const StringView *s, const StringView &f, size_t pos) { return s->find(f, pos); }), "find"); + m.add(fun([](const StringView *s, const StringView &f, size_t pos) { return s->rfind(f, pos); }), "rfind"); + m.add(fun([](const StringView *s, const StringView &f, size_t pos) { return s->find_first_of(f, pos); }), "find_first_of"); + m.add(fun([](const StringView *s, const StringView &f, size_t pos) { return s->find_last_of(f, pos); }), "find_last_of"); + m.add(fun([](const StringView *s, const StringView &f, size_t pos) { return s->find_last_not_of(f, pos); }), "find_last_not_of"); + m.add(fun([](const StringView *s, const StringView &f, size_t pos) { return s->find_first_not_of(f, pos); }), "find_first_not_of"); + + m.add(fun([](const StringView *s, const StringView &f) { return s->starts_with(f); }), "starts_with"); + m.add(fun([](const StringView *s, const StringView &f) { return s->ends_with(f); }), "ends_with"); + + m.add(fun([](const StringView *s, size_t pos, size_t len) { return s->substr(pos, len); }), "substr"); + + // Built-in implicit conversion from owning String to non-owning StringView. + m.add(type_conversion()); + + // Explicit conversion from StringView back to owning String, registered as + // to_string(sv); the call site can also register it under the owning type's + // name (e.g. string(sv)) when desired. + m.add(fun([](const StringView sv) { return String{sv}; }), "to_string"); + } + /// Add a MapType container /// http://www.sgi.com/tech/stl/Map.html template diff --git a/include/chaiscript/dispatchkit/proxy_functions.hpp b/include/chaiscript/dispatchkit/proxy_functions.hpp index 07cb55fa..e0e10163 100644 --- a/include/chaiscript/dispatchkit/proxy_functions.hpp +++ b/include/chaiscript/dispatchkit/proxy_functions.hpp @@ -737,11 +737,24 @@ namespace chaiscript { if (matching_func == end) { matching_func = begin; } else { - // handle const members vs non-const member, which is not really ambiguous const auto &mat_fun_param_types = matching_func->second->get_param_types(); const auto &next_fun_param_types = begin->second->get_param_types(); - if (plist[0].is_const() && !mat_fun_param_types[1].is_const() && next_fun_param_types[1].is_const()) { + // Prefer the candidate whose first parameter (receiver) matches the + // actual receiver type exactly over one that needs a type conversion. + // Conversions on the receiver create temporaries, so any mutation + // would be silently lost; this mirrors the deprioritization in + // dispatch() and resolves cases like myString.substr(int, int) when + // both string::substr and string_view::substr are registered. + const bool plist_empty = plist.empty(); + const bool mat_receiver_exact = !plist_empty && mat_fun_param_types[1].bare_equal(plist[0].get_type_info()); + const bool next_receiver_exact = !plist_empty && next_fun_param_types[1].bare_equal(plist[0].get_type_info()); + + if (mat_receiver_exact && !next_receiver_exact) { + // keep the old one, it has the better receiver match + } else if (!mat_receiver_exact && next_receiver_exact) { + matching_func = begin; // keep the new one, it has the better receiver match + } else if (plist[0].is_const() && !mat_fun_param_types[1].is_const() && next_fun_param_types[1].is_const()) { matching_func = begin; // keep the new one, the const/non-const matchup is correct } else if (!plist[0].is_const() && !mat_fun_param_types[1].is_const() && next_fun_param_types[1].is_const()) { // keep the old one, it has a better const/non-const matchup diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index ce54cf5b..abbc4240 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -2299,3 +2299,73 @@ TEST_CASE("Exception from C++ [] operator is catchable in ChaiScript") { caught )") == true); } + +// Issue #458: ChaiScript strings should be passable to C++ functions that +// take std::string_view, std::string_view should be a known type, and +// explicit conversion from std::string_view to std::string should work. +TEST_CASE("Issue #458: std::string_view interop with ChaiScript strings") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([](const std::string_view sv) { return std::string(sv); }), "consume_string_view"); + + CHECK(chai.eval(R"(consume_string_view("Hi there"))") == "Hi there"); + CHECK(chai.eval(R"(var s = "from variable"; consume_string_view(s))") == "from variable"); + + CHECK(chai.eval(R"(type_name(string_view("hello")) == "string_view")")); + + CHECK(chai.eval(R"(string(string_view("round trip")))") == "round trip"); +} + +// Issue #458 follow-up: string_view should expose the same search/substring +// surface as string, and overload dispatch must keep myString.substr(...) on +// String::substr while mySV.substr(...) goes to StringView::substr. +TEST_CASE("Issue #458: string_view supports string-like search and substring") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + // Receiver-type wins: string keeps String::substr, string_view picks StringView::substr. + CHECK(chai.eval(R"( + fun(){ + var s = "hello world"; + var sv = string_view(s); + type_name(sv.substr(6, 5)) == "string_view" && type_name(s.substr(6, 5)) == "string" + }() + )")); + + // Round-trip: substr through string_view yields the expected substring. + CHECK(chai.eval(R"( + fun(){ + var s = "hello world"; + var sv = string_view(s); + to_string(sv.substr(6, 5)) + }() + )") == "world"); + + // starts_with / ends_with on string_view. + CHECK(chai.eval(R"( + fun(){ + var s = "hello world"; + var sv = string_view(s); + sv.starts_with(string_view("hello")) && sv.ends_with(string_view("world")) + }() + )")); + + // find / rfind / find_first_of return the expected positions. + CHECK(chai.eval(R"( + fun(){ + var s = "hello world"; + var sv = string_view(s); + sv.find(string_view("world"), 0) == 6 && + sv.rfind(string_view("o"), sv.size()) == 7 && + sv.find_first_of(string_view("aeiou"), 0) == 1 + }() + )")); + + // [] indexed access reads the character at the given position. + CHECK(chai.eval(R"( + fun(){ + var s = "hello"; + var sv = string_view(s); + sv[1] == 'e' + }() + )")); +} diff --git a/unittests/function_introspection.chai b/unittests/function_introspection.chai index 2c511a73..d040ff6a 100644 --- a/unittests/function_introspection.chai +++ b/unittests/function_introspection.chai @@ -39,14 +39,15 @@ assert_equal(true, test_fun_types[1].bare_equal(int_type)); assert_equal(2, `==`.get_arity()); -// < should be the merging of two functions bool <(PODObject, PODObject) and bool <(string, string) +// < should be the merging of three functions bool <(PODObject, PODObject), +// bool <(string, string), and bool <(string_view, string_view) // we want to peel it apart and make sure that's true auto types = `<`.get_param_types(); assert_equal(3, types.size()); assert_equal(true, types[0].bare_equal(bool_type)); assert_equal(true, types[1].bare_equal(Object_type)); assert_equal(true, types[2].bare_equal(Object_type)); -assert_equal(2, `<`.get_contained_functions().size()); +assert_equal(3, `<`.get_contained_functions().size()); // guard existence tests