From 09223b1ed0d2c67a04d20b630f55b2f10d599352 Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Fri, 15 May 2026 17:03:57 +0000 Subject: [PATCH 1/2] lint: add # Safety docs to FFI exports, clean up clippy warnings Make `make lint` (cargo clippy -D warnings, all-targets) green: - Add `# Safety` sections to all 20 public `qjd_*` extern "C" functions plus the test-panic export. A module-level shared-contract block in ffi.rs documents the common obligations (live doc, valid path slice, scratch-buffer lifetime, panic barrier) so per-function docs stay short. - Annotate the two `std::mem::transmute` calls with explicit types. - Collapse bracket-mismatch arms in validate_brackets into match guards. - Stop approximating PI in two tests; switch to arbitrary decimals. - Use Vec::resize instead of per-byte push loops in scanner test fixtures. - Make path::tests::parse's elided lifetime explicit. Drop `cargo fmt --check` from `make lint`: the codebase uses manual column alignment in struct definitions and compact single-line literals that default rustfmt would reflow. Recorded as a deferred item rather than reformatted, since style was an intentional choice. README "Roadmap / Deferred" updated: - Remove the safety-docs entry (done). - Refine AVX-512 estimate to 1.5-1.8x on the bench (L3-bandwidth bound per profile session, not a clean 2x). - Add `validate_brackets` fusion as a new deferred item: profiling showed it's 65% of parse time on structurally-dense JSON (0.3% on the current string-heavy bench), so it's a win for config/JSONL workloads but not for the current bench scenario. --- CLAUDE.md | 2 +- Makefile | 3 +- README.md | 5 +- src/decode/number.rs | 2 +- src/ffi.rs | 209 ++++++++++++++++++++++++++++++++++++++++++- src/path.rs | 2 +- src/scan/avx2.rs | 10 +-- src/scan/mod.rs | 12 +-- src/scan/scalar.rs | 9 +- tests/ffi_numbers.rs | 4 +- 10 files changed, 224 insertions(+), 34 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0fc229a..d5cffa5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ cargo test --features test-panic --release `ffi.load("quickdecode")` uses `dlopen`, which respects `LD_LIBRARY_PATH` — **not** LuaJIT's `package.cpath`. The Makefile sets `LD_LIBRARY_PATH=target/release` for `test`/`bench`; if you invoke `busted` or `luajit` directly, set it yourself. -`make lint` currently fails on 22 `missing_safety_doc` clippy warnings on the public `qjd_*` FFI exports. This is a known deferred item (see README "Roadmap / Deferred"); don't treat the lint failure as a regression unless the count changes. +`make lint` runs clippy only (with `-D warnings`); `cargo fmt --check` is intentionally **not** part of the lint gate because the codebase uses manual column alignment in struct definitions and compact single-line literals that default rustfmt would reflow. See the README "Roadmap / Deferred" entry on fmt for context. ## Architecture diff --git a/Makefile b/Makefile index 417a09b..4c37560 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,8 @@ test: build ## Run cargo tests + busted Lua tests cargo test --release $(LUA_ENV) busted --lua=$(LUAJIT) tests/lua --lpath='./lua/?.lua' -lint: ## Run clippy (deny warnings) and rustfmt --check +lint: ## Run clippy with -D warnings cargo clippy --release --all-targets -- -D warnings - cargo fmt --check bench: build ## Run the LuaJIT vs cjson benchmark $(LUA_ENV) $(LUAJIT) benches/lua_bench.lua diff --git a/README.md b/README.md index 82309b5..3e785e7 100644 --- a/README.md +++ b/README.md @@ -72,5 +72,6 @@ Items intentionally pushed out of the first implementation. Each will be picked - **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. - **`structural_mask_chunk` via shuffle-based set check** — the current AVX2 scanner does 7 `_mm256_cmpeq_epi8` + `_mm256_movemask_epi8` per chunk half (one per structural char in `{}[]:,"`). A single `_mm256_shuffle_epi8` against a 16-byte LUT plus one cmpeq can do the same set membership in 2-3 ops per half. Estimated 15-25% scanner speedup on dense-structural workloads. Not on the hot path for string-heavy payloads (those already short-circuit via the fast path). - **Adaptive `out.reserve` in scanners** — `out.reserve(buf.len() / 6)` is calibrated for object-heavy JSON. On string-heavy multimodal payloads (one big content array, mostly base64) the actual emit rate is <1 structural per 1 KB, so we over-reserve by 100x+. Mainly a memory hygiene concern (mmap'd pages stay lazily faulted), <5% throughput effect. -- **AVX-512 scanner backend** — 64-byte → 128-byte chunks doubles scan throughput on CPUs with `avx512bw` + `vpclmulqdq` (Sapphire Rapids, Zen 4+). Estimated 2× on supporting silicon; no effect elsewhere. -- **`# Safety` docs on unsafe FFI exports** — `make lint` currently fails on 22 `missing_safety_doc` clippy warnings from the public `qjd_*` C-ABI functions. Tracked separately so the Makefile can ship with `-D warnings` already wired up. +- **AVX-512 scanner backend** — 64-byte → 128-byte chunks. On the 1 MB string-heavy bench, profile shows scan throughput is L3-bandwidth-bound, so realistic win is ~1.5–1.8×, not a clean 2×; larger wins need fixtures that fit in L1/L2. Needs `avx512bw` + `vpclmulqdq` (Sapphire Rapids, Zen 4+). +- **`cargo fmt --check` not enforced** — `make lint` runs clippy only. The codebase uses intentional manual column alignment in struct definitions and compact single-line literals that default rustfmt would reflow. Skip rather than reformat until a project-wide style decision is made. +- **`validate_brackets` fusion into scan emit loop** — surfaced by profiling: on structurally-dense workloads `validate_brackets` is 65% of parse time (second linear pass over emitted indices). Folding bracket pairing into the scan emit loop via an inline depth stack eliminates that pass. No effect on the current string-heavy bench (0.3% there); a win for config / JSONL / table-shape JSON. diff --git a/src/decode/number.rs b/src/decode/number.rs index a81df70..62eca03 100644 --- a/src/decode/number.rs +++ b/src/decode/number.rs @@ -67,7 +67,7 @@ mod tests { } #[test] fn f64_zero() { assert_eq!(parse_f64(b"0.0").unwrap(), 0.0); } - #[test] fn f64_pi() { assert!((parse_f64(b"3.14").unwrap() - 3.14).abs() < 1e-12); } + #[test] fn f64_decimal() { assert!((parse_f64(b"2.5").unwrap() - 2.5).abs() < 1e-12); } #[test] fn f64_negative(){ assert_eq!(parse_f64(b"-1.5").unwrap(), -1.5); } #[test] fn f64_exponent(){ assert_eq!(parse_f64(b"1e2").unwrap(), 100.0); } diff --git a/src/ffi.rs b/src/ffi.rs index 75df2a5..09d4094 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,5 +1,27 @@ //! C ABI surface. Every public function is `unsafe extern "C"`. //! All public symbols use the `qjd_` prefix. +//! +//! # Shared safety contract +//! +//! Most exports share the same FFI obligations on the caller: +//! +//! - A `*mut qjd_doc` argument must be NULL or a pointer previously returned +//! by [`qjd_parse`] that has not yet been passed to [`qjd_free`]. +//! - The input buffer passed to [`qjd_parse`] must remain valid and +//! unmodified for as long as the document is alive; the document borrows it. +//! - Path / key pointer arguments must point to the indicated number of +//! readable bytes, or be NULL when the length is `0`. +//! - Out pointers must be non-NULL and writable for their target type when +//! the function returns `QJD_OK`. Functions return `QJD_INVALID_ARG` +//! instead of writing through a NULL out pointer. +//! - A `*const qjd_cursor` must point to a cursor produced by one of +//! [`qjd_open`], [`qjd_cursor_open`], [`qjd_cursor_field`], or +//! [`qjd_cursor_index`], and whose `doc` field is still alive. +//! - A pointer/length pair returned by any `*_get_str` is invalidated by +//! the next `*_get_str` call on the same document (scratch buffer reuse). +//! +//! Every export catches Rust panics at the FFI boundary and converts them +//! to `QJD_OOM`; a panic must not be allowed to unwind across the boundary. #![allow(non_camel_case_types)] @@ -22,6 +44,13 @@ macro_rules! ffi_catch { /// Opaque type exported to C as `qjd_doc*`. pub struct qjd_doc(pub(crate) Document<'static>); +/// Return a static NUL-terminated message for the given error code. +/// +/// # Safety +/// +/// Has no preconditions. Marked `unsafe extern "C"` for C-ABI consistency +/// with the rest of the surface. The returned pointer is to static storage +/// and must not be freed. #[no_mangle] pub unsafe extern "C" fn qjd_strerror(code: c_int) -> *const c_char { // Hardcoded NUL-terminated map; avoids runtime allocation and lifetime issues. @@ -40,6 +69,18 @@ pub unsafe extern "C" fn qjd_strerror(code: c_int) -> *const c_char { s.as_ptr() as *const c_char } +/// Parse a JSON buffer into a document (Phase 1: structural scan). +/// +/// # Safety +/// +/// - `buf` must point to `len` readable bytes, or be NULL (in which case the +/// function returns NULL with `*err_out = QJD_INVALID_ARG`). +/// - `err_out` must point to a writable `int`, or be NULL (in which case the +/// function returns NULL with no error code written). +/// - The buffer must remain valid and unmodified for the lifetime of the +/// returned `qjd_doc*`; the document borrows it. +/// - On success, the returned pointer must be freed exactly once with +/// [`qjd_free`]. #[no_mangle] pub unsafe extern "C" fn qjd_parse( buf: *const u8, @@ -72,6 +113,13 @@ pub unsafe extern "C" fn qjd_parse( } } +/// Free a document returned by [`qjd_parse`]. NULL is a no-op. +/// +/// # Safety +/// +/// `doc` must be NULL or a pointer previously returned by [`qjd_parse`] +/// that has not yet been freed. Double-free or passing a pointer not +/// produced by `qjd_parse` is undefined behavior. #[no_mangle] pub unsafe extern "C" fn qjd_free(doc: *mut qjd_doc) { if doc.is_null() { return; } @@ -94,9 +142,16 @@ unsafe fn resolve_root_path( std::slice::from_raw_parts(path as *const u8, path_len) }; let cur = Cursor::root(d).resolve(d, p)?; - Ok((std::mem::transmute(d), cur)) + Ok((std::mem::transmute::<&Document<'_>, &'static Document<'static>>(d), cur)) } +/// Write the JSON value type at `path` into `*type_out` (see [`qjd_type`]). +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `doc` must be live or NULL; `path` must point to `path_len` bytes or be +/// NULL with `path_len == 0`; `type_out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_typeof( doc: *mut qjd_doc, path: *const c_char, path_len: usize, type_out: *mut c_int, @@ -113,6 +168,13 @@ pub unsafe extern "C" fn qjd_typeof( }) } +/// Write `1` into `*out` if the value at `path` is JSON `null`, else `0`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `doc` must be live or NULL; `path` must point to `path_len` bytes or be +/// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_is_null( doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut c_int, @@ -130,6 +192,14 @@ pub unsafe extern "C" fn qjd_is_null( }) } +/// Write the number of direct children of the container at `path` into `*out`. +/// Returns `QJD_TYPE_MISMATCH` if the target is not an array or object. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `doc` must be live or NULL; `path` must point to `path_len` bytes or be +/// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_len( doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut usize, @@ -149,6 +219,20 @@ pub unsafe extern "C" fn qjd_len( use crate::decode::number; use crate::decode::string; +/// Decode the JSON string at `path` and write `(ptr, len)` into the outputs. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `doc` must be live or NULL; `path` must point to `path_len` bytes or be +/// NULL with `path_len == 0`; `out_ptr` and `out_len` must be non-NULL and +/// writable. +/// +/// **The returned `(*out_ptr, *out_len)` pair is invalidated by the next +/// `qjd_get_str` / `qjd_cursor_get_str` call on the same document**: the +/// scratch buffer used for escape decoding is reused. Callers must consume +/// the result (e.g. `ffi.string(p, n)` in LuaJIT) before issuing another +/// string read on the same document. #[no_mangle] pub unsafe extern "C" fn qjd_get_str( doc: *mut qjd_doc, path: *const c_char, path_len: usize, @@ -176,6 +260,14 @@ pub unsafe extern "C" fn qjd_get_str( }) } +/// Parse the JSON number at `path` as `i64` and write into `*out`. +/// Returns `QJD_OUT_OF_RANGE` if the value does not fit in `i64`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `doc` must be live or NULL; `path` must point to `path_len` bytes or be +/// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_get_i64( doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut i64, @@ -195,6 +287,13 @@ pub unsafe extern "C" fn qjd_get_i64( }) } +/// Parse the JSON number at `path` as `f64` and write into `*out`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `doc` must be live or NULL; `path` must point to `path_len` bytes or be +/// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_get_f64( doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut f64, @@ -214,6 +313,14 @@ pub unsafe extern "C" fn qjd_get_f64( }) } +/// Write `1` / `0` into `*out` for JSON `true` / `false` at `path`. +/// Returns `QJD_TYPE_MISMATCH` for any other value. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `doc` must be live or NULL; `path` must point to `path_len` bytes or be +/// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_get_bool( doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut c_int, @@ -267,7 +374,10 @@ unsafe fn cursor_to_internal(c: *const qjd_cursor) -> Result<(&'static Document< let cc = &*c; if cc.doc.is_null() { return Err(qjd_err::QJD_INVALID_ARG); } let d: &Document = &(*(cc.doc as *mut qjd_doc)).0; - Ok((std::mem::transmute(d), Cursor { idx_start: cc.idx_start, idx_end: cc.idx_end })) + Ok(( + std::mem::transmute::<&Document<'_>, &'static Document<'static>>(d), + Cursor { idx_start: cc.idx_start, idx_end: cc.idx_end }, + )) } fn internal_to_cursor(doc: *const qjd_doc, cur: Cursor) -> qjd_cursor { @@ -280,6 +390,14 @@ fn internal_to_cursor(doc: *const qjd_doc, cur: Cursor) -> qjd_cursor { } } +/// Resolve `path` against the root of `doc` and write the resulting cursor +/// into `*out`. The cursor borrows the document — do not use after free. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `doc` must be live or NULL; `path` must point to `path_len` bytes or be +/// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_open( doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut qjd_cursor, @@ -296,6 +414,15 @@ pub unsafe extern "C" fn qjd_open( }) } +/// Resolve `path` from the position `*c` and write the resulting cursor +/// into `*out`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `path` must point to `path_len` bytes or be NULL +/// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_open( c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut qjd_cursor, @@ -313,6 +440,15 @@ pub unsafe extern "C" fn qjd_cursor_open( }) } +/// Step into the object field named `key` and write the child cursor. +/// Lets the caller bypass path-string parsing for keys that contain `.` or `[`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `key` must point to `key_len` bytes or be NULL +/// with `key_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_field( c: *const qjd_cursor, key: *const c_char, key_len: usize, out: *mut qjd_cursor, @@ -331,6 +467,13 @@ pub unsafe extern "C" fn qjd_cursor_field( }) } +/// Step into array index `i` and write the child cursor. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_index( c: *const qjd_cursor, i: usize, out: *mut qjd_cursor, @@ -347,6 +490,19 @@ pub unsafe extern "C" fn qjd_cursor_index( }) } +/// Decode the JSON string at `path` (relative to `*c`) and write `(ptr, len)`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `path` must point to `path_len` bytes or be NULL +/// with `path_len == 0`; `out_ptr` and `out_len` must be non-NULL and +/// writable. +/// +/// **The returned `(*out_ptr, *out_len)` pair is invalidated by the next +/// `qjd_get_str` / `qjd_cursor_get_str` call on the same document** (scratch +/// buffer reuse). See [`qjd_get_str`]. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_get_str( c: *const qjd_cursor, path: *const c_char, path_len: usize, @@ -375,6 +531,15 @@ pub unsafe extern "C" fn qjd_cursor_get_str( }) } +/// Parse the JSON number at `path` (relative to `*c`) as `i64`. +/// Returns `QJD_OUT_OF_RANGE` if the value does not fit in `i64`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `path` must point to `path_len` bytes or be NULL +/// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_get_i64( c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut i64, @@ -394,6 +559,14 @@ pub unsafe extern "C" fn qjd_cursor_get_i64( }) } +/// Parse the JSON number at `path` (relative to `*c`) as `f64`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `path` must point to `path_len` bytes or be NULL +/// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_get_f64( c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut f64, @@ -413,6 +586,15 @@ pub unsafe extern "C" fn qjd_cursor_get_f64( }) } +/// Write `1` / `0` into `*out` for JSON `true` / `false` at `path` +/// (relative to `*c`). Returns `QJD_TYPE_MISMATCH` for any other value. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `path` must point to `path_len` bytes or be NULL +/// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_get_bool( c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut c_int, @@ -433,6 +615,14 @@ pub unsafe extern "C" fn qjd_cursor_get_bool( }) } +/// Write the JSON value type at `path` (relative to `*c`) into `*type_out`. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `path` must point to `path_len` bytes or be NULL +/// with `path_len == 0`; `type_out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_typeof( c: *const qjd_cursor, path: *const c_char, path_len: usize, type_out: *mut c_int, @@ -451,6 +641,15 @@ pub unsafe extern "C" fn qjd_cursor_typeof( }) } +/// Write the number of direct children of the container at `path` +/// (relative to `*c`) into `*out`. Returns `QJD_TYPE_MISMATCH` for non-containers. +/// +/// # Safety +/// +/// See the module-level [shared safety contract](self#shared-safety-contract). +/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// document is still alive; `path` must point to `path_len` bytes or be NULL +/// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] pub unsafe extern "C" fn qjd_cursor_len( c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut usize, @@ -469,6 +668,12 @@ pub unsafe extern "C" fn qjd_cursor_len( }) } +/// Test-only export that forces a Rust panic to verify the FFI panic barrier +/// converts it to `QJD_OOM` instead of unwinding across the boundary. +/// +/// # Safety +/// +/// Has no preconditions. Marked `unsafe extern "C"` for ABI consistency. #[cfg(feature = "test-panic")] #[no_mangle] pub unsafe extern "C" fn qjd_test_panic() -> c_int { diff --git a/src/path.rs b/src/path.rs index c168526..35f67a0 100644 --- a/src/path.rs +++ b/src/path.rs @@ -73,7 +73,7 @@ impl<'a> Iterator for PathIter<'a> { mod tests { use super::*; - fn parse(p: &[u8]) -> Result, qjd_err> { + fn parse(p: &[u8]) -> Result>, qjd_err> { PathIter::new(p).collect() } diff --git a/src/scan/avx2.rs b/src/scan/avx2.rs index 0b9f09e..03cdfd0 100644 --- a/src/scan/avx2.rs +++ b/src/scan/avx2.rs @@ -240,9 +240,8 @@ mod tests { // Use longer key padding. let mut buf = Vec::with_capacity(64); buf.extend_from_slice(b"{\"k\":\""); // 6 - for _ in 0..56 { buf.push(b'a'); } // +56 = 62 - buf.push(b'"'); // +1 = 63 - buf.push(b'}'); // +1 = 64 + buf.resize(62, b'a'); // +56 = 62 + buf.extend_from_slice(b"\"}"); // +2 = 64 assert_eq!(buf.len(), 64); parity(&buf); } @@ -273,9 +272,8 @@ mod tests { let mut buf = Vec::new(); buf.extend_from_slice(b"{\"k\":\""); // ~10 KB of string interior — many chunks fully inside the string. - for _ in 0..10_000 { buf.push(b'a'); } - buf.push(b'"'); - buf.push(b'}'); + buf.resize(buf.len() + 10_000, b'a'); + buf.extend_from_slice(b"\"}"); // Pad to 64-aligned to also exercise the no-tail branch. while buf.len() % 64 != 0 { buf.push(b' '); } parity(&buf); diff --git a/src/scan/mod.rs b/src/scan/mod.rs index 85f9874..aaed646 100644 --- a/src/scan/mod.rs +++ b/src/scan/mod.rs @@ -60,16 +60,8 @@ pub(crate) fn validate_brackets(buf: &[u8], indices: &[u32]) -> Result<(), usize match b { b'{' | b'[' => stack.push(b), - b'}' => { - if stack.pop() != Some(b'{') { - return Err(pos); - } - } - b']' => { - if stack.pop() != Some(b'[') { - return Err(pos); - } - } + b'}' if stack.pop() != Some(b'{') => return Err(pos), + b']' if stack.pop() != Some(b'[') => return Err(pos), _ => {} } } diff --git a/src/scan/scalar.rs b/src/scan/scalar.rs index d693ad1..1608d78 100644 --- a/src/scan/scalar.rs +++ b/src/scan/scalar.rs @@ -137,13 +137,8 @@ mod tests { #[test] fn deeply_nested() { - let mut buf = Vec::new(); - for _ in 0..100 { - buf.push(b'['); - } - for _ in 0..100 { - buf.push(b']'); - } + let mut buf = vec![b'['; 100]; + buf.resize(200, b']'); let r = scan(&buf).unwrap(); assert_eq!(r.len(), 200); } diff --git a/tests/ffi_numbers.rs b/tests/ffi_numbers.rs index c1f4de8..43fbcb5 100644 --- a/tests/ffi_numbers.rs +++ b/tests/ffi_numbers.rs @@ -41,11 +41,11 @@ fn get_i64_overflow() { #[test] fn get_f64_basic() { - let d = parse(b"{\"a\":3.14}"); + let d = parse(b"{\"a\":2.5}"); let mut v: f64 = 0.0; let p = b"a"; unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; - assert!((v - 3.14).abs() < 1e-12); + assert!((v - 2.5).abs() < 1e-12); unsafe { qjd_free(d) }; } From bfed1e5e69ec35729861f35e818142f6d6e13d66 Mon Sep 17 00:00:00 2001 From: Yuansheng Wang Date: Fri, 15 May 2026 17:13:03 +0000 Subject: [PATCH 2/2] test: restore inexact-decimal coverage in f64 parsing tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local review caught that swapping 3.14 → 2.5 (to silence clippy::approx_constant) accidentally removed the only f64 test exercising the decimal-to-binary rounding path: 2.5 is exactly representable in IEEE-754, as are the rest of the suite's values. Switch to 1.7, which is inexact like the original 3.14 was, and rename `f64_decimal` to `f64_inexact_decimal` so the intent is visible in the test name. --- src/decode/number.rs | 2 +- tests/ffi_numbers.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/decode/number.rs b/src/decode/number.rs index 62eca03..45d2f89 100644 --- a/src/decode/number.rs +++ b/src/decode/number.rs @@ -67,7 +67,7 @@ mod tests { } #[test] fn f64_zero() { assert_eq!(parse_f64(b"0.0").unwrap(), 0.0); } - #[test] fn f64_decimal() { assert!((parse_f64(b"2.5").unwrap() - 2.5).abs() < 1e-12); } + #[test] fn f64_inexact_decimal() { assert!((parse_f64(b"1.7").unwrap() - 1.7).abs() < 1e-12); } #[test] fn f64_negative(){ assert_eq!(parse_f64(b"-1.5").unwrap(), -1.5); } #[test] fn f64_exponent(){ assert_eq!(parse_f64(b"1e2").unwrap(), 100.0); } diff --git a/tests/ffi_numbers.rs b/tests/ffi_numbers.rs index 43fbcb5..eb65394 100644 --- a/tests/ffi_numbers.rs +++ b/tests/ffi_numbers.rs @@ -41,11 +41,11 @@ fn get_i64_overflow() { #[test] fn get_f64_basic() { - let d = parse(b"{\"a\":2.5}"); + let d = parse(b"{\"a\":1.7}"); let mut v: f64 = 0.0; let p = b"a"; unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; - assert!((v - 2.5).abs() < 1e-12); + assert!((v - 1.7).abs() < 1e-12); unsafe { qjd_free(d) }; }