Skip to content

Phase 1: Seq<T> ghost type#143

Draft
coord-e wants to merge 4 commits into
mainfrom
claude/nifty-newton-wfxz2e
Draft

Phase 1: Seq<T> ghost type#143
coord-e wants to merge 4 commits into
mainfrom
claude/nifty-newton-wfxz2e

Conversation

@coord-e

@coord-e coord-e commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 of the Creusot-style ghost-sequence support tracked in
/root/.claude/plans/how-can-we-implement-buzzing-cascade.md. Eventual
goal: verify Creusot's iter::Map / iter::MapExt in Thrust.

Introduces thrust_models::model::Seq<T> as (Array<Int, T>, Int)
(mirroring the existing Vec model). Operations:

Op Lowering
Seq::<T>::empty() (seq_default_arr_int, 0)
Seq::singleton(e) (store(seq_default_arr_int, 0, e), 1)
s.len() tuple_proj(s, 1)
s.push(e) (store(fst(s), snd(s), e), snd(s) + 1)
s[i] select(fst(s), i)
s.concat(t) (seq_concat_arr_int(fst(s), snd(s), fst(t), snd(t)), snd(s) + snd(t))
s.subsequence(l, r) (seq_subseq_arr_int(fst(s), l, r), r - l)

seq_default_arr_int is ((as const (Array Int Int)) 0), always emitted
in the SMT preamble. Its concrete contents are irrelevant because callers
never access indices past the tracked length.

seq_concat_arr_int and seq_subseq_arr_int are emitted via SMT-LIB2
(define-fun-rec ...), gated by System::uses_seq_concat /
uses_seq_subseq so the recursive declarations are only in scope when
the analyzer has lowered at least one use. Unconditionally emitting the
two define-fun-rec definitions noticeably slows pcsat on unrelated
tests (a baseline ≈1 s test ran into the tens of seconds in
measurement), so gating is needed even for code paths that never touch
Seq.

Indexed reasoning

concat / subsequence length-only properties verify directly under
either solver because the length is computed inline by the analyzer.

For whole-Seq equality with a constant recursion bound (e.g.
Seq::singleton(x).concat(Seq::empty()) == Seq::singleton(x)), pcsat
unfolds the define-fun-rec definitions directly. The third commit
ships paired pass/fail tests (seq_concat_const, seq_subseq_const)
that exercise this without any peephole help — whole-Seq equality
reduces to tuple/array equality without ever constructing a
select(seq_concat_arr_int(...), _) term, so the follow-up commit's
peephole does not apply.

For unbounded indexed access (e.g. s.concat(t)[i] == s[i] when
i < s.len()), the fourth commit adds a chc::Term::select peephole
that rewrites one unfolding step of the recursive definitions:

select(seq_concat_arr_int(sa, sn, ta, tn), i)
  ↦ ite(i < sn, select(sa, i), select(ta, i - sn))

select(seq_subseq_arr_int(a, l, r), i)
  ↦ ite(0 <= i < r - l,
        select(a, l + i),
        select(seq_default_arr_int, 0))

The define-fun-rec definitions remain the ground truth — the rewrites
are exactly one unfolding step — but pcsat can prove unbounded indexed
properties against the inlined ITE form, where unfolding through
define-fun-rec over a universally quantified recursion bound requires
an inductive invariant pcsat doesn't find in a reasonable budget.

z3 / Spacer in HORN logic treats every define-fun-rec as
uninterpreted and returns unknown; per user direction, indexed
reasoning is not targeted at z3.

Tests gated on pcsat opt in with
//@rustc-env: THRUST_SOLVER=tests/thrust-pcsat-wrapper.

Term simplifiers added to chc::Term

  • tuple_proj(tuple(ts...), i)ts[i] — Spacer doesn't reduce
    datatype projections of literal constructors, so this is needed for
    e.g. Seq::empty().len() to reduce syntactically to 0.
  • select(store(a, i, v), j)v / select(a, j) for concrete i,
    j integer literals — needed for Seq::singleton(x)[0] == x.
  • select(seq_concat_arr_int(...), i) / select(seq_subseq_arr_int(...), i)
    ite(...) — described above (commit 4 only).

