diff --git a/builtin/string_find_bench_test.mbt b/builtin/string_find_bench_test.mbt new file mode 100644 index 000000000..93779aae8 --- /dev/null +++ b/builtin/string_find_bench_test.mbt @@ -0,0 +1,127 @@ +// Copyright 2026 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +let string_find_bench_len : Int = 4096 + +///| +fn make_string_find_single_miss() -> String { + String::make(string_find_bench_len, 'a') +} + +///| +fn make_string_find_single_hit_end() -> String { + String::make(string_find_bench_len - 1, 'a') + "Z" +} + +///| +fn make_string_find_single_hit_start() -> String { + "Z" + String::make(string_find_bench_len - 1, 'a') +} + +///| +fn make_string_find_substring_hit_end() -> String { + String::make(string_find_bench_len - 4, 'a') + "Zabc" +} + +///| +fn make_string_find_substring_hit_start() -> String { + "Zabc" + String::make(string_find_bench_len - 4, 'a') +} + +///| +fn make_string_find_view_substring_hit_end() -> String { + "prefix-" + String::make(string_find_bench_len - 4, 'a') + "Zabc" + "-suffix" +} + +///| +fn make_string_find_view_substring_hit_start() -> String { + "prefix-" + "Zabc" + String::make(string_find_bench_len - 4, 'a') + "-suffix" +} + +///| +test "bench StringView::find single code unit hit end n=4096" (it : @bench.T) { + let data = make_string_find_single_hit_end() + let view = data[:] + it.bench(fn() { it.keep(view.find("Z")) }) +} + +///| +test "bench StringView::find single code unit miss n=4096" (it : @bench.T) { + let data = make_string_find_single_miss() + let view = data[:] + it.bench(fn() { it.keep(view.find("Z")) }) +} + +///| +test "bench StringView::find substring rare hit end n=4096" (it : @bench.T) { + let data = make_string_find_substring_hit_end() + let view = data[:] + it.bench(fn() { it.keep(view.find("Zabc")) }) +} + +///| +test "bench StringView::find substring dense miss n=4096" (it : @bench.T) { + let data = make_string_find_single_miss() + let view = data[:] + it.bench(fn() { it.keep(view.find("aaaaZ")) }) +} + +///| +test "bench StringView::find offset view rare hit end n=4096" (it : @bench.T) { + let data = make_string_find_view_substring_hit_end() + let view = data[7:string_find_bench_len + 7] + it.bench(fn() { it.keep(view.find("Zabc")) }) +} + +///| +test "bench StringView::rev_find single code unit hit start n=4096" ( + it : @bench.T, +) { + let data = make_string_find_single_hit_start() + let view = data[:] + it.bench(fn() { it.keep(view.rev_find("Z")) }) +} + +///| +test "bench StringView::rev_find single code unit miss n=4096" (it : @bench.T) { + let data = make_string_find_single_miss() + let view = data[:] + it.bench(fn() { it.keep(view.rev_find("Z")) }) +} + +///| +test "bench StringView::rev_find substring rare hit start n=4096" ( + it : @bench.T, +) { + let data = make_string_find_substring_hit_start() + let view = data[:] + it.bench(fn() { it.keep(view.rev_find("Zabc")) }) +} + +///| +test "bench StringView::rev_find substring dense miss n=4096" (it : @bench.T) { + let data = make_string_find_single_miss() + let view = data[:] + it.bench(fn() { it.keep(view.rev_find("aaaaZ")) }) +} + +///| +test "bench StringView::rev_find offset view rare hit start n=4096" ( + it : @bench.T, +) { + let data = make_string_find_view_substring_hit_start() + let view = data[7:string_find_bench_len + 7] + it.bench(fn() { it.keep(view.rev_find("Zabc")) }) +} diff --git a/builtin/string_methods.mbt b/builtin/string_methods.mbt index 272035798..8aa3122a7 100644 --- a/builtin/string_methods.mbt +++ b/builtin/string_methods.mbt @@ -12,6 +12,207 @@ // See the License for the specific language governing permissions and // limitations under the License. +///| +let string_find_max_bruteforce : Int = 64 + +///| +let string_find_prime_rk : UInt = 16777619U + +///| +fn string_find_cutover(i : Int) -> Int { + 4 + i / 16 +} + +///| +fn string_find_prime_pow(length : Int) -> UInt { + for pow = 1U, square = string_find_prime_rk, exp = length; exp > 0; { + let next_pow = if exp % 2 == 1 { pow * square } else { pow } + continue next_pow, square * square, exp / 2 + } nobreak { + pow + } +} + +///| +fn string_find_hash_range(data : String, start : Int, length : Int) -> UInt { + for i in 0.. Bool { + if start >= end { + return true + } + let haystack_data = haystack.data() + let needle_data = needle.data() + let haystack_start = haystack.start_offset() + candidate + let needle_start = needle.start_offset() + for j in start.. Int? { + let needle_len = needle.length() + let haystack_len = haystack.length() + if start + needle_len > haystack_len { + return None + } + let needle_data = needle.data() + let needle_start = needle.start_offset() + let haystack_data = haystack.data() + let haystack_start = haystack.start_offset() + let needle_hash = string_find_hash_range( + needle_data, needle_start, needle_len, + ) + let pow = string_find_prime_pow(needle_len) + let hash = string_find_hash_range( + haystack_data, + haystack_start + start, + needle_len, + ) + if hash == needle_hash && + string_find_match_range(haystack, needle, start, 0, needle_len) { + return Some(start) + } + for index = start + needle_len, hash = hash; index < haystack_len; { + let hash = hash * string_find_prime_rk + + haystack_data.unsafe_get(haystack_start + index).to_uint() + let hash = hash - + pow * + haystack_data.unsafe_get(haystack_start + index - needle_len).to_uint() + let candidate = index - needle_len + 1 + if hash == needle_hash && + string_find_match_range(haystack, needle, candidate, 0, needle_len) { + break Some(candidate) + } + continue index + 1, hash + } nobreak { + None + } +} + +///| +fn string_rev_find_rabin_karp( + haystack : StringView, + needle : StringView, +) -> Int? { + let needle_len = needle.length() + let haystack_len = haystack.length() + if needle_len > haystack_len { + return None + } + let needle_data = needle.data() + let needle_start = needle.start_offset() + let haystack_data = haystack.data() + let haystack_start = haystack.start_offset() + let needle_hash = string_find_hash_range( + needle_data, needle_start, needle_len, + ) + let pow = string_find_prime_pow(needle_len) + let hash = string_find_hash_range(haystack_data, haystack_start, needle_len) + let mut result = None + if hash == needle_hash && + string_find_match_range(haystack, needle, 0, 0, needle_len) { + result = Some(0) + } + for index = needle_len, hash = hash; index < haystack_len; { + let hash = hash * string_find_prime_rk + + haystack_data.unsafe_get(haystack_start + index).to_uint() + let hash = hash - + pow * + haystack_data.unsafe_get(haystack_start + index - needle_len).to_uint() + let candidate = index - needle_len + 1 + if hash == needle_hash && + string_find_match_range(haystack, needle, candidate, 0, needle_len) { + result = Some(candidate) + } + continue index + 1, hash + } nobreak { + result + } +} + +///| +fn string_find_by_code_unit_scanner_from( + haystack : StringView, + needle : StringView, + start : Int, +) -> Int? { + let haystack_len = haystack.length() + let needle_len = needle.length() + let last = haystack_len - needle_len + let first = needle.unsafe_get(0) + let last_offset = needle_len - 1 + let last_code = needle.unsafe_get(last_offset) + for pos = start, failures = 0; pos <= last; { + if haystack.unsafe_get(pos) == first && + haystack.unsafe_get(pos + last_offset) == last_code { + if string_find_match_range(haystack, needle, pos, 1, last_offset) { + break Some(pos) + } + let failures = failures + 1 + if failures > string_find_max_bruteforce || + failures > string_find_cutover(pos - start) { + break string_find_rabin_karp_from(haystack, needle, pos + 1) + } + continue pos + 1, failures + } + continue pos + 1, failures + } nobreak { + None + } +} + +///| +fn string_rev_find_by_code_unit_scanner_until( + haystack : StringView, + needle : StringView, + end : Int, +) -> Int? { + let needle_len = needle.length() + let first = needle.unsafe_get(0) + let last_offset = needle_len - 1 + let last_code = needle.unsafe_get(last_offset) + for pos = end - 1, failures = 0; pos >= 0; { + if haystack.unsafe_get(pos) == first && + haystack.unsafe_get(pos + last_offset) == last_code { + if string_find_match_range(haystack, needle, pos, 1, last_offset) { + break Some(pos) + } + let failures = failures + 1 + if failures > string_find_max_bruteforce || + failures > string_find_cutover(end - 1 - pos) { + break string_rev_find_rabin_karp(haystack, needle) + } + continue pos - 1, failures + } + continue pos - 1, failures + } nobreak { + None + } +} + ///| /// Returns the offset (charcode index) of the first occurrence of the given /// substring. If the substring is not found, it returns None. @@ -21,8 +222,6 @@ pub fn StringView::find(self : StringView, str : StringView) -> Int? { } else { boyer_moore_horspool_find(self, str) } - // TODO: When the pattern string is long (>= 256), - // consider using Two-Way algorithm to ensure linear time complexity. } ///| @@ -67,18 +266,18 @@ fn boyer_moore_horspool_find( for i in 0..<(needle_len - 1) { skip_table[needle.unsafe_get(i).to_int() & 0xFF] = needle_len - 1 - i } - for i = 0 - i <= haystack_len - needle_len - i = i + - skip_table[haystack.unsafe_get(i + needle_len - 1).to_int() & 0xFF] { - // Check all charcodes for match at current position - for j in 0..<=(needle_len - 1) { - if haystack.unsafe_get(i + j) != needle.unsafe_get(j) { - break - } - } nobreak { + for i = 0, failures = 0; i <= haystack_len - needle_len; { + if string_find_match_range(haystack, needle, i, 0, needle_len) { return Some(i) } + let failures = failures + 1 + if failures > string_find_max_bruteforce || + failures > string_find_cutover(i) { + return string_find_by_code_unit_scanner_from(haystack, needle, i + 1) + } + continue i + + skip_table[haystack.unsafe_get(i + needle_len - 1).to_int() & 0xFF], + failures } None } @@ -168,8 +367,6 @@ pub fn StringView::rev_find(self : StringView, str : StringView) -> Int? { } else { boyer_moore_horspool_rev_find(self, str) } - // TODO: When the pattern string is long (>= 256), - // consider using Two-Way algorithm to ensure linear time complexity. } ///| @@ -212,17 +409,16 @@ fn boyer_moore_horspool_rev_find( for i in needle_len>..1 { skip_table[needle.unsafe_get(i).to_int() & 0xFF] = i } - for i = haystack_len - needle_len - i >= 0 - i = i - skip_table[haystack.unsafe_get(i).to_int() & 0xFF] { - // Check all charcodes for match at current position - for j in 0..= 0; { + if string_find_match_range(haystack, needle, i, 0, needle_len) { return Some(i) } + let failures = failures + 1 + if failures > string_find_max_bruteforce || + failures > string_find_cutover(haystack_len - needle_len - i) { + return string_rev_find_by_code_unit_scanner_until(haystack, needle, i) + } + continue i - skip_table[haystack.unsafe_get(i).to_int() & 0xFF], failures } None } diff --git a/builtin/string_test.mbt b/builtin/string_test.mbt index 6d5690722..d863c2784 100644 --- a/builtin/string_test.mbt +++ b/builtin/string_test.mbt @@ -253,6 +253,22 @@ test "String::rev_find partial mismatch" { debug_inspect("ababa".rev_find("abac"), content="None") } +///| +test "StringView find dense fallback cases" { + let data = String::make(80, 'a') + debug_inspect(data[:].find("aaaaZ"), content="None") + let forward = data + "Zabcd" + debug_inspect(forward[:].find("Zabcd"), content="Some(80)") + let wrapped_forward = "prefix-" + forward + "-suffix" + let forward_view = wrapped_forward[7:7 + forward.length()] + debug_inspect(forward_view.find("Zabcd"), content="Some(80)") + let reverse = "Zabcd" + data + debug_inspect(reverse[:].rev_find("Zabcd"), content="Some(0)") + let wrapped_reverse = "prefix-" + reverse + "-suffix" + let reverse_view = wrapped_reverse[7:7 + reverse.length()] + debug_inspect(reverse_view.rev_find("Zabcd"), content="Some(0)") +} + ///| test "panic StringView repeat negative" { let view = "hi"[:]