Skip to content

Lef/circular buffer#554

Open
elefthei wants to merge 27 commits intomainfrom
lef/circular_buffer
Open

Lef/circular buffer#554
elefthei wants to merge 27 commits intomainfrom
lef/circular_buffer

Conversation

@elefthei
Copy link

@elefthei elefthei commented Feb 8, 2026

A circular buffer that wraps around, and doubles in size when appending more than N elements.

Lef Ioannidis and others added 27 commits February 8, 2026 03:28
…lock

- Remove 230 lines of dead code across 3 files (16 unused definitions/lemmas
  identified via binary search against Z3 verification)
- Fix write_buffer resize branch fold failure by adding 4 standalone Spec
  lemmas (contents_length, wf, prefix, coherence_transfer) and restructuring
  the resize write loop with bool-bound invariant for exit condition
- Fix read_buffer loop body verification by adding read_step_invariant Spec
  lemma that bundles coherence extraction + Seq.upd reasoning
- Remove --admit_smt_queries true block: full module now verifies with zero
  errors and zero admits (only platform_fits_u64 assume remains)

Spec.fst: +5 lemmas (write_buffer_resize_*, read_step_invariant)
Modular.fst: -41 lines (removed upd_at_different_index, wrap_decomposes)
CircularBuffer.fst: -admit block, +lemma calls, restructured resize loop
- S2: Remove write_buffer_resize_contents_length (subsumed by write_buffer_resize_wf)
- S3: Collapse gapless_preserved_by_resize from forall_intro+aux to ()
- S4: Collapse phys_log_coherent_seq_equal from forall_intro+aux to ()
- S6: Collapse lemma_resize_invariant_step: merge j<vi and j>vi branches
- S7: Simplify read_step_invariant: remove redundant else branch structure
- Simplify write_buffer_resize_prefix aux body

All modules fully verified, 0 admits. Top-level specs unchanged.
Spec.fst: 545 -> 516 lines (-29)
CircularBuffer.fst: 756 -> 753 lines (-3)
Total: -32 lines
Replace 3 predicate modes (is_circular_buffer, is_circular_buffer_ooo,
is_circular_buffer_rm) with a single unified is_circular_buffer that
always uses RangeMap-based gap tracking, modeled after msquic's
recv_buffer.c design.

Key changes:
- Single is_circular_buffer cb rm st predicate (always RM-tracked)
- create returns (circular_buffer & RM.range_map_t)
- write_buffer uses absolute stream offsets with auto-resize
- read_buffer works with RM predicate (copy-based)
- read_zerocopy returns segment pointers with trade-based borrow
- drain leaves RangeMap unchanged (absolute offset advantage)
- Removed gapless predicate and all sequential-only code
- Removed enter_ooo/enter_rm/exit_rm_to_ooo transitions
- Added ranges_match_create_nones lemma for create verification

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Thin wrapper (no .fsti) that re-exports all CircularBuffer public
functions. Required because modules with .fsti produce private krml
symbols that KaRaMeL drops during reachability analysis.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a Pulse module with .fsti defines a polymorphic type with an erased
parameter, krml's monomorphizer creates two incompatible C struct types:
  node__range_()  = { key, left, right }        (from caller, () erased)
  node__range_any = { key, value, left, right }  (from impl, any erased)

The checker rejects the subtype check and silently drops 5 RangeMap
functions from the C output.

Run: ./extraction/repro_krml_bundle_bug.sh

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace multi-module reproducer with a single Pulse file that triggers the
monomorphization type mismatch: AVLTree erased value param v is monomorphized
as () in the caller but any in the implementation, creating incompatible C
structs (3 vs 4 fields).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop-in replacement for AVL-based RangeMap with better cache locality
and clean KaRaMeL extraction. Uses sorted Pulse.Lib.Vector of ranges
with linear scan insert and automatic merge of overlapping intervals.

Verified API surface:
- range_vec_create / range_vec_free
- range_vec_contiguous_from / range_vec_max_endpoint
- range_vec_add (body with admits, proofs TODO)