chc::Function::ITE and chc::Term::ite are introduced; Term::select
gains a V: Clone bound (the rewrites duplicate index / sn) which
cascades into Resolver::Output: Clone plus matching bounds on the
three Resolver impls — all concrete usages already satisfy Clone,
so this is a non-breaking strengthening.

Also fixes chc::Term::App emission to print bare f (not (f)) for
nullary functions, matching SMT-LIB conventions for declare-const.

Commit layout

  1. Add Seq<T> ghost type — the type and Model impl, no operations.
  2. Add basic operationsempty, singleton, len, push,
    indexing. Each lowers to existing CHC primitives. Consolidated
    seq_basic pass/fail pair plus an end-to-end seq_specs_vec_build
    program test.
  3. Add concat / subsequence via define-fun-rec — adds the two
    recursive SMT definitions and the analyzer arms that lower the
    methods. Paired length-only tests (seq_concat, seq_subsequence)
    plus paired whole-Seq equality tests with a constant recursion bound
    (seq_concat_const, seq_subseq_const) that exercise pcsat's
    unfolding of the define-fun-rec definitions directly, without
    passing through any select peephole.
  4. Peephole-rewrite indexed accesschc::Function::ITE,
    Term::ite, and the two select rewrites described above. Paired
    pass/fail unbounded indexed tests for both ops (seq_concat_index,
    seq_concat_index_right, seq_subseq_index) plus an end-to-end
    seq_specs_concat_run test exercising the rewrites through a real
    runtime function.

Phases that follow (separate PRs)

  • Phase 2: snapshot!{} / proof_assert!{} macros
  • Phase 3: trait #[predicate] propagation
  • Phase 4: end-to-end port of map.rs / map_ext.rs

Test plan

  • cargo build
  • cargo fmt --all -- --check
  • cargo clippy -- -D warnings
  • cargo test --test ui (272 / 272 passing, including 20 new
    paired seq_* tests: seq_basic, seq_specs_vec_build,
    seq_concat, seq_subsequence, seq_concat_const,
    seq_subseq_const, seq_concat_index, seq_concat_index_right,
    seq_subseq_index, seq_specs_concat_run)

Generated with Claude Code.

https://claude.ai/code/session_01A7nMxM1sJ8wxVJmMvsEvSX

@coord-e coord-e changed the title Phase 1: Seq&lt;T&gt; ghost type — len, push, indexing Phase 1: Seq&lt;T&gt; ghost type Jun 20, 2026
A spec-only `thrust_models::model::Seq<T>` analogous to the existing
`model::Vec`: the logical representation is `(Array<Int, T>, Int)`
(the array carries elements, the int carries length). The
`#[thrust::def::seq_model]` marker lets the analyzer recognise `Seq`
ADTs in future patches.

This commit introduces only the type and its `Model` impl; the
operations (`empty`, `singleton`, `len`, `push`, `concat`,
`subsequence`, indexing) are added in follow-up commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7nMxM1sJ8wxVJmMvsEvSX
@coord-e coord-e force-pushed the claude/nifty-newton-wfxz2e branch 3 times, most recently from a09bb38 to 877868a Compare June 23, 2026 04:58
claude added 3 commits June 23, 2026 05:20
Adds the non-recursive operations of `Seq<T>`. Each lowers
mechanically to existing CHC primitives — no SMT-side function
definition or peephole is involved here:

| API                | Lowering                                            |
|--------------------|-----------------------------------------------------|
| `Seq::<T>::empty()`| `(seq_default_arr_int, 0)`                          |
| `Seq::singleton(e)`| `(store(seq_default_arr_int, 0, e), 1)`             |
| `s.len()`          | `tuple_proj(s, 1)`                                  |
| `s.push(e)`        | `(store(fst(s), snd(s), e), snd(s) + 1)`            |
| `s[i]`             | `select(fst(s), i)`                                 |

`seq_default_arr_int` is a designated constant array
`((as const (Array Int Int)) 0)` always emitted in the SMT preamble;
its concrete contents are irrelevant because callers never access
indices past the length they keep track of.

The `s[i]` lowering reuses the existing `ExprKind::Index` analyzer
path, gated on `def_ids.seq_model()` to detect `Seq`-typed receivers
(otherwise the path expects an `Array`).

