diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36e4982..cd39c2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,9 @@ jobs: - name: Test (release) run: cargo test --release + - name: Test scalar-only (no AVX2 feature) + run: cargo test --release --no-default-features + - name: Test with test-panic feature run: cargo test --features test-panic --release diff --git a/Cargo.toml b/Cargo.toml index d4c5df5..98433a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ name = "quickdecode" crate-type = ["cdylib", "rlib"] [features] +default = ["avx2"] +avx2 = [] test-panic = [] [dependencies] diff --git a/README.md b/README.md index 2646c35..4bd9044 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ Rust-implemented fast JSON decoder exposed to LuaJIT via FFI. Optimized for the common case where a large JSON is parsed once and only a small number of fields are extracted before the document is discarded. -Design document: `docs/superpowers/specs/2026-05-15-rust-quick-json-decode-design.md` (in progress). +Design document: `docs/superpowers/specs/2026-05-15-rust-quick-json-decode-design.md`. ## Status -Currently in design phase. No implementation yet. +Initial implementation complete: scalar + AVX2/PCLMUL structural scanner, root-path and cursor APIs, escape-decoded strings, integer/float/bool/typeof/len, FFI panic barrier, and a LuaJIT wrapper. Rust unit/integration tests and Lua busted tests run in CI. The benchmark harness compares against lua-cjson but tuning is pending — see `Roadmap / Deferred` below. ## Building @@ -68,3 +68,4 @@ Items intentionally pushed out of the first implementation. Each will be picked - **Skip-cache LRU eviction** — only if memory pressure on huge documents proves problematic in practice. - **Path-position info on Phase 1 errors** — currently only an opaque `QJD_PARSE_ERROR`. - **AVX2 tail-bypass optimization** — current implementation falls back to whole-buffer scalar when a tail exists; could be optimized by emitting tail structural offsets directly. +- **Large bench fixtures** — spec §9.3 lists `large_dump.json` (~20 MB) and `deep_nest.json` (depth stress test); not yet committed. Only `small_api.json` and `medium_resp.json` ship today. diff --git a/docs/superpowers/specs/2026-05-15-rust-quick-json-decode-design.md b/docs/superpowers/specs/2026-05-15-rust-quick-json-decode-design.md index 404aafc..196e9f1 100644 --- a/docs/superpowers/specs/2026-05-15-rust-quick-json-decode-design.md +++ b/docs/superpowers/specs/2026-05-15-rust-quick-json-decode-design.md @@ -62,16 +62,18 @@ It does so by performing a **single fast SIMD structural scan** in Phase 1 (only src/ ├── lib.rs — crate root, re-exports ├── ffi.rs — pub extern "C" symbols (C ABI layer) -├── doc.rs — Document & Cursor (internal Rust API) +├── doc.rs — Document type (Phase 1 + container helpers) +├── cursor.rs — Cursor, path resolution, skip-cache walk +├── path.rs — path string parse (zero-alloc iterator) +├── error.rs — error / type enums ├── scan/ -│ ├── mod.rs — StructScanner trait, dispatch +│ ├── mod.rs — Scanner trait + runtime dispatch (OnceCell-cached) │ ├── scalar.rs — scalar fallback -│ ├── avx2.rs — x86_64 AVX2 + PCLMUL -│ └── runtime_dispatch.rs +│ └── avx2.rs — x86_64 AVX2 + PCLMUL (gated by `avx2` feature) ├── decode/ +│ ├── mod.rs │ ├── number.rs — lazy i64/f64 parse -│ ├── string.rs — lazy escape decode + UTF-8 check on \u -│ └── path.rs — path string parse (zero-alloc iterator) +│ └── string.rs — lazy escape decode + UTF-8 check on \u └── skip_cache.rs — Phase 2 sibling-skip cache lua/ @@ -144,8 +146,8 @@ typedef struct { const qjd_doc* doc; uint32_t idx_start; /* opener position in doc.indices */ uint32_t idx_end; /* one past closer */ - uint32_t cache_slot; /* skip-cache slot; 0 if not populated */ - uint32_t _pad; + uint32_t _reserved0; /* reserved for future fast-path */ + uint32_t _reserved1; /* reserved / padding */ } qjd_cursor; /* 24 bytes, by-value, no allocation */ ``` @@ -337,11 +339,11 @@ pub(crate) struct Cursor<'d> { /// idx_start points at '{' or '['; idx_end points one past matching '}' / ']'. idx_start: u32, idx_end: u32, - /// Skip-cache slot for this range (0 = not yet built). - cache_slot: u32, } ``` +The published `qjd_cursor` carries two `_reservedN` slots beyond `idx_start`/`idx_end`; they are unused in v1 but reserved so a future per-cursor skip-cache fast-path can be added without breaking the ABI. + `Cursor` is `Copy` and never allocates. `open()`, `field()`, `index()` return new cursors by value. ### 6.2 Resolution Algorithm @@ -375,8 +377,9 @@ pub(crate) struct SkipSlot { /// (for object: pointing at the key's opening '"'; /// for array: pointing at the value's first token). child_starts: Vec, - /// Position of the closing '}' / ']' in doc.indices. - closer_idx: u32, + /// child_ends[i] = idx_end for a Cursor pointing at the i-th child's value. + /// Storing this lets cache-hit lookups skip the brace-counting walk. + child_ends: Vec, } ``` diff --git a/include/lua_quick_decode.h b/include/lua_quick_decode.h index fb2e34f..0c0c0a0 100644 --- a/include/lua_quick_decode.h +++ b/include/lua_quick_decode.h @@ -31,8 +31,8 @@ typedef struct { const qjd_doc* doc; uint32_t idx_start; uint32_t idx_end; - uint32_t cache_slot; - uint32_t _pad; + uint32_t _reserved0; + uint32_t _reserved1; } qjd_cursor; const char* qjd_strerror(int code); diff --git a/lua/quickdecode.lua b/lua/quickdecode.lua index 4822437..9675231 100644 --- a/lua/quickdecode.lua +++ b/lua/quickdecode.lua @@ -4,7 +4,7 @@ ffi.cdef[[ typedef struct qjd_doc qjd_doc; typedef struct { const qjd_doc* doc; - uint32_t idx_start, idx_end, cache_slot, _pad; + uint32_t idx_start, idx_end, _reserved0, _reserved1; } qjd_cursor; const char* qjd_strerror(int code); @@ -161,24 +161,21 @@ function Cursor:len(path) end function Cursor:open(path) - local out = ffi.new("qjd_cursor[1]") - local rc = C.qjd_cursor_open(self._cur, path, #path, out) + local rc = C.qjd_cursor_open(self._cur, path, #path, cur_box) if not check_err(rc) then return nil end - return setmetatable({ _cur = out[0], _doc = self._doc }, Cursor) + return setmetatable({ _cur = cur_box[0], _doc = self._doc }, Cursor) end function Cursor:field(key) - local out = ffi.new("qjd_cursor[1]") - local rc = C.qjd_cursor_field(self._cur, key, #key, out) + local rc = C.qjd_cursor_field(self._cur, key, #key, cur_box) if not check_err(rc) then return nil end - return setmetatable({ _cur = out[0], _doc = self._doc }, Cursor) + return setmetatable({ _cur = cur_box[0], _doc = self._doc }, Cursor) end function Cursor:index(i) - local out = ffi.new("qjd_cursor[1]") - local rc = C.qjd_cursor_index(self._cur, i, out) + local rc = C.qjd_cursor_index(self._cur, i, cur_box) if not check_err(rc) then return nil end - return setmetatable({ _cur = out[0], _doc = self._doc }, Cursor) + return setmetatable({ _cur = cur_box[0], _doc = self._doc }, Cursor) end return _M diff --git a/src/cursor.rs b/src/cursor.rs index 2b35b3c..cc21556 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -61,14 +61,17 @@ fn walk_children(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result = Vec::new(); + let mut ends: Vec = Vec::new(); let mut i = cur.idx_start + 1; let end = cur.idx_end; let mut arr_idx: u32 = 0; @@ -79,6 +82,7 @@ fn walk_children(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result Result Ok(c), @@ -117,9 +123,9 @@ fn walk_children(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result Result { - for (k, &i) in starts.iter().enumerate() { + for (k, (&i, &cursor_end)) in starts.iter().zip(ends.iter()).enumerate() { let matched = if is_obj { let key_open = doc.indices[i as usize] as usize; let key_close = doc.indices[(i + 1) as usize] as usize; @@ -130,7 +136,6 @@ fn resolve_in_known_children( }; if matched { let value_idx_start = if is_obj { i + 3 } else { i }; - let (cursor_end, _) = find_value_span(doc, value_idx_start)?; return Ok(Cursor { idx_start: value_idx_start, idx_end: cursor_end }); } } diff --git a/src/doc.rs b/src/doc.rs index f5f34c1..7c6ecda 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -3,11 +3,10 @@ use std::cell::RefCell; use crate::error::qjd_err; use crate::skip_cache::SkipCache; -#[allow(dead_code)] pub struct Document<'a> { pub(crate) buf: &'a [u8], pub(crate) indices: Vec, - pub(crate) scratch: Vec, + pub(crate) scratch: RefCell>, pub(crate) skip: RefCell, } @@ -20,8 +19,8 @@ impl<'a> Document<'a> { Ok(Self { buf, indices, - scratch: Vec::new(), - skip: RefCell::new(SkipCache::new()), + scratch: RefCell::new(Vec::new()), + skip: RefCell::new(SkipCache::new()), }) } } diff --git a/src/ffi.rs b/src/ffi.rs index f04b0f5..75df2a5 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -20,7 +20,6 @@ macro_rules! ffi_catch { } /// Opaque type exported to C as `qjd_doc*`. -#[allow(dead_code)] pub struct qjd_doc(pub(crate) Document<'static>); #[no_mangle] @@ -169,12 +168,8 @@ pub unsafe extern "C" fn qjd_get_str( // String ends at the close quote, whose indices position is idx_start + 1. let close = d.indices[(cur.idx_start + 1) as usize] as usize; - // SAFETY: scratch is owned by the qjd_doc; we obtain a mutable reference - // to it through the raw *mut qjd_doc pointer (not through the shared &Document - // alias `d`). Lua-side callers consume the returned ptr before any further - // FFI calls. Single-threaded use enforced by C ABI contract. - let scratch = &mut (*doc).0.scratch; - match string::decode_string(d.buf, pos + 1, close, scratch) { + let mut scratch = d.scratch.borrow_mut(); + match string::decode_string(d.buf, pos + 1, close, &mut scratch) { Ok((p, n)) => { *out_ptr = p; *out_len = n; qjd_err::QJD_OK as c_int } Err(e) => e as c_int, } @@ -262,8 +257,8 @@ pub struct qjd_cursor { pub doc: *const qjd_doc, pub idx_start: u32, pub idx_end: u32, - pub cache_slot: u32, - pub _pad: u32, + pub _reserved0: u32, + pub _reserved1: u32, } /// Turn a `*const qjd_cursor` into `(&'static Document<'static>, Cursor)` for Rust use. @@ -278,10 +273,10 @@ unsafe fn cursor_to_internal(c: *const qjd_cursor) -> Result<(&'static Document< fn internal_to_cursor(doc: *const qjd_doc, cur: Cursor) -> qjd_cursor { qjd_cursor { doc, - idx_start: cur.idx_start, - idx_end: cur.idx_end, - cache_slot: 0, - _pad: 0, + idx_start: cur.idx_start, + idx_end: cur.idx_end, + _reserved0: 0, + _reserved1: 0, } } @@ -372,10 +367,8 @@ pub unsafe extern "C" fn qjd_cursor_get_str( } let close = d.indices[(cur.idx_start + 1) as usize] as usize; - // Access scratch via raw pointer through doc to avoid aliasing the &Document. - let doc_ptr = (*c).doc as *mut qjd_doc; - let scratch = &mut (*doc_ptr).0.scratch; - match string::decode_string(d.buf, pos + 1, close, scratch) { + let mut scratch = d.scratch.borrow_mut(); + match string::decode_string(d.buf, pos + 1, close, &mut scratch) { Ok((p, n)) => { *out_ptr = p; *out_len = n; qjd_err::QJD_OK as c_int } Err(e) => e as c_int, } diff --git a/src/lib.rs b/src/lib.rs index a39d637..43a07ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,6 @@ pub mod ffi; #[doc(hidden)] pub mod __test_api { pub use crate::scan::{Scanner, ScalarScanner}; - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", feature = "avx2"))] pub use crate::scan::avx2::Avx2Scanner; } diff --git a/src/scan/avx2.rs b/src/scan/avx2.rs index b5bf5a2..2467d08 100644 --- a/src/scan/avx2.rs +++ b/src/scan/avx2.rs @@ -170,6 +170,10 @@ mod tests { use super::*; use crate::scan::{Scanner, scalar::ScalarScanner}; + fn host_supports_avx2() -> bool { + std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("pclmulqdq") + } + fn parity(input: &[u8]) { let mut a = Vec::new(); let mut b = Vec::new(); @@ -180,6 +184,7 @@ mod tests { #[test] fn no_strings_matches_scalar() { + if !host_supports_avx2() { return; } parity(b"{}"); parity(b"[]"); parity(b"[{}]"); @@ -190,6 +195,7 @@ mod tests { #[test] fn within_chunk_strings_match_scalar() { + if !host_supports_avx2() { return; } // These are <64 bytes so they go through the scalar tail path only; // they still verify Avx2Scanner does not corrupt the output for these // inputs, but they do NOT exercise the AVX2 string handling. @@ -203,6 +209,7 @@ mod tests { /// within a single 64-byte chunk. #[test] fn chunked_path_with_string() { + if !host_supports_avx2() { return; } // Build a 64-byte input where bytes 0..64 are a single AVX2 chunk // containing a string, and there is no tail. // Layout: `{"k":"<48 a's>"}` = 1 + 4 + 1 + 48 + 1 + 1 = 56 bytes. Need 64. @@ -219,6 +226,7 @@ mod tests { /// String with internal escapes inside a 64-byte chunk. #[test] fn chunked_path_with_escapes() { + if !host_supports_avx2() { return; } // Bytes: {"k":"aa\"bb\\cc"} // Need exactly 64 bytes. Build it carefully. let mut buf = Vec::with_capacity(64); @@ -235,6 +243,7 @@ mod tests { /// for multiple strings in a single 64-byte chunk. #[test] fn pclmul_inside_string_correct() { + if !host_supports_avx2() { return; } // {"a":"foo","b":"bar"} // Strings "foo" and "bar" both fully within the chunk. let mut buf = Vec::with_capacity(64); diff --git a/src/scan/mod.rs b/src/scan/mod.rs index 041b700..910e539 100644 --- a/src/scan/mod.rs +++ b/src/scan/mod.rs @@ -1,5 +1,5 @@ pub(crate) mod scalar; -#[cfg(target_arch = "x86_64")] +#[cfg(all(target_arch = "x86_64", feature = "avx2"))] pub(crate) mod avx2; use once_cell::sync::OnceCell; @@ -21,7 +21,7 @@ static SCAN_FN: OnceCell = OnceCell::new(); pub(crate) fn scan(buf: &[u8], out: &mut Vec) -> Result<(), usize> { let f = *SCAN_FN.get_or_init(|| { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", feature = "avx2"))] { if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("pclmulqdq") diff --git a/src/skip_cache.rs b/src/skip_cache.rs index 5a38a81..6b6b1dd 100644 --- a/src/skip_cache.rs +++ b/src/skip_cache.rs @@ -13,12 +13,16 @@ pub(crate) struct SkipSlot { /// marker. For object children this is the key's opening '"'; for array /// children, the value's first marker. pub(crate) child_starts: Vec, + /// child_ends[i] = the `cursor_end` value for the i-th child (i.e. the + /// idx_end to put in a Cursor pointing at that child's value). Storing + /// this lets cache-hit resolution skip the brace-counting find_value_span. + pub(crate) child_ends: Vec, } impl SkipCache { pub(crate) fn new() -> Self { Self { - slots: vec![SkipSlot { child_starts: Vec::new() }], + slots: vec![SkipSlot { child_starts: Vec::new(), child_ends: Vec::new() }], by_opener: FxHashMap::default(), } } @@ -30,7 +34,7 @@ impl SkipCache { return (slot, true); } let new = self.slots.len() as u32; - self.slots.push(SkipSlot { child_starts: Vec::new() }); + self.slots.push(SkipSlot { child_starts: Vec::new(), child_ends: Vec::new() }); self.by_opener.insert(opener_idx, new); (new, false) } @@ -43,5 +47,6 @@ impl SkipCache { &self.slots[n as usize] } + #[cfg(test)] pub(crate) fn len(&self) -> usize { self.by_opener.len() } } diff --git a/tests/ffi_numbers.rs b/tests/ffi_numbers.rs index 1065974..c1f4de8 100644 --- a/tests/ffi_numbers.rs +++ b/tests/ffi_numbers.rs @@ -61,3 +61,64 @@ fn get_bool() { assert_eq!(v, 0); unsafe { qjd_free(d) }; } + +#[test] +fn get_i64_max_and_min() { + let json = format!("{{\"hi\":{},\"lo\":{}}}", i64::MAX, i64::MIN); + let d = parse(json.as_bytes()); + let mut v: i64 = 0; + let p = b"hi"; + let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); + assert_eq!(v, i64::MAX); + let p = b"lo"; + let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); + assert_eq!(v, i64::MIN); + unsafe { qjd_free(d) }; +} + +#[test] +fn get_i64_just_over_max_overflows() { + // 9223372036854775808 = i64::MAX + 1 + let d = parse(b"{\"a\":9223372036854775808}"); + let mut v: i64 = 0; + let p = b"a"; + let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 4); // OUT_OF_RANGE + unsafe { qjd_free(d) }; +} + +#[test] +fn get_f64_large_magnitude() { + let d = parse(b"{\"a\":1.7e308}"); + let mut v: f64 = 0.0; + let p = b"a"; + let rc = unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); + assert!(v > 1.0e308 && v < f64::INFINITY); + unsafe { qjd_free(d) }; +} + +#[test] +fn get_f64_negative_zero_and_exponent() { + let d = parse(b"{\"a\":-0.0,\"b\":1e-300}"); + let mut v: f64 = 1.0; + let p = b"a"; + unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(v, 0.0); + let p = b"b"; + unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert!(v > 0.0 && v < 1e-200); + unsafe { qjd_free(d) }; +} + +#[test] +fn get_i64_rejects_float_form() { + let d = parse(b"{\"a\":1.5}"); + let mut v: i64 = 0; + let p = b"a"; + let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_ne!(rc, 0); // any error code is acceptable; not a valid i64 + unsafe { qjd_free(d) }; +} diff --git a/tests/ffi_wide_object.rs b/tests/ffi_wide_object.rs new file mode 100644 index 0000000..0dbbfc5 --- /dev/null +++ b/tests/ffi_wide_object.rs @@ -0,0 +1,54 @@ +//! Wide-object skip-cache test (spec §9.2): 5K keys, repeatedly access random +//! keys via the same cursor and confirm correctness. + +use std::os::raw::c_int; +use quickdecode::ffi::*; + +fn build_wide(n: usize) -> (String, Vec) { + let mut s = String::from("{"); + let mut keys = Vec::with_capacity(n); + for i in 0..n { + if i > 0 { s.push(','); } + let k = format!("k{:05}", i); + s.push('"'); s.push_str(&k); s.push_str("\":"); + s.push_str(&format!("{}", i * 2)); + keys.push(k); + } + s.push('}'); + (s, keys) +} + +#[test] +fn wide_object_5k_keys_all_resolvable() { + let n = 5000; + let (json, keys) = build_wide(n); + let mut err: c_int = -1; + let d = unsafe { qjd_parse(json.as_ptr(), json.len(), &mut err) }; + assert!(!d.is_null()); + + // Hit a sparse sample, in non-sequential order, twice (second pass exercises + // the cache-hit path). + let samples: Vec = (0..n).step_by(173).collect(); + for &i in &samples { + let mut v: i64 = -1; + let k = keys[i].as_bytes(); + let rc = unsafe { qjd_get_i64(d, k.as_ptr() as *const i8, k.len(), &mut v) }; + assert_eq!(rc, 0, "miss on first pass for key {}", keys[i]); + assert_eq!(v as usize, i * 2); + } + for &i in samples.iter().rev() { + let mut v: i64 = -1; + let k = keys[i].as_bytes(); + let rc = unsafe { qjd_get_i64(d, k.as_ptr() as *const i8, k.len(), &mut v) }; + assert_eq!(rc, 0, "miss on cache-hit pass for key {}", keys[i]); + assert_eq!(v as usize, i * 2); + } + + // Unknown key still returns NOT_FOUND after the cache is populated. + let bogus = b"definitely_not_a_key"; + let mut v: i64 = 0; + let rc = unsafe { qjd_get_i64(d, bogus.as_ptr() as *const i8, bogus.len(), &mut v) }; + assert_eq!(rc, 2); // QJD_NOT_FOUND + + unsafe { qjd_free(d) }; +} diff --git a/tests/lua/gc_spec.lua b/tests/lua/gc_spec.lua new file mode 100644 index 0000000..ddc0938 --- /dev/null +++ b/tests/lua/gc_spec.lua @@ -0,0 +1,30 @@ +local qd = require("quickdecode") + +describe("quickdecode GC", function() + it("collects Doc without crashing and frees underlying qjd_doc", function() + -- Create and drop many Docs to exercise the ffi.gc finalizer path. + -- A leak or double-free would surface as either crash, memory growth, + -- or use-after-free under valgrind. Here we just confirm the loop + -- completes and that values remain correct mid-loop. + for i = 1, 200 do + local d = qd.parse(string.format('{"i":%d}', i)) + assert.are.equal(i, d:get_i64("i")) + d = nil -- drop reference + end + collectgarbage("collect") + collectgarbage("collect") + end) + + it("Doc finalizer runs after collectgarbage", function() + -- Use a weak table to confirm the Doc is reachable for collection. + local refs = setmetatable({}, { __mode = "v" }) + do + local d = qd.parse('{"a":1}') + refs[1] = d + assert.are.equal(1, d:get_i64("a")) + end + collectgarbage("collect") + collectgarbage("collect") + assert.is_nil(refs[1]) + end) +end) diff --git a/tests/scanner_crosscheck.rs b/tests/scanner_crosscheck.rs index 476ff88..66ee338 100644 --- a/tests/scanner_crosscheck.rs +++ b/tests/scanner_crosscheck.rs @@ -1,9 +1,10 @@ +#[cfg(all(target_arch = "x86_64", feature = "avx2"))] use proptest::prelude::*; -#[cfg(target_arch = "x86_64")] +#[cfg(all(target_arch = "x86_64", feature = "avx2"))] use quickdecode::__test_api::{Scanner, ScalarScanner, Avx2Scanner}; -#[cfg(target_arch = "x86_64")] +#[cfg(all(target_arch = "x86_64", feature = "avx2"))] proptest! { #![proptest_config(ProptestConfig::with_cases(2000))] @@ -26,7 +27,7 @@ proptest! { } } -#[cfg(target_arch = "x86_64")] +#[cfg(all(target_arch = "x86_64", feature = "avx2"))] fn valid_jsonish() -> impl Strategy { proptest::collection::vec( prop_oneof![ @@ -50,5 +51,5 @@ fn valid_jsonish() -> impl Strategy { ).prop_map(|v| v.concat()) } -#[cfg(not(target_arch = "x86_64"))] +#[cfg(not(all(target_arch = "x86_64", feature = "avx2")))] #[test] fn skip() {}