Skip to content

Commit dbaf67d

Browse files
committed
nightly simd support
1 parent bbbac06 commit dbaf67d

File tree

14 files changed

+833
-686
lines changed

14 files changed

+833
-686
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ default = ["std"]
2828
serde = ["dep:serde", "std"]
2929
# enable allocations
3030
std = []
31-
# enable SIMD/SWAR optimizations for hot scanning paths
32-
simd = []
31+
# nightly-only: portable_simd (std::simd) — generates optimal SIMD for any target
32+
nightly-simd = []
3333

3434
[dependencies]
3535
serde = { version = "1", optional = true, features = ["derive"] }

examples/bench_debug.rs

Lines changed: 0 additions & 28 deletions
This file was deleted.

examples/check_idna.rs

Lines changed: 0 additions & 30 deletions
This file was deleted.

justfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ fmt *args:
1414

1515
# run tests on all feature combinations
1616
test *args:
17-
cargo hack test --feature-powerset {{args}}
17+
cargo hack test --feature-powerset --exclude-features nightly-simd,simd {{args}}
1818

1919
# type check and lint code on all feature combinations
2020
clippy *args:
21-
cargo hack clippy --feature-powerset {{args}} -- -D warnings
21+
cargo hack clippy --feature-powerset --exclude-features nightly-simd,simd {{args}} -- -D warnings
2222

2323
# lint documentation on all feature combinations
2424
doc *args:
25-
RUSTDOCFLAGS='-D warnings' cargo hack doc --feature-powerset {{args}}
25+
RUSTDOCFLAGS='-D warnings' cargo hack doc --feature-powerset --exclude-features nightly-simd,simd {{args}}