Paired pass/fail `seq_basic` test covering the five operations in
one place, plus an end-to-end `seq_specs_vec_build` test that
specifies a Vec-building runtime function through Seq operations.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7nMxM1sJ8wxVJmMvsEvSX
`s.concat(t)` and `s.subsequence(l, r)` lower in `annot_fn.rs` to
two new CHC function symbols whose bodies are emitted as recursive
SMT-LIB2 `define-fun-rec`s:

  (define-fun-rec seq_concat_arr_int
    ((sa (Array Int Int)) (sn Int) (ta (Array Int Int)) (tn Int))
    (Array Int Int)
   (ite (<= tn 0) sa
        (store (seq_concat_arr_int sa sn ta (- tn 1))
               (+ sn (- tn 1))
               (select ta (- tn 1)))))

  (define-fun-rec seq_subseq_arr_int
    ((a (Array Int Int)) (l Int) (r Int)) (Array Int Int)
   (ite (<= r l) seq_default_arr_int
        (store (seq_subseq_arr_int a l (- r 1))
               (- (- r 1) l)
               (select a (- r 1)))))

The Thrust analyzer wraps each result in the usual
`(array, length)` tuple, with the length computed inline as
`s.len + t.len` / `r - l` so length-only properties (`len(concat(s, t))
== len(s) + len(t)`) verify on z3 + pcsat directly — the SMT
obligation simply never references the recursive function.

The emissions are gated by `System::uses_seq_concat` /
`uses_seq_subseq`, set when the analyzer lowers the operation.
Unconditionally emitting the `define-fun-rec` definitions
significantly slows pcsat on unrelated tests (measured: a baseline
1-second test rises into the tens of seconds), so gating is needed
for the benefit of code paths that never touch `Seq`.

pcsat unfolds the definitions for any concrete recursion bound.
The follow-up peephole commit covers the unbounded indexed case.

z3-Spacer in HORN logic treats every `define-fun-rec` as
uninterpreted and returns `unknown` (by user direction, we don't
aim to support indexed reasoning on Z3).

Paired pass/fail length tests (`seq_concat`, `seq_subsequence`)
exercise the length-only encoding. Paired pass/fail constant-bound
whole-Seq equality tests (`seq_concat_const`, `seq_subseq_const`)
exercise pcsat's unfolding of the `define-fun-rec` definitions
directly: whole-Seq equality reduces to tuple/array equality without
ever constructing a `select(seq_concat_arr_int(...), _)` term, so
the follow-up commit's `select` peephole does not apply and pcsat
must consult the recursive definition itself.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7nMxM1sJ8wxVJmMvsEvSX
`select(seq_concat_arr_int(sa, sn, ta, tn), i)` and
`select(seq_subseq_arr_int(a, l, r), i)` are simplified at
`chc::Term::select` construction time to equivalent
`(ite ...)` expressions over the underlying sequences:

  select(seq_concat_arr_int(sa, sn, ta, tn), i)
    ↦ ite(i < sn, select(sa, i), select(ta, i - sn))

  select(seq_subseq_arr_int(a, l, r), i)
    ↦ ite(0 <= i < r - l,
          select(a, l + i),
          select(seq_default_arr_int, 0))

The `define-fun-rec` definitions from the previous commit remain
the ground truth — the rewrites are exactly one unfolding step of
those definitions — but pcsat can prove unbounded indexed
properties against the inlined ITE form, where unfolding through
`define-fun-rec` over a universally quantified recursion bound
requires an inductive invariant pcsat doesn't find in a reasonable
budget. Bounded (constant) cases still work via direct unfolding
of the recursive definitions.

`chc::Function::ITE` and `chc::Term::ite` are introduced for this
rewrite, and `Term::select` gains a `V: Clone` bound (the rewrites
need to duplicate `index` and `sn`). That bound cascades into
`Resolver::Output: Clone` plus matching bounds on the three
`Resolver` impls (`RefinementResolver`, `MappedResolver`,
`StackedResolver`) — all concrete usages already satisfy `Clone`,
so this is a non-breaking strengthening.

Paired pass/fail indexed tests for both operations, plus an
end-to-end `seq_specs_concat_run` test exercising the same
rewrites through a real runtime function.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7nMxM1sJ8wxVJmMvsEvSX
@coord-e coord-e force-pushed the claude/nifty-newton-wfxz2e branch from 877868a to f2666fd Compare June 23, 2026 05:27
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.

2 participants