diff --git a/Cargo.toml b/Cargo.toml index b39e36d..e386a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,3 +69,8 @@ harness = false [[bench]] name = "simd" harness = false + +[[bench]] +name = "packed" +harness = false +required-features = ["experimental-packed"] diff --git a/bench-results/README.md b/bench-results/README.md index 1a5441a..aba363c 100644 --- a/bench-results/README.md +++ b/bench-results/README.md @@ -20,6 +20,15 @@ bench-results/ topic.json properties.json remoting-header.json + packed-evidence/ + YYYYMMDD-HHMMSS/ + summary.md + packed-test.txt + packed-miri.txt + packed-asan.txt + fuzz-packed-from-bytes.txt + fuzz-packed-push-str.txt + packed-bench.txt summaries/ summary-v1.1-v1.2.md summary-v1.2-v2-packed.md @@ -51,3 +60,14 @@ On Windows PowerShell: ```powershell scripts/bench-all.ps1 current ``` + +For the experimental packed representation evidence gate: + +```powershell +scripts/verify-packed.ps1 -RunMiri -RunSanitizer -RunFuzz -RunBench +``` + +The packed evidence script always runs `cargo test --features experimental-packed`. +The Miri, sanitizer, fuzz, and packed benchmark gates are opt-in because they +require nightly components, optional cargo subcommands, target support, or more +runtime than the default test suite. diff --git a/bench-results/packed-evidence/20260624-130416/fuzz-packed-from-bytes.txt b/bench-results/packed-evidence/20260624-130416/fuzz-packed-from-bytes.txt new file mode 100644 index 0000000..8ef97e0 --- /dev/null +++ b/bench-results/packed-evidence/20260624-130416/fuzz-packed-from-bytes.txt @@ -0,0 +1,25 @@ +warning: linker stdout: 姝e湪鍒涘缓搴?C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\target\x86_64-pc-windows-msvc\release\deps\fuzz_packed_from_bytes.lib 鍜屽璞?C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\target\x86_64-pc-windows-msvc\release\deps\fuzz_packed_from_bytes.exp + | + = note: `#[warn(linker_messages)]` on by default + +warning: `cheetah-string-fuzz` (bin "fuzz_packed_from_bytes") generated 1 warning + Finished `release` profile [optimized + debuginfo] target(s) in 0.04s +warning: linker stdout: 姝e湪鍒涘缓搴?C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\target\x86_64-pc-windows-msvc\release\deps\fuzz_packed_from_bytes.lib 鍜屽璞?C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\target\x86_64-pc-windows-msvc\release\deps\fuzz_packed_from_bytes.exp + | + = note: `#[warn(linker_messages)]` on by default + +warning: `cheetah-string-fuzz` (bin "fuzz_packed_from_bytes") generated 1 warning + Finished `release` profile [optimized + debuginfo] target(s) in 0.01s + Running `fuzz\target\x86_64-pc-windows-msvc\release\fuzz_packed_from_bytes.exe -artifact_prefix=C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\artifacts\fuzz_packed_from_bytes\ -runs=100 C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\corpus\fuzz_packed_from_bytes` +INFO: Running with entropic power schedule (0xFF, 100). +INFO: Seed: 493125877 +INFO: Loaded 1 modules (290 inline 8-bit counters): 290 [00007FF659A85EC8, 00007FF659A85FEA), +INFO: Loaded 1 PC tables (290 PCs): 290 [00007FF659A66BD8,00007FF659A67DF8), +INFO: 0 files found in C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\corpus\fuzz_packed_from_bytes +INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes +INFO: A corpus is not provided, starting from an empty corpus +#2 INITED cov: 26 ft: 26 corp: 1/1b exec/s: 0 rss: 41Mb +#4 NEW cov: 27 ft: 27 corp: 2/3b lim: 4 exec/s: 0 rss: 41Mb L: 2/2 MS: 2 ChangeBit-InsertByte- +#28 REDUCE cov: 27 ft: 27 corp: 2/2b lim: 4 exec/s: 0 rss: 41Mb L: 1/1 MS: 4 CrossOver-CopyPart-ChangeBinInt-EraseBytes- +#100 DONE cov: 27 ft: 27 corp: 2/2b lim: 4 exec/s: 0 rss: 41Mb +Done 100 runs in 0 second(s) diff --git a/bench-results/packed-evidence/20260624-130416/fuzz-packed-push-str.txt b/bench-results/packed-evidence/20260624-130416/fuzz-packed-push-str.txt new file mode 100644 index 0000000..5f07e92 --- /dev/null +++ b/bench-results/packed-evidence/20260624-130416/fuzz-packed-push-str.txt @@ -0,0 +1,27 @@ +warning: linker stdout: 姝e湪鍒涘缓搴?C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\target\x86_64-pc-windows-msvc\release\deps\fuzz_packed_push_str.lib 鍜屽璞?C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\target\x86_64-pc-windows-msvc\release\deps\fuzz_packed_push_str.exp + | + = note: `#[warn(linker_messages)]` on by default + +warning: `cheetah-string-fuzz` (bin "fuzz_packed_push_str") generated 1 warning + Finished `release` profile [optimized + debuginfo] target(s) in 0.03s +warning: linker stdout: 姝e湪鍒涘缓搴?C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\target\x86_64-pc-windows-msvc\release\deps\fuzz_packed_push_str.lib 鍜屽璞?C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\target\x86_64-pc-windows-msvc\release\deps\fuzz_packed_push_str.exp + | + = note: `#[warn(linker_messages)]` on by default + +warning: `cheetah-string-fuzz` (bin "fuzz_packed_push_str") generated 1 warning + Finished `release` profile [optimized + debuginfo] target(s) in 0.01s + Running `fuzz\target\x86_64-pc-windows-msvc\release\fuzz_packed_push_str.exe -artifact_prefix=C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\artifacts\fuzz_packed_push_str\ -runs=100 C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\corpus\fuzz_packed_push_str` +INFO: Running with entropic power schedule (0xFF, 100). +INFO: Seed: 503145824 +INFO: Loaded 1 modules (359 inline 8-bit counters): 359 [00007FF6904D7088, 00007FF6904D71EF), +INFO: Loaded 1 PC tables (359 PCs): 359 [00007FF6904B6BD8,00007FF6904B8248), +INFO: 0 files found in C:\Users\ljbmx\Desktop\mxsm\cheetah-string\fuzz\corpus\fuzz_packed_push_str +INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes +INFO: A corpus is not provided, starting from an empty corpus +#2 INITED cov: 37 ft: 37 corp: 1/1b exec/s: 0 rss: 41Mb +#6 NEW cov: 41 ft: 41 corp: 2/3b lim: 4 exec/s: 0 rss: 41Mb L: 2/2 MS: 4 CrossOver-ChangeByte-CrossOver-CopyPart- +#20 NEW cov: 42 ft: 42 corp: 3/6b lim: 4 exec/s: 0 rss: 41Mb L: 3/3 MS: 4 ChangeBit-InsertByte-CopyPart-ChangeByte- +#31 REDUCE cov: 42 ft: 42 corp: 3/5b lim: 4 exec/s: 0 rss: 41Mb L: 2/2 MS: 1 EraseBytes- +#34 NEW cov: 43 ft: 43 corp: 4/7b lim: 4 exec/s: 0 rss: 41Mb L: 2/2 MS: 3 ShuffleBytes-InsertByte-ChangeByte- +#100 DONE cov: 43 ft: 43 corp: 4/7b lim: 4 exec/s: 0 rss: 41Mb +Done 100 runs in 0 second(s) diff --git a/bench-results/packed-evidence/20260624-130416/packed-asan.txt b/bench-results/packed-evidence/20260624-130416/packed-asan.txt new file mode 100644 index 0000000..23a8b71 --- /dev/null +++ b/bench-results/packed-evidence/20260624-130416/packed-asan.txt @@ -0,0 +1,93 @@ + +running 1 test +test packed::tests::packed_size_is_three_words ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.01s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 33 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 41 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s + + +running 1 test +test packed_layout_is_24_bytes ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 22 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s + + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.15s + Running unittests src\lib.rs (target\debug\deps\cheetah_string-ef19ecdb4ae50af0.exe) + Running tests\api_extensions.rs (target\debug\deps\api_extensions-265ea9d412c4d589.exe) + Running tests\basic.rs (target\debug\deps\basic-5a9db4b2e7a37375.exe) + Running tests\bytes.rs (target\debug\deps\bytes-28bff98899307ff2.exe) + Running tests\comprehensive_tests.rs (target\debug\deps\comprehensive_tests-9762dbcc65ca96cf.exe) + Running tests\layout_snapshot.rs (target\debug\deps\layout_snapshot-178567cc15cd9278.exe) + Running tests\mutation.rs (target\debug\deps\mutation-261bb87b5dc079ef.exe) + Running tests\packed.rs (target\debug\deps\packed-4719a7e1f01d1234.exe) + Running tests\patterns.rs (target\debug\deps\patterns-4bb1c92062e3a356.exe) + Running tests\search.rs (target\debug\deps\search-70c948bc42af4b07.exe) + Running tests\serde.rs (target\debug\deps\serde-64c9281c5b632aae.exe) + Running tests\simd.rs (target\debug\deps\simd-9ebcc041882bf452.exe) + Running tests\split_edge_cases.rs (target\debug\deps\split_edge_cases-c696d0bfbb0ebff4.exe) + Running tests\sso.rs (target\debug\deps\sso-fe28e8b20a68cdd8.exe) + Running tests\type_split.rs (target\debug\deps\type_split-936df32e17a021ec.exe) diff --git a/bench-results/packed-evidence/20260624-130416/packed-bench.txt b/bench-results/packed-evidence/20260624-130416/packed-bench.txt new file mode 100644 index 0000000..cea3b57 --- /dev/null +++ b/bench-results/packed-evidence/20260624-130416/packed-bench.txt @@ -0,0 +1,135 @@ +packed_construction/CheetahString short + time: [9.6566 ns 9.7726 ns 9.9076 ns] + change: [-1.9188% +1.3898% +4.6098%] (p = 0.41 > 0.05) + No change in performance detected. +Found 13 outliers among 100 measurements (13.00%) + 12 (12.00%) high mild + 1 (1.00%) high severe +packed_construction/PackedCheetahString short + time: [24.627 ns 25.266 ns 25.947 ns] + change: [+8.8753% +11.638% +14.563%] (p = 0.00 < 0.05) + Performance has regressed. +Found 9 outliers among 100 measurements (9.00%) + 4 (4.00%) high mild + 5 (5.00%) high severe +packed_construction/CheetahString long + time: [33.496 ns 33.947 ns 34.421 ns] + change: [+2.2096% +4.7316% +7.0119%] (p = 0.00 < 0.05) + Performance has regressed. +Found 11 outliers among 100 measurements (11.00%) + 9 (9.00%) high mild + 2 (2.00%) high severe +packed_construction/PackedCheetahString long + time: [21.843 ns 22.464 ns 23.123 ns] + change: [+5.9260% +8.5757% +11.188%] (p = 0.00 < 0.05) + Performance has regressed. +Found 8 outliers among 100 measurements (8.00%) + 7 (7.00%) high mild + 1 (1.00%) high severe + +packed_push_str/CheetahString inline append + time: [21.574 ns 21.770 ns 21.967 ns] + change: [+2.5228% +3.8924% +5.0881%] (p = 0.00 < 0.05) + Performance has regressed. +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high mild +packed_push_str/PackedCheetahString inline append + time: [12.594 ns 12.698 ns 12.819 ns] + change: [+0.5722% +1.7418% +2.8416%] (p = 0.00 < 0.05) + Change within noise threshold. +packed_push_str/CheetahString promote to heap + time: [52.082 ns 52.768 ns 53.515 ns] + change: [-1.7095% +0.3918% +2.4332%] (p = 0.71 > 0.05) + No change in performance detected. +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high mild +packed_push_str/PackedCheetahString promote to heap + time: [68.553 ns 70.513 ns 72.384 ns] + change: [-3.8334% -1.0159% +1.7525%] (p = 0.48 > 0.05) + No change in performance detected. +Found 3 outliers among 100 measurements (3.00%) + 3 (3.00%) high mild + +packed_mq_topic_insert/CheetahString short topics + time: [33.419 碌s 34.183 碌s 35.004 碌s] + thrpt: [28.568 Melem/s 29.255 Melem/s 29.923 Melem/s] + change: + time: [-0.1583% +2.5345% +5.4747%] (p = 0.08 > 0.05) + thrpt: [-5.1905% -2.4719% +0.1586%] + No change in performance detected. +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high mild +packed_mq_topic_insert/PackedCheetahString short topics + time: [52.003 碌s 53.170 碌s 54.214 碌s] + thrpt: [18.445 Melem/s 18.807 Melem/s 19.230 Melem/s] + change: + time: [+3.6370% +7.3587% +11.110%] (p = 0.00 < 0.05) + thrpt: [-9.9992% -6.8543% -3.5093%] + Performance has regressed. +packed_mq_topic_insert/CheetahString long topics + time: [65.165 碌s 66.119 碌s 67.130 碌s] + thrpt: [14.896 Melem/s 15.124 Melem/s 15.346 Melem/s] + change: + time: [+5.4655% +7.9547% +10.517%] (p = 0.00 < 0.05) + thrpt: [-9.5165% -7.3686% -5.1822%] + Performance has regressed. +packed_mq_topic_insert/PackedCheetahString long topics + time: [50.294 碌s 51.659 碌s 52.852 碌s] + thrpt: [18.921 Melem/s 19.358 Melem/s 19.883 Melem/s] + change: + time: [-16.933% -14.191% -11.417%] (p = 0.00 < 0.05) + thrpt: [+12.889% +16.538% +20.384%] + Performance has improved. + + Compiling cheetah-string v2.0.0 (C:\Users\ljbmx\Desktop\mxsm\cheetah-string) + Finished `bench` profile [optimized] target(s) in 1.72s + Running benches\packed.rs (target\release\deps\packed-5044d38116574fc7.exe) +Gnuplot not found, using plotters backend +Benchmarking packed_construction/CheetahString short +Benchmarking packed_construction/CheetahString short: Warming up for 3.0000 s +Benchmarking packed_construction/CheetahString short: Collecting 100 samples in estimated 5.0000 s (533M iterations) +Benchmarking packed_construction/CheetahString short: Analyzing +Benchmarking packed_construction/PackedCheetahString short +Benchmarking packed_construction/PackedCheetahString short: Warming up for 3.0000 s +Benchmarking packed_construction/PackedCheetahString short: Collecting 100 samples in estimated 5.0001 s (203M iterations) +Benchmarking packed_construction/PackedCheetahString short: Analyzing +Benchmarking packed_construction/CheetahString long +Benchmarking packed_construction/CheetahString long: Warming up for 3.0000 s +Benchmarking packed_construction/CheetahString long: Collecting 100 samples in estimated 5.0000 s (147M iterations) +Benchmarking packed_construction/CheetahString long: Analyzing +Benchmarking packed_construction/PackedCheetahString long +Benchmarking packed_construction/PackedCheetahString long: Warming up for 3.0000 s +Benchmarking packed_construction/PackedCheetahString long: Collecting 100 samples in estimated 5.0000 s (223M iterations) +Benchmarking packed_construction/PackedCheetahString long: Analyzing +Benchmarking packed_push_str/CheetahString inline append +Benchmarking packed_push_str/CheetahString inline append: Warming up for 3.0000 s +Benchmarking packed_push_str/CheetahString inline append: Collecting 100 samples in estimated 5.0001 s (233M iterations) +Benchmarking packed_push_str/CheetahString inline append: Analyzing +Benchmarking packed_push_str/PackedCheetahString inline append +Benchmarking packed_push_str/PackedCheetahString inline append: Warming up for 3.0000 s +Benchmarking packed_push_str/PackedCheetahString inline append: Collecting 100 samples in estimated 5.0000 s (393M iterations) +Benchmarking packed_push_str/PackedCheetahString inline append: Analyzing +Benchmarking packed_push_str/CheetahString promote to heap +Benchmarking packed_push_str/CheetahString promote to heap: Warming up for 3.0000 s +Benchmarking packed_push_str/CheetahString promote to heap: Collecting 100 samples in estimated 5.0002 s (93M iterations) +Benchmarking packed_push_str/CheetahString promote to heap: Analyzing +Benchmarking packed_push_str/PackedCheetahString promote to heap +Benchmarking packed_push_str/PackedCheetahString promote to heap: Warming up for 3.0000 s +Benchmarking packed_push_str/PackedCheetahString promote to heap: Collecting 100 samples in estimated 5.0000 s (76M iterations) +Benchmarking packed_push_str/PackedCheetahString promote to heap: Analyzing +Benchmarking packed_mq_topic_insert/CheetahString short topics +Benchmarking packed_mq_topic_insert/CheetahString short topics: Warming up for 3.0000 s +Benchmarking packed_mq_topic_insert/CheetahString short topics: Collecting 100 samples in estimated 5.0949 s (146k iterations) +Benchmarking packed_mq_topic_insert/CheetahString short topics: Analyzing +Benchmarking packed_mq_topic_insert/PackedCheetahString short topics +Benchmarking packed_mq_topic_insert/PackedCheetahString short topics: Warming up for 3.0000 s +Benchmarking packed_mq_topic_insert/PackedCheetahString short topics: Collecting 100 samples in estimated 5.0093 s (106k iterations) +Benchmarking packed_mq_topic_insert/PackedCheetahString short topics: Analyzing +Benchmarking packed_mq_topic_insert/CheetahString long topics +Benchmarking packed_mq_topic_insert/CheetahString long topics: Warming up for 3.0000 s +Benchmarking packed_mq_topic_insert/CheetahString long topics: Collecting 100 samples in estimated 5.1416 s (76k iterations) +Benchmarking packed_mq_topic_insert/CheetahString long topics: Analyzing +Benchmarking packed_mq_topic_insert/PackedCheetahString long topics +Benchmarking packed_mq_topic_insert/PackedCheetahString long topics: Warming up for 3.0000 s +Benchmarking packed_mq_topic_insert/PackedCheetahString long topics: Collecting 100 samples in estimated 5.1184 s (96k iterations) +Benchmarking packed_mq_topic_insert/PackedCheetahString long topics: Analyzing diff --git a/bench-results/packed-evidence/20260624-130416/packed-miri.txt b/bench-results/packed-evidence/20260624-130416/packed-miri.txt new file mode 100644 index 0000000..5d8f525 --- /dev/null +++ b/bench-results/packed-evidence/20260624-130416/packed-miri.txt @@ -0,0 +1,93 @@ + +running 1 test +test packed::tests::packed_size_is_three_words ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.23s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 33 filtered out; finished in 0.20s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 41 filtered out; finished in 0.23s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.08s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.05s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.07s + + +running 1 test +test packed_layout_is_24_bytes ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.20s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 0.08s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.06s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.06s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 22 filtered out; finished in 0.15s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.06s + + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.18s + Running unittests src\lib.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\cheetah_string-710c23509e0bb080.exe) + Running tests\api_extensions.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\api_extensions-fd98183ef344a2e5.exe) + Running tests\basic.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\basic-15be6d680e20282c.exe) + Running tests\bytes.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\bytes-6771c63a096fe224.exe) + Running tests\comprehensive_tests.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\comprehensive_tests-d4e9bb008aa26815.exe) + Running tests\layout_snapshot.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\layout_snapshot-b04da07ba1e6ac18.exe) + Running tests\mutation.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\mutation-8d538d93e11b03bf.exe) + Running tests\packed.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\packed-1b4879052c370245.exe) + Running tests\patterns.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\patterns-93a03ab7fa2fc1aa.exe) + Running tests\search.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\search-f28354d0fc25247e.exe) + Running tests\serde.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\serde-aab437a38b16cf71.exe) + Running tests\simd.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\simd-d041527da270bdfc.exe) + Running tests\split_edge_cases.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\split_edge_cases-2d53f003663b2458.exe) + Running tests\sso.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\sso-74003129971ecabb.exe) + Running tests\type_split.rs (target\miri\x86_64-pc-windows-msvc\debug\deps\type_split-d7bd6d4db0597945.exe) diff --git a/bench-results/packed-evidence/20260624-130416/packed-test.txt b/bench-results/packed-evidence/20260624-130416/packed-test.txt new file mode 100644 index 0000000..e3f64d7 --- /dev/null +++ b/bench-results/packed-evidence/20260624-130416/packed-test.txt @@ -0,0 +1,280 @@ + +running 6 tests +test cheetah_string::tests::long_borrowed_str_uses_shared_storage ... ok +test cheetah_string::tests::with_capacity_above_inline_uses_heap_storage ... ok +test cheetah_string::tests::push_str_promotes_builder_growth_to_owned_storage ... ok +test cheetah_string::tests::try_from_vec_short_input_uses_inline_storage ... ok +test cheetah_string::tests::long_vec_conversion_uses_owned_storage ... ok +test packed::tests::packed_size_is_three_words ... ok + +test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s + + +running 33 tests +test test_add_assign_chain ... ok +test test_add_assign_cheetah_string ... ok +test test_add_assign_str ... ok +test test_add_chain ... ok +test test_lines ... ok +test test_add_long_strings ... ok +test test_add_sso_strings ... ok +test test_add_str ... ok +test test_add_string ... ok +test test_chars ... ok +test test_contains ... ok +test test_empty_string_operations ... ok +test test_add_cheetah_string ... ok +test test_ends_with ... ok +test test_find ... ok +test test_add_empty_strings ... ok +test test_repeat ... ok +test test_replace ... ok +test test_replacen ... ok +test test_rfind ... ok +test test_split ... ok +test test_starts_with ... ok +test test_substring ... ok +test test_substring_invalid_range - should panic ... ok +test test_to_lowercase ... ok +test test_to_uppercase ... ok +test test_trim ... ok +test test_trim_end ... ok +test test_trim_start ... ok +test test_unicode_queries ... ok +test test_unicode_split ... ok +test test_unicode_transform ... ok +test test_whitespace_operations ... ok + +test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s + + +running 41 tests +test test_as_ref_bytes ... ok +test test_as_ref_str ... ok +test test_borrow ... ok +test test_clone ... ok +test test_clone_arc_sharing ... ok +test test_cow_owned ... ok +test test_cow_static ... ok +test test_debug ... ok +test test_default ... ok +test test_display ... ok +test test_empty ... ok +test test_eq ... ok +test test_from_arc_string ... ok +test test_from_char ... ok +test test_deref ... ok +test test_empty_string ... ok +test test_from_iter_chars ... ok +test test_from_iter_str ... ok +test test_from_iter_string ... ok +test test_from_static ... ok +test test_from_str ... ok +test test_from_string ... ok +test test_from_string_ref ... ok +test test_hash ... ok +test test_into_string_copies_shared_arc_string_input ... ok +test test_into_string_reuses_owned_arc_string_buffer_after_clone ... ok +test test_into_string_reuses_owned_vec_buffer_after_clone ... ok +test test_into_string_reuses_unique_arc_string_buffer ... ok +test test_into_string_reuses_unique_vec_buffer ... ok +test test_ord ... ok +test test_parse ... ok +test test_to_string ... ok +test test_try_from_bytes_method ... ok +test test_try_from_invalid_bytes ... ok +test test_try_from_invalid_vec ... ok +test test_try_from_slice_trait ... ok +test test_try_from_valid_bytes ... ok +test test_try_from_valid_vec ... ok +test test_try_from_vec_method ... ok +test test_try_from_vec_trait ... ok +test test_unicode ... ok + +test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + +running 8 tests +test test_chars_double_ended ... ok +test test_special_patterns ... ok +test test_long_strings ... ok +test test_split_str_reverse_panics - should panic ... ok +test test_split_char_reverse ... ok +test test_split_iterator_behavior ... ok +test test_pattern_traits ... ok +test test_unicode_split ... ok + +test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + +running 1 test +test layout_snapshot ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s + + +running 8 tests +test add_assign_uses_push_str_path ... ok +test from_string_shared_keeps_long_immutable_inputs_clone_cheap ... ok +test from_string_owned_preserves_spare_capacity_for_push ... ok +test add_reuses_owned_lhs_capacity ... ok +test inline_push_str_appends_in_place ... ok +test owned_push_str_reuses_spare_capacity ... ok +test reserve_zero_keeps_existing_buffer ... ok +test from_string_defaults_to_owned_policy_in_v2 ... ok + +test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s + + +running 6 tests +test clone_is_independent_for_heap_storage ... ok +test hash_matches_str_semantics ... ok +test heap_string_roundtrips ... ok +test inline_string_roundtrips ... ok +test packed_layout_is_24_bytes ... ok +test push_str_promotes_inline_to_heap ... ok + +test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s + + +running 9 tests +test pattern_tests::test_chars_double_ended ... ok +test pattern_tests::test_ends_with_char ... ok +test pattern_tests::test_chars_reverse ... ok +test pattern_tests::test_contains_char ... ok +test pattern_tests::test_combined_patterns ... ok +test pattern_tests::test_split_char ... ok +test pattern_tests::test_split_char_reverse ... ok +test pattern_tests::test_split_str ... ok +test pattern_tests::test_starts_with_char ... ok + +test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s + + +running 5 tests +test empty_needle_matches_str_find_semantics ... ok +test memmem_search_reports_byte_indices ... ok +test reusable_empty_finder_matches_start ... ok +test reusable_finder_matches_repeated_needle ... ok +test unicode_search_matches_str_indices ... ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + +running 4 tests +test test_empty_pattern ... ok +test test_split_edge_cases ... ok +test test_single_char_string_pattern_reverse_panics - should panic ... ok +test test_string_pattern_consecutive_separators ... ok + +test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s + + +running 22 tests +test test_sso_as_bytes ... ok +test test_sso_clone_short_string ... ok +test test_sso_boundary_24_bytes ... ok +test test_sso_boundary_23_bytes ... ok +test test_sso_hash ... ok +test test_sso_ordering ... ok +test test_sso_empty ... ok +test test_sso_empty_string ... ok +test test_sso_from_char ... ok +test test_sso_from_string ... ok +test test_sso_long_string ... ok +test test_sso_mixed_lengths ... ok +test test_sso_display_debug ... ok +test test_sso_short_string ... ok +test test_sso_equality ... ok +test test_sso_deref ... ok +test test_sso_special_chars ... ok +test test_sso_to_string ... ok +test test_sso_try_from_bytes ... ok +test test_sso_try_from_vec ... ok +test test_sso_unicode_boundary ... ok +test test_sso_unicode_short ... ok + +test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s + + +running 5 tests +test builder_finishes_to_clone_cheap_str ... ok +test cheetah_str_works_as_hash_map_key ... ok +test cheetah_str_keeps_long_clones_shared ... ok +test builder_finishes_to_mutable_string_with_spare_capacity ... ok +test cheetah_string_can_be_compacted_into_cheetah_str ... ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + +running 34 tests +test src\cheetah_string.rs - cheetah_string::CheetahString::ends_with (line 561) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::add_assign (line 1229) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::add (line 1163) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::lines (line 750) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::ends_with_char (line 590) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::chars (line 766) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::add (line 1184) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::find (line 652) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::add_assign (line 1247) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::push_str (line 981) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::contains_char (line 632) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::repeat (line 909) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::contains (line 610) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::replace (line 816) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::from (line 105) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::add (line 1206) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::replacen (line 831) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::reserve (line 1001) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::starts_with (line 513) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::rfind (line 669) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::starts_with_char (line 542) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::substring (line 852) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::to_uppercase (line 786) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::split (line 729) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::to_lowercase (line 801) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::trim_start (line 699) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::trim (line 684) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::trim_end (line 714) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::try_from_vec (line 280) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::try_from_bytes (line 303) ... ok +test src\lib.rs - (line 37) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::try_substring (line 870) ... ok +test src\cheetah_string.rs - cheetah_string::CheetahString::with_capacity (line 930) ... ok +test src\lib.rs - (line 51) ... ok + +test result: ok. 34 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.11s + + Compiling cheetah-string v2.0.0 (C:\Users\ljbmx\Desktop\mxsm\cheetah-string) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.52s + Running unittests src\lib.rs (target\debug\deps\cheetah_string-85f2889286d24cb9.exe) + Running tests\api_extensions.rs (target\debug\deps\api_extensions-bf40243e7bdd7ec0.exe) + Running tests\basic.rs (target\debug\deps\basic-38e8da84a7e8c294.exe) + Running tests\bytes.rs (target\debug\deps\bytes-68d85f4c017cdc53.exe) + Running tests\comprehensive_tests.rs (target\debug\deps\comprehensive_tests-8714d8fed17b3073.exe) + Running tests\layout_snapshot.rs (target\debug\deps\layout_snapshot-e73ee3d87ac5e868.exe) + Running tests\mutation.rs (target\debug\deps\mutation-aa2e72da69bb2617.exe) + Running tests\packed.rs (target\debug\deps\packed-b423d9b4183ee71c.exe) + Running tests\patterns.rs (target\debug\deps\patterns-2c2d1729d6e5d713.exe) + Running tests\search.rs (target\debug\deps\search-b551036e977c2369.exe) + Running tests\serde.rs (target\debug\deps\serde-bc2847d30396b160.exe) + Running tests\simd.rs (target\debug\deps\simd-63374949017eee43.exe) + Running tests\split_edge_cases.rs (target\debug\deps\split_edge_cases-b23b1b59afb216b1.exe) + Running tests\sso.rs (target\debug\deps\sso-ecea8e0eb4a2c69d.exe) + Running tests\type_split.rs (target\debug\deps\type_split-c825cd27b0045e57.exe) + Doc-tests cheetah_string diff --git a/bench-results/packed-evidence/20260624-130416/summary.md b/bench-results/packed-evidence/20260624-130416/summary.md new file mode 100644 index 0000000..011216a --- /dev/null +++ b/bench-results/packed-evidence/20260624-130416/summary.md @@ -0,0 +1,23 @@ +# Packed Evidence Capture + +- Date: 2026-06-24T13:04:16.4234003+08:00 +- Repository: cheetah-string +- Output directory: C:\Users\ljbmx\Desktop\mxsm\cheetah-string\bench-results\packed-evidence\20260624-130416 + +- `cargo test --features experimental-packed`: running +- `cargo test --features experimental-packed`: PASS, see `packed-test.txt` +- `cargo +nightly miri --version`: miri 0.1.0 (9e2abe0c6a 2026-06-16) +- `cargo +nightly miri test --features experimental-packed packed`: running +- `cargo +nightly miri test --features experimental-packed packed`: PASS, see `packed-miri.txt` +- `ASan runtime`: using `C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.51.36231\bin\Hostx64\x64` +- `address sanitizer packed tests`: running +- `address sanitizer packed tests`: PASS, see `packed-asan.txt` +- `ASan runtime`: using `C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.51.36231\bin\Hostx64\x64` +- `cargo +nightly fuzz run fuzz_packed_from_bytes`: running +- `cargo +nightly fuzz run fuzz_packed_from_bytes`: PASS, see `fuzz-packed-from-bytes.txt` +- `cargo +nightly fuzz run fuzz_packed_push_str`: running +- `cargo +nightly fuzz run fuzz_packed_push_str`: PASS, see `fuzz-packed-push-str.txt` +- `cargo bench --bench packed --features experimental-packed`: running +- `cargo bench --bench packed --features experimental-packed`: PASS, see `packed-bench.txt` + +Total failing required gates: 0 diff --git a/benches/packed.rs b/benches/packed.rs new file mode 100644 index 0000000..ee6a549 --- /dev/null +++ b/benches/packed.rs @@ -0,0 +1,129 @@ +#![cfg(feature = "experimental-packed")] + +use cheetah_string::packed::PackedCheetahString; +use cheetah_string::CheetahString; +use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput}; +use std::collections::HashMap; + +const TOPIC_COUNT: usize = 1_000; + +fn short_topics() -> Vec { + (0..TOPIC_COUNT).map(|i| format!("T{:022}", i)).collect() +} + +fn long_topics() -> Vec { + (0..TOPIC_COUNT) + .map(|i| format!("RMQ_SYS_TRACE_TOPIC_{:05}_LONG_ROUTE_SEGMENT", i)) + .collect() +} + +fn bench_packed_construction(c: &mut Criterion) { + let short = "topic-a"; + let long = "RMQ_SYS_TRACE_TOPIC_00001_LONG_ROUTE_SEGMENT"; + + let mut group = c.benchmark_group("packed_construction"); + + group.bench_function("CheetahString short", |b| { + b.iter(|| CheetahString::from(black_box(short))) + }); + group.bench_function("PackedCheetahString short", |b| { + b.iter(|| PackedCheetahString::from(black_box(short))) + }); + group.bench_function("CheetahString long", |b| { + b.iter(|| CheetahString::from(black_box(long))) + }); + group.bench_function("PackedCheetahString long", |b| { + b.iter(|| PackedCheetahString::from(black_box(long))) + }); + + group.finish(); +} + +fn bench_packed_push_str(c: &mut Criterion) { + let mut group = c.benchmark_group("packed_push_str"); + + group.bench_function("CheetahString inline append", |b| { + b.iter(|| { + let mut value = CheetahString::from("topic"); + value.push_str(black_box("-a")); + black_box(value) + }) + }); + group.bench_function("PackedCheetahString inline append", |b| { + b.iter(|| { + let mut value = PackedCheetahString::from("topic"); + value.push_str(black_box("-a")); + black_box(value) + }) + }); + group.bench_function("CheetahString promote to heap", |b| { + b.iter(|| { + let mut value = CheetahString::from("12345678901234567890123"); + value.push_str(black_box("-overflow")); + black_box(value) + }) + }); + group.bench_function("PackedCheetahString promote to heap", |b| { + b.iter(|| { + let mut value = PackedCheetahString::from("12345678901234567890123"); + value.push_str(black_box("-overflow")); + black_box(value) + }) + }); + + group.finish(); +} + +fn bench_packed_topic_insert(c: &mut Criterion) { + let short_topics = short_topics(); + let long_topics = long_topics(); + let mut group = c.benchmark_group("packed_mq_topic_insert"); + group.throughput(Throughput::Elements(TOPIC_COUNT as u64)); + + group.bench_function("CheetahString short topics", |b| { + b.iter(|| { + let mut map = HashMap::with_capacity(TOPIC_COUNT); + for (idx, topic) in short_topics.iter().enumerate() { + map.insert(black_box(CheetahString::from(topic.as_str())), idx); + } + black_box(map) + }) + }); + group.bench_function("PackedCheetahString short topics", |b| { + b.iter(|| { + let mut map = HashMap::with_capacity(TOPIC_COUNT); + for (idx, topic) in short_topics.iter().enumerate() { + map.insert(black_box(PackedCheetahString::from(topic.as_str())), idx); + } + black_box(map) + }) + }); + group.bench_function("CheetahString long topics", |b| { + b.iter(|| { + let mut map = HashMap::with_capacity(TOPIC_COUNT); + for (idx, topic) in long_topics.iter().enumerate() { + map.insert(black_box(CheetahString::from(topic.as_str())), idx); + } + black_box(map) + }) + }); + group.bench_function("PackedCheetahString long topics", |b| { + b.iter(|| { + let mut map = HashMap::with_capacity(TOPIC_COUNT); + for (idx, topic) in long_topics.iter().enumerate() { + map.insert(black_box(PackedCheetahString::from(topic.as_str())), idx); + } + black_box(map) + }) + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_packed_construction, + bench_packed_push_str, + bench_packed_topic_insert +); +criterion_main!(benches); diff --git a/docs/simd-portability.md b/docs/simd-portability.md new file mode 100644 index 0000000..3b60da2 --- /dev/null +++ b/docs/simd-portability.md @@ -0,0 +1,33 @@ +# SIMD Portability Strategy + +## Current Decision + +The `simd` feature remains an x86_64 SSE2-only optimization path for now. +It is compiled only behind `#[cfg(all(feature = "simd", target_arch = "x86_64"))]`. + +The accelerated surface is intentionally limited to selected byte comparisons: + +- equality +- `starts_with` +- `ends_with` + +Substring search remains on the `memchr`/`memmem` backend. The project should +not claim SIMD acceleration for `find` or `contains` unless a dedicated search +implementation is wired into those APIs and benchmarked against `memchr`. + +## AArch64 / NEON Position + +No AArch64 NEON implementation is being added in this phase. The repository has +no current ARM benchmark evidence, and adding a second architecture-specific +unsafe/vectorized path without target hardware evidence would increase +maintenance risk more than it would improve the supported API. + +Before adding NEON support, require: + +- an AArch64 CI or local benchmark target +- Criterion comparisons against the scalar and `memchr`/`memmem` paths +- feature-gated implementation isolated from the x86_64 SSE2 module +- tests covering the same equality, prefix, and suffix behavior as SSE2 + +Until those gates exist, the portable behavior is scalar plus `memchr`/`memmem`, +with SSE2 as an optional x86_64 fast path. diff --git a/scripts/verify-packed.ps1 b/scripts/verify-packed.ps1 new file mode 100644 index 0000000..06ee868 --- /dev/null +++ b/scripts/verify-packed.ps1 @@ -0,0 +1,202 @@ +param( + [switch]$RunMiri, + [switch]$RunSanitizer, + [switch]$RunFuzz, + [switch]$RunBench, + [int]$FuzzRuns = 1000, + [string]$OutputRoot = "bench-results/packed-evidence" +) + +$ErrorActionPreference = "Continue" +$RepoRoot = Split-Path -Parent $PSScriptRoot +$Stamp = Get-Date -Format "yyyyMMdd-HHmmss" +$OutputDir = Join-Path (Join-Path $RepoRoot $OutputRoot) $Stamp +$SummaryPath = Join-Path $OutputDir "summary.md" +$FuzzRunArg = "-runs=$FuzzRuns" + +New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null + +function Add-SummaryLine { + param([string]$Line) + Add-Content -LiteralPath $SummaryPath -Value $Line -Encoding UTF8 +} + +function Test-CommandAvailable { + param([string]$Command) + $null -ne (Get-Command $Command -ErrorAction SilentlyContinue) +} + +function Add-AsanRuntimeToPath { + $Roots = @( + (Join-Path $env:ProgramFiles "Microsoft Visual Studio"), + (Join-Path $env:ProgramFiles "LLVM") + ) | Where-Object { Test-Path -LiteralPath $_ } + + $RuntimeCandidates = foreach ($Root in $Roots) { + $Matches = Get-ChildItem -LiteralPath $Root -Recurse -Filter "clang_rt.asan_dynamic-x86_64.dll" -ErrorAction SilentlyContinue + $Matches | Where-Object { $_.FullName -like "*Hostx64*x64*" } | Select-Object -First 1 + $Matches | Select-Object -First 1 + } + $Runtime = $RuntimeCandidates | Select-Object -First 1 + + if ($null -eq $Runtime) { + Add-SummaryLine "- ``ASan runtime``: not found on PATH or known install roots" + return + } + + $RuntimeDir = Split-Path -Parent $Runtime.FullName + $PathParts = $env:PATH -split ";" + if ($PathParts -notcontains $RuntimeDir) { + $env:PATH = "$RuntimeDir;$env:PATH" + } + + Add-SummaryLine "- ``ASan runtime``: using ``$RuntimeDir``" +} + +function Invoke-Gate { + param( + [string]$Name, + [string]$CommandLine, + [string]$LogName, + [scriptblock]$Before, + [scriptblock]$After + ) + + $LogPath = Join-Path $OutputDir $LogName + $StdoutPath = Join-Path $OutputDir "$LogName.stdout" + $StderrPath = Join-Path $OutputDir "$LogName.stderr" + Add-SummaryLine "- ``$Name``: running" + Push-Location $RepoRoot + try { + if ($Before) { + & $Before + } + $Process = Start-Process ` + -FilePath "cmd.exe" ` + -ArgumentList @("/d", "/s", "/c", $CommandLine) ` + -RedirectStandardOutput $StdoutPath ` + -RedirectStandardError $StderrPath ` + -WindowStyle Hidden ` + -Wait ` + -PassThru ` + -ErrorAction Stop + if ($null -eq $Process) { + $ExitCode = 1 + } else { + $ExitCode = $Process.ExitCode + } + } catch { + $_ | Out-File -FilePath $StderrPath -Encoding UTF8 + $ExitCode = 1 + } finally { + if ($After) { + & $After + } + Pop-Location + } + + $LogParts = @($StdoutPath, $StderrPath) | Where-Object { Test-Path -LiteralPath $_ } + if ($LogParts.Count -gt 0) { + $LogLines = @(Get-Content -LiteralPath $LogParts) + Set-Content -LiteralPath $LogPath -Encoding UTF8 -Value $LogLines + $LogLines | Out-Host + Remove-Item -LiteralPath $LogParts -Force -ErrorAction SilentlyContinue + } else { + New-Item -ItemType File -Force -Path $LogPath | Out-Null + } + + if ($ExitCode -eq 0) { + Add-SummaryLine "- ``$Name``: PASS, see ``$LogName``" + } else { + Add-SummaryLine "- ``$Name``: FAIL exit $ExitCode, see ``$LogName``" + } + + return [int]$ExitCode +} + +Set-Content -LiteralPath $SummaryPath -Encoding UTF8 -Value @( + "# Packed Evidence Capture", + "", + "- Date: $(Get-Date -Format o)", + "- Repository: cheetah-string", + "- Output directory: $OutputDir", + "" +) + +$Failed = 0 + +$Failed += Invoke-Gate ` + -Name "cargo test --features experimental-packed" ` + -LogName "packed-test.txt" ` + -CommandLine "cargo test --features experimental-packed" + +if ($RunMiri) { + $MiriVersion = cargo +nightly miri --version 2>$null + if ($LASTEXITCODE -eq 0) { + Add-SummaryLine "- ``cargo +nightly miri --version``: $MiriVersion" + $Failed += Invoke-Gate ` + -Name "cargo +nightly miri test --features experimental-packed packed" ` + -LogName "packed-miri.txt" ` + -CommandLine "cargo +nightly miri test --features experimental-packed packed" + } else { + Add-SummaryLine "- ``miri``: SKIP, install with ``rustup component add --toolchain nightly-x86_64-pc-windows-msvc miri``" + } +} else { + Add-SummaryLine "- ``miri``: SKIP, pass ``-RunMiri`` to execute" +} + +if ($RunSanitizer) { + Add-AsanRuntimeToPath + $Failed += Invoke-Gate ` + -Name "address sanitizer packed tests" ` + -LogName "packed-asan.txt" ` + -Before { + $script:PreviousRustflags = $env:RUSTFLAGS + $script:HadRustflags = Test-Path Env:RUSTFLAGS + $env:RUSTFLAGS = "-Z sanitizer=address" + } ` + -After { + if ($script:HadRustflags) { + $env:RUSTFLAGS = $script:PreviousRustflags + } else { + Remove-Item Env:RUSTFLAGS -ErrorAction SilentlyContinue + } + } ` + -CommandLine "cargo +nightly test --features experimental-packed packed" +} else { + Add-SummaryLine "- ``address sanitizer``: SKIP, pass ``-RunSanitizer`` on a supported nightly target" +} + +if ($RunFuzz) { + if (Test-CommandAvailable "cargo-fuzz") { + Add-AsanRuntimeToPath + $Failed += Invoke-Gate ` + -Name "cargo +nightly fuzz run fuzz_packed_from_bytes" ` + -LogName "fuzz-packed-from-bytes.txt" ` + -CommandLine "cargo +nightly fuzz run fuzz_packed_from_bytes -- $FuzzRunArg" + $Failed += Invoke-Gate ` + -Name "cargo +nightly fuzz run fuzz_packed_push_str" ` + -LogName "fuzz-packed-push-str.txt" ` + -CommandLine "cargo +nightly fuzz run fuzz_packed_push_str -- $FuzzRunArg" + } else { + Add-SummaryLine "- ``cargo-fuzz``: SKIP, install with ``cargo install cargo-fuzz``" + } +} else { + Add-SummaryLine "- ``cargo fuzz``: SKIP, pass ``-RunFuzz`` to execute" +} + +if ($RunBench) { + $Failed += Invoke-Gate ` + -Name "cargo bench --bench packed --features experimental-packed" ` + -LogName "packed-bench.txt" ` + -CommandLine "cargo bench --bench packed --features experimental-packed" +} else { + Add-SummaryLine "- ``packed benchmark``: SKIP, pass ``-RunBench`` to execute" +} + +Add-SummaryLine "" +Add-SummaryLine "Total failing required gates: $Failed" + +if ($Failed -ne 0) { + exit 1 +} diff --git a/src/cheetah_str.rs b/src/cheetah_str.rs index 2d6020b..e24db4a 100644 --- a/src/cheetah_str.rs +++ b/src/cheetah_str.rs @@ -6,12 +6,11 @@ use core::cmp::Ordering; use core::fmt; use core::hash::{Hash, Hasher}; use core::ops::Deref; -use core::str::{self, FromStr}; +use core::str::FromStr; +use crate::inline::InlineStr; use crate::CheetahString; -const INLINE_CAPACITY: usize = 23; - /// Immutable, clone-cheap string value for key/name/topic style workloads. /// /// `CheetahStr` intentionally has no mutation API. Use [`CheetahBuilder`] or @@ -25,10 +24,7 @@ pub struct CheetahStr { #[derive(Clone)] enum Repr { - Inline { - len: u8, - data: [u8; INLINE_CAPACITY], - }, + Inline(InlineStr), Static(&'static str), Shared(Arc), } @@ -38,10 +34,7 @@ impl CheetahStr { #[inline] pub const fn empty() -> Self { Self { - inner: Repr::Inline { - len: 0, - data: [0; INLINE_CAPACITY], - }, + inner: Repr::Inline(InlineStr::empty()), } } @@ -62,14 +55,9 @@ impl CheetahStr { /// Creates an immutable string from a borrowed string slice. #[inline] pub fn from_slice(s: &str) -> Self { - if s.len() <= INLINE_CAPACITY { - let mut data = [0u8; INLINE_CAPACITY]; - data[..s.len()].copy_from_slice(s.as_bytes()); + if let Some(inline) = InlineStr::from_str(s) { Self { - inner: Repr::Inline { - len: s.len() as u8, - data, - }, + inner: Repr::Inline(inline), } } else { Self { @@ -81,8 +69,10 @@ impl CheetahStr { /// Creates an immutable string from owned storage. #[inline] pub fn from_string(s: String) -> Self { - if s.len() <= INLINE_CAPACITY { - Self::from_slice(&s) + if let Some(inline) = InlineStr::from_str(&s) { + Self { + inner: Repr::Inline(inline), + } } else { Self { inner: Repr::Shared(s.into_boxed_str().into()), @@ -94,10 +84,7 @@ impl CheetahStr { #[inline] pub fn as_str(&self) -> &str { match &self.inner { - Repr::Inline { len, data } => { - // SAFETY: Inline data is copied only from valid UTF-8 strings. - unsafe { str::from_utf8_unchecked(&data[..*len as usize]) } - } + Repr::Inline(inline) => inline.as_str(), Repr::Static(s) => s, Repr::Shared(s) => s.as_ref(), } diff --git a/src/cheetah_string.rs b/src/cheetah_string.rs index f1dd1fc..2c01108 100644 --- a/src/cheetah_string.rs +++ b/src/cheetah_string.rs @@ -9,19 +9,24 @@ use core::hash::{Hash, Hasher}; use core::ops::{Add, AddAssign, Deref}; use core::str::{self, FromStr, Utf8Error}; +mod pattern; +mod repr; + +use crate::inline::InlineStr; +use pattern::StrPatternImpl; +pub use pattern::{SplitPattern, SplitStr, SplitWrapper, StrPattern}; +use repr::{InnerString, INLINE_CAPACITY}; + #[derive(Clone)] #[repr(transparent)] pub struct CheetahString { - pub(super) inner: InnerString, + inner: InnerString, } impl Default for CheetahString { fn default() -> Self { CheetahString { - inner: InnerString::Inline { - len: 0, - data: [0; INLINE_CAPACITY], - }, + inner: InnerString::Inline(InlineStr::empty()), } } } @@ -166,11 +171,8 @@ impl From for String { fn from(s: CheetahString) -> Self { match s { CheetahString { - inner: InnerString::Inline { len, data }, - } => { - // SAFETY: Inline strings are always valid UTF-8 - unsafe { String::from_utf8_unchecked(data[..len as usize].to_vec()) } - } + inner: InnerString::Inline(inline), + } => inline.into_string(), CheetahString { inner: InnerString::Static(s), } => s.to_string(), @@ -225,10 +227,7 @@ impl CheetahString { #[inline] pub const fn empty() -> Self { CheetahString { - inner: InnerString::Inline { - len: 0, - data: [0; INLINE_CAPACITY], - }, + inner: InnerString::Inline(InlineStr::empty()), } } @@ -258,18 +257,16 @@ impl CheetahString { #[inline] fn from_validated_vec_unchecked(s: Vec) -> Self { if s.len() <= INLINE_CAPACITY { - let mut data = [0u8; INLINE_CAPACITY]; - data[..s.len()].copy_from_slice(&s); - CheetahString { - inner: InnerString::Inline { - len: s.len() as u8, - data, - }, - } - } else { // SAFETY: Callers validate UTF-8 before reaching this helper. - CheetahString::from_builder_string(unsafe { String::from_utf8_unchecked(s) }) + let value = unsafe { str::from_utf8_unchecked(&s) }; + let inline = InlineStr::from_str(value).expect("short str must fit inline storage"); + return CheetahString { + inner: InnerString::Inline(inline), + }; } + + // SAFETY: Callers validate UTF-8 before reaching this helper. + CheetahString::from_builder_string(unsafe { String::from_utf8_unchecked(s) }) } /// Creates a `CheetahString` from a byte vector with UTF-8 validation. @@ -368,15 +365,9 @@ impl CheetahString { #[inline] pub fn from_slice(s: &str) -> Self { - if s.len() <= INLINE_CAPACITY { - // Use inline storage for short strings - let mut data = [0u8; INLINE_CAPACITY]; - data[..s.len()].copy_from_slice(s.as_bytes()); + if let Some(inline) = InlineStr::from_str(s) { CheetahString { - inner: InnerString::Inline { - len: s.len() as u8, - data, - }, + inner: InnerString::Inline(inline), } } else { // Use Arc for long borrowed strings to avoid the extra String header. @@ -410,15 +401,9 @@ impl CheetahString { /// `CheetahStr`. #[inline] pub fn from_string_shared(s: String) -> Self { - if s.len() <= INLINE_CAPACITY { - // Use inline storage for short strings - let mut data = [0u8; INLINE_CAPACITY]; - data[..s.len()].copy_from_slice(s.as_bytes()); + if let Some(inline) = InlineStr::from_str(&s) { CheetahString { - inner: InnerString::Inline { - len: s.len() as u8, - data, - }, + inner: InnerString::Inline(inline), } } else { // Use Arc for long strings to avoid double allocation @@ -432,13 +417,9 @@ impl CheetahString { #[inline] fn from_builder_string(s: String) -> Self { if s.len() <= INLINE_CAPACITY && s.capacity() <= INLINE_CAPACITY { - let mut data = [0u8; INLINE_CAPACITY]; - data[..s.len()].copy_from_slice(s.as_bytes()); + let inline = InlineStr::from_str(&s).expect("short String must fit inline storage"); CheetahString { - inner: InnerString::Inline { - len: s.len() as u8, - data, - }, + inner: InnerString::Inline(inline), } } else { CheetahString { @@ -483,11 +464,7 @@ impl CheetahString { #[inline] pub fn as_str(&self) -> &str { match &self.inner { - InnerString::Inline { len, data } => { - // SAFETY: Inline strings are only created from valid UTF-8 sources. - // The data is always valid UTF-8 up to len bytes. - unsafe { str::from_utf8_unchecked(&data[..*len as usize]) } - } + InnerString::Inline(inline) => inline.as_str(), InnerString::Static(s) => s, InnerString::Shared(s) => s.as_ref(), InnerString::Owned(s) => s.as_str(), @@ -497,7 +474,7 @@ impl CheetahString { #[inline] pub fn as_bytes(&self) -> &[u8] { match &self.inner { - InnerString::Inline { len, data } => &data[..*len as usize], + InnerString::Inline(inline) => inline.as_bytes(), InnerString::Static(s) => s.as_bytes(), InnerString::Shared(s) => s.as_bytes(), InnerString::Owned(s) => s.as_bytes(), @@ -507,7 +484,7 @@ impl CheetahString { #[inline] pub fn len(&self) -> usize { match &self.inner { - InnerString::Inline { len, .. } => *len as usize, + InnerString::Inline(inline) => inline.len(), InnerString::Static(s) => s.len(), InnerString::Shared(s) => s.len(), InnerString::Owned(s) => s.len(), @@ -517,7 +494,7 @@ impl CheetahString { #[inline] pub fn is_empty(&self) -> bool { match &self.inner { - InnerString::Inline { len, .. } => *len == 0, + InnerString::Inline(inline) => inline.is_empty(), InnerString::Static(s) => s.is_empty(), InnerString::Shared(s) => s.is_empty(), InnerString::Owned(s) => s.is_empty(), @@ -624,8 +601,9 @@ impl CheetahString { /// Returns `true` if the string contains the given pattern. /// - /// When the `simd` feature is enabled, this method uses SIMD instructions - /// for improved performance on longer patterns. + /// This method uses the `memchr`/`memmem` search backend. The `simd` + /// feature currently accelerates equality, prefix, and suffix checks, not + /// substring search. /// /// # Examples /// @@ -665,8 +643,9 @@ impl CheetahString { /// Returns the byte index of the first occurrence of the pattern, or `None` if not found. /// - /// When the `simd` feature is enabled, this method uses SIMD instructions - /// for improved performance on longer patterns. + /// This method uses the `memchr`/`memmem` search backend. The `simd` + /// feature currently accelerates equality, prefix, and suffix checks, not + /// substring search. /// /// # Examples /// @@ -864,7 +843,9 @@ impl CheetahString { /// /// # Panics /// - /// Panics if the indices are not on valid UTF-8 character boundaries. + /// Panics if the range is out of bounds, inverted, or not on valid UTF-8 + /// character boundaries. Use [`CheetahString::try_substring`] for a + /// recoverable error. /// /// # Examples /// @@ -877,7 +858,48 @@ impl CheetahString { /// ``` #[inline] pub fn substring(&self, start: usize, end: usize) -> CheetahString { - CheetahString::from_slice(&self.as_str()[start..end]) + self.try_substring(start, end) + .expect("substring range must be in bounds and on UTF-8 character boundaries") + } + + /// Returns a substring as a new `CheetahString`, or a public error when + /// the requested range is invalid. + /// + /// # Examples + /// + /// ``` + /// use cheetah_string::CheetahString; + /// + /// let s = CheetahString::from("hello world"); + /// assert_eq!(s.try_substring(0, 5).unwrap(), "hello"); + /// assert!(s.try_substring(0, 20).is_err()); + /// ``` + #[inline] + pub fn try_substring(&self, start: usize, end: usize) -> crate::Result { + let value = self.as_str(); + let len = value.len(); + + if start > end { + return Err(crate::Error::InvalidRange { start, end }); + } + + if start > len { + return Err(crate::Error::IndexOutOfBounds { index: start, len }); + } + + if end > len { + return Err(crate::Error::IndexOutOfBounds { index: end, len }); + } + + if !value.is_char_boundary(start) { + return Err(crate::Error::InvalidCharBoundary { index: start }); + } + + if !value.is_char_boundary(end) { + return Err(crate::Error::InvalidCharBoundary { index: end }); + } + + Ok(CheetahString::from_slice(&value[start..end])) } /// Repeats the string `n` times. @@ -928,11 +950,8 @@ impl CheetahString { } match &mut self.inner { - InnerString::Inline { len, data } => { - let total_len = *len as usize + string.len(); - if total_len <= INLINE_CAPACITY { - data[*len as usize..total_len].copy_from_slice(string.as_bytes()); - *len = total_len as u8; + InnerString::Inline(inline) => { + if inline.push_str(string) { return; } } @@ -993,10 +1012,10 @@ impl CheetahString { } match &mut self.inner { - InnerString::Inline { len, .. } if *len as usize + additional <= INLINE_CAPACITY => { + InnerString::Inline(inline) if inline.len() + additional <= INLINE_CAPACITY => { return; } - InnerString::Inline { .. } => {} + InnerString::Inline(_) => {} InnerString::Owned(s) => { s.reserve(additional); return; @@ -1239,169 +1258,6 @@ impl AddAssign<&CheetahString> for CheetahString { } } -/// Maximum capacity for inline string storage (23 bytes + 1 byte for length = 24 bytes total) -const INLINE_CAPACITY: usize = 23; - -/// The `InnerString` enum represents different types of string storage. -/// -/// This enum uses Small String Optimization (SSO) to avoid heap allocations for short strings. -/// -/// Variants: -/// -/// * `Inline` - Inline storage for strings <= 23 bytes (zero heap allocations). -/// * `Static(&'static str)` - A static string slice (zero heap allocations). -/// * `Shared(Arc)` - A reference-counted string slice (single heap allocation, optimized). -/// * `Owned(String)` - An owned heap string used for builder-style mutation. -#[derive(Clone)] -pub(super) enum InnerString { - /// Inline storage for short strings (up to 23 bytes). - /// Stores the length and data directly without heap allocation. - Inline { - len: u8, - data: [u8; INLINE_CAPACITY], - }, - /// Static string slice with 'static lifetime. - Static(&'static str), - /// Reference-counted string slice (single heap allocation). - /// Preferred for long immutable strings created from owned or borrowed data. - Shared(Arc), - /// Owned heap-allocated string used when exclusive mutability matters. - Owned(String), -} - -// Sealed trait pattern to support both &str and char in starts_with/ends_with/contains -mod private { - use alloc::string::String; - - pub trait Sealed {} - impl Sealed for char {} - impl Sealed for &str {} - impl Sealed for &String {} - - pub trait SplitSealed {} - impl SplitSealed for char {} - impl SplitSealed for &str {} -} - -/// A pattern that can be used with `starts_with` and `ends_with` methods. -pub trait StrPattern: private::Sealed { - #[doc(hidden)] - fn as_str_pattern(&self) -> StrPatternImpl<'_>; -} - -#[doc(hidden)] -pub enum StrPatternImpl<'a> { - Char(char), - Str(&'a str), -} - -impl StrPattern for char { - #[inline] - fn as_str_pattern(&self) -> StrPatternImpl<'_> { - StrPatternImpl::Char(*self) - } -} - -impl StrPattern for &str { - #[inline] - fn as_str_pattern(&self) -> StrPatternImpl<'_> { - StrPatternImpl::Str(self) - } -} - -impl StrPattern for &String { - #[inline] - fn as_str_pattern(&self) -> StrPatternImpl<'_> { - StrPatternImpl::Str(self.as_str()) - } -} - -/// A pattern that can be used with `split` method. -pub trait SplitPattern<'a>: private::SplitSealed { - #[doc(hidden)] - fn split_str(self, s: &'a str) -> SplitWrapper<'a>; -} - -impl SplitPattern<'_> for char { - fn split_str(self, s: &str) -> SplitWrapper<'_> { - SplitWrapper::Char(s.split(self)) - } -} - -impl<'a> SplitPattern<'a> for &'a str { - fn split_str(self, s: &'a str) -> SplitWrapper<'a> { - let inner = match single_char_pattern(self) { - Some(ch) => SplitStrInner::Char(s.split(ch)), - None => SplitStrInner::Str(s.split(self)), - }; - - SplitWrapper::Str(SplitStr(inner)) - } -} - -/// Helper struct for splitting strings by a string pattern -pub struct SplitStr<'a>(SplitStrInner<'a>); - -enum SplitStrInner<'a> { - Str(str::Split<'a, &'a str>), - Char(str::Split<'a, char>), -} - -#[inline] -fn single_char_pattern(pattern: &str) -> Option { - let mut chars = pattern.chars(); - let ch = chars.next()?; - - if chars.next().is_none() { - Some(ch) - } else { - None - } -} - -impl<'a> Iterator for SplitStr<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - match &mut self.0 { - SplitStrInner::Str(iter) => iter.next(), - SplitStrInner::Char(iter) => iter.next(), - } - } -} - -/// Wrapper for split iterator that supports both char and str patterns -pub enum SplitWrapper<'a> { - #[doc(hidden)] - Char(str::Split<'a, char>), - #[doc(hidden)] - Str(SplitStr<'a>), -} - -impl<'a> Iterator for SplitWrapper<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - match self { - SplitWrapper::Char(iter) => iter.next(), - SplitWrapper::Str(iter) => iter.next(), - } - } -} - -impl<'a> DoubleEndedIterator for SplitWrapper<'a> { - fn next_back(&mut self) -> Option { - match self { - SplitWrapper::Char(iter) => iter.next_back(), - SplitWrapper::Str(_) => { - // String pattern split doesn't support reverse iteration - // This is consistent with std::str::Split<&str> - panic!("split with string pattern does not support reverse iteration") - } - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -1461,9 +1317,9 @@ mod tests { let s = CheetahString::try_from_vec(b"hello".to_vec()).expect("valid utf-8"); match &s.inner { - InnerString::Inline { len, data } => { - assert_eq!(*len as usize, 5); - assert_eq!(&data[..5], b"hello"); + InnerString::Inline(inline) => { + assert_eq!(inline.len(), 5); + assert_eq!(inline.as_bytes(), b"hello"); } other => panic!( "expected inline storage for short validated Vec, got {:?}", diff --git a/src/cheetah_string/pattern.rs b/src/cheetah_string/pattern.rs new file mode 100644 index 0000000..eb3b2f2 --- /dev/null +++ b/src/cheetah_string/pattern.rs @@ -0,0 +1,134 @@ +use alloc::string::String; +use core::str; + +// Sealed trait pattern to support both &str and char in starts_with/ends_with/contains. +mod private { + use alloc::string::String; + + pub trait Sealed {} + impl Sealed for char {} + impl Sealed for &str {} + impl Sealed for &String {} + + pub trait SplitSealed {} + impl SplitSealed for char {} + impl SplitSealed for &str {} +} + +/// A pattern that can be used with `starts_with` and `ends_with` methods. +pub trait StrPattern: private::Sealed { + #[doc(hidden)] + fn as_str_pattern(&self) -> StrPatternImpl<'_>; +} + +#[doc(hidden)] +pub enum StrPatternImpl<'a> { + Char(char), + Str(&'a str), +} + +impl StrPattern for char { + #[inline] + fn as_str_pattern(&self) -> StrPatternImpl<'_> { + StrPatternImpl::Char(*self) + } +} + +impl StrPattern for &str { + #[inline] + fn as_str_pattern(&self) -> StrPatternImpl<'_> { + StrPatternImpl::Str(self) + } +} + +impl StrPattern for &String { + #[inline] + fn as_str_pattern(&self) -> StrPatternImpl<'_> { + StrPatternImpl::Str(self.as_str()) + } +} + +/// A pattern that can be used with `split` method. +pub trait SplitPattern<'a>: private::SplitSealed { + #[doc(hidden)] + fn split_str(self, s: &'a str) -> SplitWrapper<'a>; +} + +impl SplitPattern<'_> for char { + fn split_str(self, s: &str) -> SplitWrapper<'_> { + SplitWrapper::Char(s.split(self)) + } +} + +impl<'a> SplitPattern<'a> for &'a str { + fn split_str(self, s: &'a str) -> SplitWrapper<'a> { + let inner = match single_char_pattern(self) { + Some(ch) => SplitStrInner::Char(s.split(ch)), + None => SplitStrInner::Str(s.split(self)), + }; + + SplitWrapper::Str(SplitStr(inner)) + } +} + +/// Helper struct for splitting strings by a string pattern. +pub struct SplitStr<'a>(SplitStrInner<'a>); + +enum SplitStrInner<'a> { + Str(str::Split<'a, &'a str>), + Char(str::Split<'a, char>), +} + +#[inline] +fn single_char_pattern(pattern: &str) -> Option { + let mut chars = pattern.chars(); + let ch = chars.next()?; + + if chars.next().is_none() { + Some(ch) + } else { + None + } +} + +impl<'a> Iterator for SplitStr<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + match &mut self.0 { + SplitStrInner::Str(iter) => iter.next(), + SplitStrInner::Char(iter) => iter.next(), + } + } +} + +/// Wrapper for split iterator that supports both char and str patterns. +pub enum SplitWrapper<'a> { + #[doc(hidden)] + Char(str::Split<'a, char>), + #[doc(hidden)] + Str(SplitStr<'a>), +} + +impl<'a> Iterator for SplitWrapper<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + match self { + SplitWrapper::Char(iter) => iter.next(), + SplitWrapper::Str(iter) => iter.next(), + } + } +} + +impl<'a> DoubleEndedIterator for SplitWrapper<'a> { + fn next_back(&mut self) -> Option { + match self { + SplitWrapper::Char(iter) => iter.next_back(), + SplitWrapper::Str(_) => { + // String pattern split doesn't support reverse iteration. + panic!("split with string pattern does not support reverse iteration") + } + } + } +} diff --git a/src/cheetah_string/repr.rs b/src/cheetah_string/repr.rs new file mode 100644 index 0000000..87d0e8e --- /dev/null +++ b/src/cheetah_string/repr.rs @@ -0,0 +1,29 @@ +use alloc::string::String; +use alloc::sync::Arc; + +use crate::inline::InlineStr; +pub(super) use crate::inline::INLINE_CAPACITY; + +/// The `InnerString` enum represents different types of string storage. +/// +/// This enum uses Small String Optimization (SSO) to avoid heap allocations for short strings. +/// +/// Variants: +/// +/// * `Inline` - Inline storage for strings <= 23 bytes (zero heap allocations). +/// * `Static(&'static str)` - A static string slice (zero heap allocations). +/// * `Shared(Arc)` - A reference-counted string slice (single heap allocation, optimized). +/// * `Owned(String)` - An owned heap string used for builder-style mutation. +#[derive(Clone)] +pub(super) enum InnerString { + /// Inline storage for short strings (up to 23 bytes). + /// Stores the length and data directly without heap allocation. + Inline(InlineStr), + /// Static string slice with 'static lifetime. + Static(&'static str), + /// Reference-counted string slice (single heap allocation). + /// Preferred for long immutable strings created from owned or borrowed data. + Shared(Arc), + /// Owned heap-allocated string used when exclusive mutability matters. + Owned(String), +} diff --git a/src/error.rs b/src/error.rs index ca275c8..3c16da1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,8 @@ pub enum Error { Utf8Error(Utf8Error), /// Index out of bounds IndexOutOfBounds { index: usize, len: usize }, + /// Invalid range where the start is greater than the end + InvalidRange { start: usize, end: usize }, /// Invalid character boundary InvalidCharBoundary { index: usize }, } @@ -19,6 +21,9 @@ impl fmt::Display for Error { Error::IndexOutOfBounds { index, len } => { write!(f, "index {} out of bounds (len: {})", index, len) } + Error::InvalidRange { start, end } => { + write!(f, "range start {} is greater than end {}", start, end) + } Error::InvalidCharBoundary { index } => { write!(f, "index {} is not a char boundary", index) } diff --git a/src/inline.rs b/src/inline.rs new file mode 100644 index 0000000..dc2601c --- /dev/null +++ b/src/inline.rs @@ -0,0 +1,74 @@ +use alloc::string::String; +use core::str; + +/// Maximum capacity for inline string storage (23 bytes + 1 byte for length = 24 bytes total). +pub(crate) const INLINE_CAPACITY: usize = 23; + +/// Shared inline storage for short UTF-8 strings. +#[derive(Clone, Copy)] +pub(crate) struct InlineStr { + len: u8, + data: [u8; INLINE_CAPACITY], +} + +impl InlineStr { + #[inline] + pub(crate) const fn empty() -> Self { + Self { + len: 0, + data: [0; INLINE_CAPACITY], + } + } + + #[inline] + pub(crate) fn from_str(value: &str) -> Option { + if value.len() > INLINE_CAPACITY { + return None; + } + + let mut inline = Self::empty(); + inline.data[..value.len()].copy_from_slice(value.as_bytes()); + inline.len = value.len() as u8; + Some(inline) + } + + #[inline] + pub(crate) fn as_str(&self) -> &str { + // SAFETY: InlineStr is only constructed from valid UTF-8 strings. + unsafe { str::from_utf8_unchecked(self.as_bytes()) } + } + + #[inline] + pub(crate) fn as_bytes(&self) -> &[u8] { + &self.data[..self.len as usize] + } + + #[inline] + pub(crate) fn len(&self) -> usize { + self.len as usize + } + + #[inline] + pub(crate) fn is_empty(&self) -> bool { + self.len == 0 + } + + #[inline] + pub(crate) fn push_str(&mut self, value: &str) -> bool { + let current_len = self.len(); + let total_len = current_len + value.len(); + if total_len > INLINE_CAPACITY { + return false; + } + + self.data[current_len..total_len].copy_from_slice(value.as_bytes()); + self.len = total_len as u8; + true + } + + #[inline] + pub(crate) fn into_string(self) -> String { + // SAFETY: InlineStr is only constructed from valid UTF-8 strings. + unsafe { String::from_utf8_unchecked(self.as_bytes().to_vec()) } + } +} diff --git a/src/lib.rs b/src/lib.rs index 14f8fb7..eed2fe3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,9 @@ //! - `ends_with()` - Pattern suffix matching //! - Equality comparisons (`==`, `!=`) //! +//! Substring search through `find()` and `contains()` continues to use +//! `memchr`/`memmem`, which is the stable default search backend. +//! //! The implementation automatically uses SIMD for strings >= 16 bytes and falls back to scalar operations //! for smaller inputs or when SIMD is not available. //! @@ -44,7 +47,7 @@ //! //! ``` //! -//! Using accelerated search operations: +//! Using search operations: //! ```rust //! use cheetah_string::CheetahString; //! @@ -66,6 +69,7 @@ mod builder; mod cheetah_str; mod cheetah_string; mod error; +mod inline; mod search; #[cfg(feature = "bytes")] diff --git a/src/serde.rs b/src/serde.rs index f0d448d..ec07bca 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -1,9 +1,9 @@ -use crate::CheetahString; +use crate::{CheetahStr, CheetahString}; use alloc::string::String; use alloc::vec::Vec; use core::fmt; use core::str; -use serde::de::{Error, Visitor}; +use serde::de::{Error as DeError, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; impl Serialize for CheetahString { @@ -30,48 +30,48 @@ where fn visit_str(self, v: &str) -> Result where - E: Error, + E: DeError, { Ok(CheetahString::from_slice(v)) } fn visit_borrowed_str(self, v: &'a str) -> Result where - E: Error, + E: DeError, { Ok(CheetahString::from_slice(v)) } fn visit_string(self, v: String) -> Result where - E: Error, + E: DeError, { Ok(CheetahString::from_string(v)) } fn visit_bytes(self, v: &[u8]) -> Result where - E: Error, + E: DeError, { str::from_utf8(v) .map(CheetahString::from_slice) - .map_err(Error::custom) + .map_err(DeError::custom) } fn visit_borrowed_bytes(self, v: &'a [u8]) -> Result where - E: Error, + E: DeError, { str::from_utf8(v) .map(CheetahString::from_slice) - .map_err(Error::custom) + .map_err(DeError::custom) } fn visit_byte_buf(self, v: Vec) -> Result where - E: Error, + E: DeError, { - CheetahString::try_from_vec(v).map_err(Error::custom) + CheetahString::try_from_vec(v).map_err(DeError::custom) } } deserializer.deserialize_str(CheetahStringVisitor) @@ -85,3 +85,86 @@ impl<'de> Deserialize<'de> for CheetahString { cheetah_string(deserializer) } } + +impl Serialize for CheetahStr { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +pub fn cheetah_str<'de: 'a, 'a, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct CheetahStrVisitor; + + impl<'a> Visitor<'a> for CheetahStrVisitor { + type Value = CheetahStr; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: DeError, + { + Ok(CheetahStr::from_slice(v)) + } + + fn visit_borrowed_str(self, v: &'a str) -> Result + where + E: DeError, + { + Ok(CheetahStr::from_slice(v)) + } + + fn visit_string(self, v: String) -> Result + where + E: DeError, + { + Ok(CheetahStr::from_string(v)) + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: DeError, + { + str::from_utf8(v) + .map(CheetahStr::from_slice) + .map_err(DeError::custom) + } + + fn visit_borrowed_bytes(self, v: &'a [u8]) -> Result + where + E: DeError, + { + str::from_utf8(v) + .map(CheetahStr::from_slice) + .map_err(DeError::custom) + } + + fn visit_byte_buf(self, v: Vec) -> Result + where + E: DeError, + { + String::from_utf8(v) + .map(CheetahStr::from_string) + .map_err(DeError::custom) + } + } + + deserializer.deserialize_str(CheetahStrVisitor) +} + +impl<'de> Deserialize<'de> for CheetahStr { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + cheetah_str(deserializer) + } +} diff --git a/src/simd.rs b/src/simd.rs index 9b4d32f..9d1ba92 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -87,77 +87,6 @@ pub(crate) fn ends_with_bytes(haystack: &[u8], needle: &[u8]) -> bool { haystack.ends_with(needle) } -/// Find the first occurrence of needle in haystack using SIMD when available -#[allow(dead_code)] -#[inline] -pub(crate) fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { - if needle.is_empty() { - return Some(0); - } - - let needle_len = needle.len(); - - if needle_len > haystack.len() { - return None; - } - - if needle_len == 1 { - return find_first_byte(haystack, needle[0]); - } - - if needle_len < SIMD_THRESHOLD { - return find_short_bytes(haystack, needle); - } - - #[cfg(all(feature = "simd", target_arch = "x86_64"))] - { - if has_sse2() && needle_len >= SIMD_THRESHOLD && haystack.len() >= SIMD_THRESHOLD { - return unsafe { find_bytes_sse2(haystack, needle) }; - } - } - - // Fallback to standard search - haystack - .windows(needle_len) - .position(|window| window == needle) -} - -#[allow(dead_code)] -#[inline] -fn find_first_byte(haystack: &[u8], needle: u8) -> Option { - #[cfg(all(feature = "simd", target_arch = "x86_64"))] - { - if has_sse2() && haystack.len() >= SIMD_THRESHOLD { - return unsafe { find_byte_sse2(haystack, needle) }; - } - } - - haystack.iter().position(|&b| b == needle) -} - -#[allow(dead_code)] -#[inline] -fn find_short_bytes(haystack: &[u8], needle: &[u8]) -> Option { - debug_assert!(needle.len() > 1 && needle.len() < SIMD_THRESHOLD); - - let needle_len = needle.len(); - let last_start = haystack.len() - needle_len; - let mut pos = 0; - - while pos <= last_start { - let offset = find_first_byte(&haystack[pos..last_start + 1], needle[0])?; - let candidate = pos + offset; - - if haystack[candidate + 1..candidate + needle_len] == needle[1..] { - return Some(candidate); - } - - pos = candidate + 1; - } - - None -} - // SIMD implementations for x86_64 with SSE2 #[cfg(all(feature = "simd", target_arch = "x86_64"))] @@ -193,80 +122,6 @@ unsafe fn eq_bytes_sse2(a: &[u8], b: &[u8]) -> bool { true } -#[cfg(all(feature = "simd", target_arch = "x86_64"))] -#[allow(dead_code)] -#[target_feature(enable = "sse2")] -#[inline] -unsafe fn find_bytes_sse2(haystack: &[u8], needle: &[u8]) -> Option { - let haystack_len = haystack.len(); - let needle_len = needle.len(); - - if needle_len > haystack_len { - return None; - } - - // For small needles, use a simple SIMD approach - if needle_len == 1 { - return find_byte_sse2(haystack, needle[0]); - } - - // For larger needles, use a hybrid approach - // First, search for the first byte of the needle - let first_byte = needle[0]; - let mut pos = 0; - - while pos + needle_len <= haystack_len { - // Find the next occurrence of the first byte - let offset = find_byte_sse2(&haystack[pos..], first_byte)?; - let candidate_pos = pos + offset; - - // Check if the rest matches - if candidate_pos + needle_len <= haystack_len { - if eq_bytes_sse2(&haystack[candidate_pos..candidate_pos + needle_len], needle) { - return Some(candidate_pos); - } - pos = candidate_pos + 1; - } else { - return None; - } - } - - None -} - -#[cfg(all(feature = "simd", target_arch = "x86_64"))] -#[allow(dead_code)] -#[target_feature(enable = "sse2")] -#[inline] -unsafe fn find_byte_sse2(haystack: &[u8], needle: u8) -> Option { - let len = haystack.len(); - let mut offset = 0; - - // Broadcast the needle byte to all positions in the vector - let needle_vec = _mm_set1_epi8(needle as i8); - - // Process 16 bytes at a time - while offset + 16 <= len { - let haystack_vec = _mm_loadu_si128(haystack.as_ptr().add(offset) as *const __m128i); - let cmp = _mm_cmpeq_epi8(haystack_vec, needle_vec); - let mask = _mm_movemask_epi8(cmp); - - if mask != 0 { - // Found at least one match - let bit_pos = mask.trailing_zeros() as usize; - return Some(offset + bit_pos); - } - - offset += 16; - } - - // Handle remaining bytes - haystack[offset..len] - .iter() - .position(|&b| b == needle) - .map(|pos| offset + pos) -} - #[cfg(all(test, feature = "simd"))] mod tests { use super::*; @@ -300,29 +155,6 @@ mod tests { assert!(ends_with_bytes(haystack, b"")); } - #[test] - fn test_find_bytes() { - let haystack = b"hello world, this is a test"; - assert_eq!(find_bytes(haystack, b"world"), Some(6)); - assert_eq!(find_bytes(haystack, b"test"), Some(23)); - assert_eq!(find_bytes(haystack, b"xyz"), None); - assert_eq!(find_bytes(haystack, b""), Some(0)); - assert_eq!(find_bytes(b"aaabaaaab", b"aab"), Some(1)); - assert_eq!(find_bytes(b"abcabcabcd", b"abcd"), Some(6)); - assert_eq!(find_bytes(b"aaaaaa", b"aab"), None); - } - - #[test] - fn test_find_byte() { - let haystack = b"hello world"; - unsafe { - assert_eq!(find_byte_sse2(haystack, b'w'), Some(6)); - assert_eq!(find_byte_sse2(haystack, b'h'), Some(0)); - assert_eq!(find_byte_sse2(haystack, b'd'), Some(10)); - assert_eq!(find_byte_sse2(haystack, b'x'), None); - } - } - #[test] fn test_simd_threshold() { // Test with strings below SIMD threshold diff --git a/src/unsafe_proof.md b/src/unsafe_proof.md index bc284f5..6183c1f 100644 --- a/src/unsafe_proof.md +++ b/src/unsafe_proof.md @@ -34,6 +34,13 @@ This document applies only to `feature = "experimental-packed"` and the - layout snapshot comparison against stable `CheetahString` - MQ workload benchmark comparison against stable `CheetahString` +On Windows PowerShell, this command captures the required packed evidence under +`bench-results/packed-evidence//`: + +```powershell +scripts/verify-packed.ps1 -RunMiri -RunSanitizer -RunFuzz -RunBench +``` + ## Current Status The prototype is intentionally not wired into stable `CheetahString`. It is a diff --git a/tests/serde.rs b/tests/serde.rs new file mode 100644 index 0000000..12a0a3a --- /dev/null +++ b/tests/serde.rs @@ -0,0 +1,46 @@ +#![cfg(feature = "serde")] + +use cheetah_string::{CheetahStr, CheetahString, Error}; + +#[test] +fn cheetah_str_serializes_and_deserializes_inline_values() { + let value = CheetahStr::from("topic-a"); + + let json = serde_json::to_string(&value).unwrap(); + let decoded: CheetahStr = serde_json::from_str(&json).unwrap(); + + assert_eq!(json, "\"topic-a\""); + assert_eq!(decoded, "topic-a"); +} + +#[test] +fn cheetah_str_serializes_and_deserializes_shared_values() { + let value = CheetahStr::from("topic.".repeat(16)); + + let json = serde_json::to_string(&value).unwrap(); + let decoded: CheetahStr = serde_json::from_str(&json).unwrap(); + + assert_eq!(decoded, value); +} + +#[test] +fn try_substring_reports_public_error_variants() { + let value = CheetahString::from("hello"); + + assert_eq!(value.try_substring(1, 4).unwrap(), "ell"); + + assert!(matches!( + value.try_substring(0, 6), + Err(Error::IndexOutOfBounds { index: 6, len: 5 }) + )); + assert!(matches!( + value.try_substring(4, 2), + Err(Error::InvalidRange { start: 4, end: 2 }) + )); + + let unicode = CheetahString::from("éx"); + assert!(matches!( + unicode.try_substring(1, 2), + Err(Error::InvalidCharBoundary { index: 1 }) + )); +} diff --git a/tests/simd.rs b/tests/simd.rs index f915636..f31fe98 100644 --- a/tests/simd.rs +++ b/tests/simd.rs @@ -72,7 +72,7 @@ fn test_simd_ends_with() { } #[test] -fn test_simd_contains() { +fn test_contains_with_simd_feature_enabled() { let s = CheetahString::from("hello world, this is a test"); // Short patterns @@ -93,7 +93,7 @@ fn test_simd_contains() { } #[test] -fn test_simd_find() { +fn test_find_with_simd_feature_enabled() { let s = CheetahString::from("hello world, this is a test"); // Short patterns @@ -167,7 +167,7 @@ fn test_simd_boundary_conditions() { } #[test] -fn test_simd_pattern_at_end() { +fn test_search_pattern_at_end_with_simd_feature_enabled() { // Test finding pattern at the very end let s = CheetahString::from("aaaaaaaaaaaaaaab"); assert_eq!(s.find("b"), Some(15)); @@ -180,7 +180,7 @@ fn test_simd_pattern_at_end() { } #[test] -fn test_simd_multiple_occurrences() { +fn test_search_multiple_occurrences_with_simd_feature_enabled() { // Test that find returns the first occurrence let s = CheetahString::from("abcabcabc"); assert_eq!(s.find("abc"), Some(0));