Spec bridge reuses Pulse.Lib.RangeMap.Spec unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop-in replacement: same API signatures, same spec bridge via
Pulse.Lib.RangeMap.Spec. All verification conditions still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prevents KaRaMeL from trying to extract spec-only functions
(seq_all_valid, seq_to_spec, etc.) that use FStar.Seq which
has no C implementation.

KaRaMeL extraction now succeeds:
- All 7 public functions extracted to clean C
- range struct maps to { size_t start; size_t len; }
- No monomorphization bug (no .fsti = no erased type issue)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both helper functions now have verified implementations instead of
admit(). Uses mutable boolean flag pattern for while loops to avoid
Pulse's 'Cannot check relation with uvars' error with invariant b.

vec_insert_at: push_back + shift-right loop + set at insertion point
vec_remove_range: shift-left loop + pop_back loop

Remaining admits only in range_vec_add spec bridge proofs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Standalone benchmark for the extracted vector-based range tracker.
Performance comparison with old AVL-based CB (full operations):
- Old OOO writes (CB+AVL rangemap): ~500K ops/s
- New range tracking only (vector): 170M-970M ops/s

Sequential add: 554M-972M add/s
OOO add: 170M-244M add/s
Gap-fill add: 898M-967M add/s
Queries: 784M-998M query/s

Also updates RecvBufferWrapper.fst to use RangeVec and
includes fresh KaRaMeL C extraction of RangeVec.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add capacity invariant to is_range_vec predicate (enables push_back
  without separate capacity precondition tracking)
- Add add_range_all_before and add_range_insert_no_overlap lemmas to
  Spec.fst for bridging imperative add to recursive spec
- Add seq_insert/seq_remove pure helper functions
- Fix KaRaMeL extraction: use -library Pulse.Lib.Vector to avoid
  stack-allocated VLA bug in Vector extraction
- Provide proper heap-allocated Vector stubs in krmlinit_rv.c
- Reduce admits from 6 to 4 (removed capacity-related admits)