src/character_sets.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub const HEX: &[u8; 1023] = b"%00\0%01\0%02\0%03\0%04\0%05\0%06\0%07\0\
4444
// Character set: C0 control percent-encode
4545
// Encodes: U+0000–U+001F, U+007F, and U+0080–U+FFFF
4646
// ---------------------------------------------------------------------------
47-
pub const C0_CONTROL_PERCENT_ENCODE: [u8; 32] = [
47+
pub static C0_CONTROL_PERCENT_ENCODE: [u8; 32] = [
4848
0xFF, 0xFF, 0xFF, 0xFF, // 0x00–0x1F
4949
0x00, 0x00, 0x00, 0x00, // 0x20–0x3F
5050
0x00, 0x00, 0x00, 0x00, // 0x40–0x5F
@@ -60,7 +60,7 @@ pub const C0_CONTROL_PERCENT_ENCODE: [u8; 32] = [
6060
// Encodes C0-control, space (0x20), " (0x22), < (0x3C), > (0x3E),
6161
// ` (0x60), and everything >= 0x7F
6262
// ---------------------------------------------------------------------------
63-
pub const FRAGMENT_PERCENT_ENCODE: [u8; 32] = [
63+
pub static FRAGMENT_PERCENT_ENCODE: [u8; 32] = [
6464
0xFF, 0xFF, 0xFF, 0xFF, // 0x00–0x1F
6565
0x05, 0x00, 0x00, 0x50, // 0x20–0x3F: 0x05=space+", 0x50=<+>
6666
0x00, 0x00, 0x00, 0x00, // 0x40–0x5F
@@ -76,7 +76,7 @@ pub const FRAGMENT_PERCENT_ENCODE: [u8; 32] = [
7676
// Encodes C0-control, space, ", #, <, >, and everything >= 0x7F
7777
// NOTE: backtick (0x60) is NOT encoded here (only in fragment and path sets).
7878
// ---------------------------------------------------------------------------
79-
pub const QUERY_PERCENT_ENCODE: [u8; 32] = [
79+
pub static QUERY_PERCENT_ENCODE: [u8; 32] = [
8080
0xFF, 0xFF, 0xFF, 0xFF, // 0x00–0x1F
8181
0x0D, 0x00, 0x00, 0x50, // 0x20–0x3F: 0x0D=space+"+#, 0x50=<+>
8282
0x00, 0x00, 0x00, 0x00, // 0x40–0x5F
@@ -91,7 +91,7 @@ pub const QUERY_PERCENT_ENCODE: [u8; 32] = [
9191
// Character set: special-query percent-encode
9292
// Same as query but also encodes ' (0x27)
9393
// ---------------------------------------------------------------------------
94-
pub const SPECIAL_QUERY_PERCENT_ENCODE: [u8; 32] = [
94+
pub static SPECIAL_QUERY_PERCENT_ENCODE: [u8; 32] = [
9595
0xFF, 0xFF, 0xFF, 0xFF, // 0x00–0x1F
9696
0x8D, 0x00, 0x00, 0x50, // 0x20–0x3F: 0x8D=space+"+#+', 0x50=<+>
9797
0x00, 0x00, 0x00, 0x00, // 0x40–0x5F
@@ -106,7 +106,7 @@ pub const SPECIAL_QUERY_PERCENT_ENCODE: [u8; 32] = [
106106
// Character set: path percent-encode
107107
// Encodes C0-control, space, ", #, <, >, ?, ^, `, {, |, }, DEL, >= 0x80
108108
// ---------------------------------------------------------------------------
109-
pub const PATH_PERCENT_ENCODE: [u8; 32] = [
109+
pub static PATH_PERCENT_ENCODE: [u8; 32] = [
110110
0xFF, 0xFF, 0xFF, 0xFF, // 0x00–0x1F
111111
0x0D, 0x00, 0x00, 0xD0, // 0x20–0x3F: space+"+# and <+>+?
112112
0x00, 0x00, 0x00, 0x40, // 0x40–0x5F: ^ (0x5E)
@@ -122,7 +122,7 @@ pub const PATH_PERCENT_ENCODE: [u8; 32] = [
122122
// Encodes C0-control, space, ", #, /, :, ;, <, =, >, ?, @, [, \, ], ^,
123123
// `, {, |, }, DEL, and >= 0x80
124124
// ---------------------------------------------------------------------------
125-
pub const USERINFO_PERCENT_ENCODE: [u8; 32] = [
125+
pub static USERINFO_PERCENT_ENCODE: [u8; 32] = [
126126
0xFF, 0xFF, 0xFF, 0xFF, // 0x00–0x1F
127127
0x0D, 0x80, 0x00, 0xFC, // 0x20–0x3F: space+"+#+ / and :;<=>?
128128
0x01, 0x00, 0x00, 0x78, // 0x40–0x5F: @ and [,\,],^
@@ -137,7 +137,7 @@ pub const USERINFO_PERCENT_ENCODE: [u8; 32] = [
137137
// Character set: application/x-www-form-urlencoded percent-encode
138138
// Like userinfo but space is NOT encoded (it becomes '+' separately)
139139
// ---------------------------------------------------------------------------
140-
pub const WWW_FORM_URLENCODED_PERCENT_ENCODE: [u8; 32] = [
140+
pub static WWW_FORM_URLENCODED_PERCENT_ENCODE: [u8; 32] = [
141141
0xFF, 0xFF, 0xFF, 0xFF, // 0x00–0x1F
142142
0xFE, 0x9B, 0x00, 0xFC, // 0x20–0x3F: (not space) !"#$%&' ()+,/ :;<=>?
143143
0x01, 0x00, 0x00, 0x78, // 0x40–0x5F: @ and [,\,],^

src/checkers.rs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ pub fn is_ipv4(input: &str) -> bool {
115115
/// bit 1 (0x02) – backslash `\`
116116
/// bit 2 (0x04) – dot `.`
117117
/// bit 3 (0x08) – percent `%`
118-
const PATH_SIG_TABLE: [u8; 256] = {
118+
pub(crate) const PATH_SIG_TABLE: [u8; 256] = {
119119
let mut t = [0u8; 256];
120120
// Needs encoding: C0 controls (0x00-0x1F), DEL (0x7F), high bytes (0x80-0xFF)
121121
let mut i = 0usize;
@@ -147,26 +147,33 @@ const PATH_SIG_TABLE: [u8; 256] = {
147147
///
148148
/// Ada C++ uses `for (; i + 7 < size; i += 8)` — we match that exactly.
149149
pub fn path_signature(input: &str) -> u8 {
150-
let b = input.as_bytes();
151-
let mut acc = 0u8;
152-
let mut i = 0;
153-
// 8-at-a-time — Ada C++ uses this exact unroll factor
154-
while i + 8 <= b.len() {
155-
acc |= PATH_SIG_TABLE[b[i] as usize]
156-
| PATH_SIG_TABLE[b[i + 1] as usize]
157-
| PATH_SIG_TABLE[b[i + 2] as usize]
158-
| PATH_SIG_TABLE[b[i + 3] as usize]
159-
| PATH_SIG_TABLE[b[i + 4] as usize]
160-
| PATH_SIG_TABLE[b[i + 5] as usize]
161-
| PATH_SIG_TABLE[b[i + 6] as usize]
162-
| PATH_SIG_TABLE[b[i + 7] as usize];
163-
i += 8;
164-
}
165-
while i < b.len() {
166-
acc |= PATH_SIG_TABLE[b[i] as usize];
167-
i += 1;
150+
#[cfg(feature = "nightly-simd")]
151+
{
152+
return crate::portable_simd_impl::path_signature(input);
153+
}
154+
#[cfg(not(feature = "nightly-simd"))]
155+
{
156+
let b = input.as_bytes();
157+
let mut acc = 0u8;
158+
let mut i = 0;
159+
// 8-at-a-time — Ada C++ uses this exact unroll factor
160+
while i + 8 <= b.len() {
161+
acc |= PATH_SIG_TABLE[b[i] as usize]
162+
| PATH_SIG_TABLE[b[i + 1] as usize]
163+
| PATH_SIG_TABLE[b[i + 2] as usize]
164+
| PATH_SIG_TABLE[b[i + 3] as usize]
165+
| PATH_SIG_TABLE[b[i + 4] as usize]
166+
| PATH_SIG_TABLE[b[i + 5] as usize]
167+
| PATH_SIG_TABLE[b[i + 6] as usize]
168+
| PATH_SIG_TABLE[b[i + 7] as usize];
169+
i += 8;
170+
}
171+
while i < b.len() {
172+
acc |= PATH_SIG_TABLE[b[i] as usize];
173+
i += 1;
174+
}
175+
acc
168176
}
169-
acc
170177
}
171178

172179
/// Check that the domain name length and label lengths are within DNS limits.

src/helpers.rs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,52 @@ use crate::unicode::{
3434
/// U+200D ZERO WIDTH JOINER, whose low byte is 0x0D) are never removed.
3535
#[inline]
3636
pub fn strip_tabs_newlines(s: &str) -> Cow<'_, str> {
37-
// Fast presence check — SIMD-accelerated when the `simd` feature is on.
38-
#[cfg(feature = "simd")]
39-
let has_special = crate::simd::has_tabs_or_newline(s.as_bytes());
40-
#[cfg(not(feature = "simd"))]
41-
let has_special = s.bytes().any(|b| matches!(b, b'\t' | b'\n' | b'\r'));
37+
// Fast presence check.
38+
// nightly-simd: portable SIMD (16 bytes/iter via std::simd)
39+
// otherwise: SWAR — 8 bytes/iter using u64 broadcast/xor, zero overhead
40+
#[cfg(feature = "nightly-simd")]
41+
let has_special = crate::portable_simd_impl::has_tabs_or_newline(s.as_bytes());
42+
#[cfg(not(feature = "nightly-simd"))]
43+
let has_special = {
44+
// SWAR: process 8 bytes at a time
45+
#[inline(always)]
46+
const fn bcast(v: u8) -> u64 {
47+
(v as u64).wrapping_mul(0x0101_0101_0101_0101)
48+
}
49+
#[inline(always)]
50+
const fn has_zero(w: u64) -> u64 {
51+
w.wrapping_sub(0x0101_0101_0101_0101) & !w & 0x8080_8080_8080_8080
52+
}
53+
let b = s.as_bytes();
54+
let (cr, lf, ht) = (bcast(b'\r'), bcast(b'\n'), bcast(b'\t'));
55+
let n = b.len();
56+
let mut running: u64 = 0;
57+
let mut i = 0;
58+
while i + 8 <= n {
59+
let mut w: u64 = 0;
60+
unsafe {
61+
core::ptr::copy_nonoverlapping(b.as_ptr().add(i), &mut w as *mut u64 as *mut u8, 8)
62+
};
63+
running |= has_zero(w ^ cr) | has_zero(w ^ lf) | has_zero(w ^ ht);
64+
i += 8;
65+
}
66+
if running != 0 {
67+
true
68+
} else {
69+
if i < n {
70+
let mut w: u64 = 0;
71+
unsafe {
72+
core::ptr::copy_nonoverlapping(
73+
b.as_ptr().add(i),
74+
&mut w as *mut u64 as *mut u8,
75+
n - i,
76+
)
77+
};
78+
running |= has_zero(w ^ cr) | has_zero(w ^ lf) | has_zero(w ^ ht);
79+
}
80+
running != 0
81+
}
82+
};
4283
if !has_special {
4384
return Cow::Borrowed(s); // zero allocation — common path
4485
}
@@ -111,8 +152,8 @@ pub fn shorten_path(path: &mut String, scheme_type: SchemeType) -> bool {
111152
/// Next authority delimiter for special URLs: `@`, `/`, `\`, `?`
112153
#[inline]
113154
pub fn find_authority_delimiter_special(view: &str) -> usize {
114-
#[cfg(feature = "simd")]
115-
return crate::simd::find_authority_delimiter_special(view);
155+
#[cfg(feature = "nightly-simd")]
156+
return crate::portable_simd_impl::find_authority_delimiter_special(view);
116157
#[allow(unreachable_code)]
117158
view.bytes()
118159
.position(|b| matches!(b, b'@' | b'/' | b'\\' | b'?'))
@@ -122,8 +163,8 @@ pub fn find_authority_delimiter_special(view: &str) -> usize {
122163
/// Next authority delimiter for non-special URLs: `@`, `/`, `?`
123164
#[inline]
124165
pub fn find_authority_delimiter(view: &str) -> usize {
125-
#[cfg(feature = "simd")]
126-
return crate::simd::find_authority_delimiter(view);
166+
#[cfg(feature = "nightly-simd")]
167+
return crate::portable_simd_impl::find_authority_delimiter(view);
127168
#[allow(unreachable_code)]
128169
view.bytes()
129170
.position(|b| matches!(b, b'@' | b'/' | b'?'))
@@ -136,8 +177,8 @@ pub fn find_authority_delimiter(view: &str) -> usize {
136177

137178
#[inline]
138179
fn find_next_host_delimiter_special(view: &str, from: usize) -> usize {
139-
#[cfg(feature = "simd")]
140-
return crate::simd::find_next_host_delimiter_special(view, from);
180+
#[cfg(feature = "nightly-simd")]
181+
return crate::portable_simd_impl::find_next_host_delimiter_special(view, from);
141182
#[allow(unreachable_code)]
142183
view.as_bytes()[from..]
143184
.iter()
@@ -148,8 +189,8 @@ fn find_next_host_delimiter_special(view: &str, from: usize) -> usize {
148189

149190
#[inline]
150191
fn find_next_host_delimiter(view: &str, from: usize) -> usize {
151-
#[cfg(feature = "simd")]
152-
return crate::simd::find_next_host_delimiter(view, from);
192+
#[cfg(feature = "nightly-simd")]
193+
return crate::portable_simd_impl::find_next_host_delimiter(view, from);
153194
#[allow(unreachable_code)]
154195
view.as_bytes()[from..]
155196
.iter()

src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Enable std::simd when the nightly-simd feature is active.
2+
#![cfg_attr(feature = "nightly-simd", feature(portable_simd))]
13
//! # Ada URL
24
//!
35
//! Fast, WHATWG-compliant URL parser written in pure Rust.
@@ -36,10 +38,10 @@ pub(crate) mod idna_impl;
3638
pub(crate) mod idna_norm_tables;
3739
pub(crate) mod idna_tables;
3840
pub(crate) mod parser;
41+
#[cfg(feature = "nightly-simd")]
42+
pub(crate) mod portable_simd_impl;
3943
pub(crate) mod scheme;
4044
pub(crate) mod serializers;
41-
#[cfg(feature = "simd")]
42-
pub(crate) mod simd;
4345
pub(crate) mod unicode;
4446
pub(crate) mod url_search_params;
4547
pub(crate) mod validator;
@@ -1123,7 +1125,7 @@ impl Url {
11231125
/// `input` sets the query to the empty string (not null) — `href` will end
11241126
/// with `?`. CoW: borrows `input` directly when no encoding is needed.
11251127
pub(crate) fn update_base_search_with_encode(&mut self, input: &str, set: &[u8; 32]) {
1126-
// CoW: only allocates when encoding is actually required
1128+
// CoW: borrows when no encoding needed, owns otherwise.
11271129
let enc: Cow<'_, str> = percent_encode(input, set);
11281130
self.write_search_content(&enc);
11291131
}

src/parser.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! WHATWG URL parser state machine.
2-
//! https://url.spec.whatwg.org/#concept-url-parser
2+
//! <https://url.spec.whatwg.org/#concept-url-parser>
33
44
#[cfg(not(feature = "std"))]
55
extern crate alloc;
@@ -888,7 +888,7 @@ pub(crate) fn try_parse_absolute_fast(raw_input: &str) -> Option<Url> {
888888
/// Returns `Some(())` = definitely valid, `None` = fall back to full validator.
889889
#[inline]
890890
pub(crate) fn try_validate_absolute_fast(raw_input: &str) -> Option<()> {
891-
use crate::unicode::{contains_xn_prefix_pub, DOMAIN_CHECK};
891+
use crate::unicode::{DOMAIN_CHECK, contains_xn_prefix_pub};
892892

893893
let raw = raw_input.as_bytes();
894894

0 commit comments

Comments
 (0)