Address review: expand string_view binding with substr/find/[]

Adds substr, find/rfind/find_first_of/find_last_of/find_first_not_of/
find_last_not_of, starts_with/ends_with, and operator[] to string_view_type,
mirroring string_type's search/substring surface so script authors can
traverse a buffer through string_view without allocating.

Sharing method names with string_type creates an ambiguity in
dispatch_with_conversions when the script call needs arithmetic conversion
(e.g. myString.substr(int, int)): both string::substr and string_view::substr
match-except-for-arithmetic, and the existing const/non-const tiebreaker
doesn't apply. Extend dispatch_with_conversions to prefer the candidate
whose first/receiver parameter type exactly matches the actual receiver,
mirroring the deprioritization already in dispatch(). With this in place,
myString.substr(1, 2) still resolves to string::substr while
mySV.substr(1, 2) resolves to string_view::substr (returning a string_view).

Updates the doc comment on string_view_type and adds a compiled test that
locks in both dispatch directions plus the new search/substring methods.

Requested by @lefticus in PR #697 review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-05-02 11:18:19 -06:00
parent b7315f2db9
commit 047e41ca40
3 changed files with 94 additions and 6 deletions

View File

@ -542,10 +542,13 @@ namespace chaiscript::bootstrap::standard_library {
/// Add a String_View type (e.g. std::string_view), with conversions to and from /// Add a String_View type (e.g. std::string_view), with conversions to and from
/// the matching owning String type (e.g. std::string). /// the matching owning String type (e.g. std::string).
/// ///
/// Only registers operations that don't share names with the owning String type's /// Mirrors the search/substring surface of string_type so that scripts can
/// methods that take arithmetic arguments (e.g. substr/find). Those would create /// traverse a buffer through StringView without allocating. Sharing method
/// dispatch ambiguity once the implicit String -> StringView conversion is in /// names with the owning String type is safe: dispatch deprioritizes any
/// play, because neither overload would exactly match a (String, int, int) call. /// 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 /// \note A String_View is a non-owning reference. Constructing one from a
/// temporary owning String yields a dangling reference once that temporary /// temporary owning String yields a dangling reference once that temporary
@ -564,6 +567,24 @@ namespace chaiscript::bootstrap::standard_library {
m.add(fun([](const StringView *s) { return s->empty(); }), "empty"); m.add(fun([](const StringView *s) { return s->empty(); }), "empty");
m.add(fun([](const StringView *s) { return s->data(); }), "data"); 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<typename StringView::size_type>(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. // Built-in implicit conversion from owning String to non-owning StringView.
m.add(type_conversion<const String &, StringView>()); m.add(type_conversion<const String &, StringView>());

View File

@ -737,11 +737,24 @@ namespace chaiscript {
if (matching_func == end) { if (matching_func == end) {
matching_func = begin; matching_func = begin;
} else { } 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 &mat_fun_param_types = matching_func->second->get_param_types();
const auto &next_fun_param_types = begin->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 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()) { } 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 // keep the old one, it has a better const/non-const matchup

View File

@ -2220,3 +2220,57 @@ TEST_CASE("Issue #458: std::string_view interop with ChaiScript strings") {
CHECK(chai.eval<std::string>(R"(string(string_view("round trip")))") == "round trip"); CHECK(chai.eval<std::string>(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<bool>(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<std::string>(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<bool>(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<bool>(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<bool>(R"(
fun(){
var s = "hello";
var sv = string_view(s);
sv[1] == 'e'
}()
)"));
}