Benchmark results (1000 iterations):
  Sequential add: 10-37M ops/s
  OOO add: 10-22M ops/s
  Queries: 347M ops/s

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add seq_to_spec bridge lemmas: snoc, append, slice, insert
- Add forall_high_below_to_spec to lift range-level facts to spec
- Strengthen search loop invariant with forall k<iv. high(s[k]) < offset
- Make seq_insert/seq_remove total functions (avoid Pulse postcondition refinement issues)
- Strengthen vec_insert_at postcondition to track content (s' == seq_insert s i r)
- Add add_range_skip_prefix lemma to Spec.fst for merge-case decomposition
- Fill admits 1 & 2 (insert cases) using add_range_all_before and add_range_insert_no_overlap
- Remaining admits: 2 in vec_insert_at (content tracking), 2 capacity, 2 merge case

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Strengthen merge loop invariant: mh > merged_low (strict)
- Remove bounds admit: SZ.sub final_high merged_low now provable
- Restructure merge case: single admit covers set+remove+spec bridge
- 5 remaining admits: 2 vec_insert_at content, 2 capacity, 1 merge spec

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Spec.fst:
- Add range_map_wf_sorted: transitive sortedness from wf
- Add merge_absorbed_high: compute final merged endpoint
- Add add_range_merge_step: unfold add_range merge branch
- Add add_range_merge_scan: inductive merge characterization
  (when first k elements overlap, result is merged + suffix)

RangeVec.fst:
- Strengthen merge loop invariant to track overlap forall
  and exit condition (jv==n or mh < s[jv].start)
- Add forall_overlap_to_spec bridge lemma

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split add_range_merge_full into add_range_merge_suffix (inner chain:
merge_step + merge_scan) and add_range_merge_full (skip_prefix +
merge_suffix). This avoids Prims.subtype_of quantifier cascade that
caused Z3 timeout at rlimit 200. Now verifies at rlimit 40 with
split_queries always.

Also adds range_map_wf_slice lemma (wf preserved through suffix slice).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add merge_loop_body_step bridge lemma (packages mah tracking + mh0 coverage)
- Add exit_condition_to_spec bridge (lifts range-level exit to spec-level)
- Add add_range_merge_full_explicit to Spec.fst (explicit mh0 for easier SMT matching)
- Add merge_absorbed_high helpers: unfold_right, eq_max_last, mh0_covers_absorbed
- Prove merge loop body verifies (was Error 19 at loop body)
- Fill no-remove merge case admit (seq_upd_remove_spec + range_to_interval match)
- Remaining admits: vec_insert_at content (2), vec_remove_range content (1),
  capacity invariants (2), remove-path merge (1, blocked on vec_remove_range)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add shift_set_is_seq_insert bridge lemma for shift-right + V.set
- Strengthen shift-right loop invariant with prefix/shifted/exit tracking
- Call bridge lemma BEFORE V.set (avoids complex Seq.upd expression in Pulse)
- Prove else branch: snoc_is_seq_insert with explicit bounds assertions
- Both branches of vec_insert_at now fully proved (no admits)
- 3 remaining admits are all Class B capacity invariants (L746, L764, L864)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add size_bounded ghost fn to Vector (exports |s| <= cap from is_vector)
- Strengthen push_back postcondition: cap' ∈ {cap, 2*cap}
- Strengthen pop_back postcondition: capacity condition preserved
- Prove insert_capacity_condition bridge lemma (one admit for cap≥2^63)
- Prove all capacity admits in range_vec_add:
  - L746 (append-at-end): via vec_insert_at capacity postcondition
  - L764 (insert-no-overlap): via vec_insert_at capacity postcondition
  - L864 (merge-remove): via vec_remove_range capacity postcondition
- Only remaining admit: edge case in insert_capacity_condition
  requiring cap ≥ 2^63 (physically unreachable)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the physically-unreachable admit() in insert_capacity_condition
with a complete proof using:

- assume val platform_is_64bit: 64-bit platform (SZ.fits_u64)
- assume val max_range_vec_entries: entry count bound (≤ pow2 62)
- SZ.fits_u64_implies_fits: derives SZ.fits(cap+cap) from cap < pow2 63

The proof: when cap = sz+1 ≤ max_range_vec_entries ≤ pow2 62,
cap+cap ≤ 2·pow2_62 = pow2_63 < pow2_64, so SZ.fits(cap+cap).

RangeVec.fst now has zero admits. The two assume vals are standard
platform/physical constraints (64-bit, bounded entry count).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change max_range_vec_entries from pow2_62 to pow2_62 + 1 (needed for
  strict inequality: n separated intervals <= pow2_62 < pow2_62 + 1)
- Add drain_ranges_length: drain never increases interval count
- Add wf_count_bound: n separated intervals span 2n-1 offsets minimum
  (inductive proof using gap >= 1 and count >= 1)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RangeMap.Spec: drain_repr function + 5 lemmas (wf, bounded, length,
  mem_above, add_range_length)
- CB.Spec: ranges_match_drain_repr bridge, drain_repr_preserves_rwp,
  repr_count_bound_gap for bounding interval count after writes
- RangeVec: range_vec_drain implementation (remove or trim first entry),
  seq_to_spec_tail and seq_to_spec_upd0 bridge lemmas
- CB.fst: Added Seq.length repr < max_range_vec_entries to is_circular_buffer

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Drain now calls range_vec_drain to remove/trim consumed intervals,
  keeping repr bounded and first.low >= bo invariant
- All 3 write fold sites prove |repr'| < max via:
  add_range_first_low (first.low >= bo preserved) +
  repr_count_bound_gap (2*n <= al + 1) +
  al <= pow2_63 → n <= pow2_62 < pow2_62 + 1 = max
- Added first.low >= bo conjunct to is_circular_buffer predicate
- Fixed max_range_vec_entries to n == pow2 62 + 1 (exact bound needed)
- RangeMap.Spec: added add_range_first_low inductive lemma

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant