Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 52 additions & 26 deletions proofs/rocq/Schema.v
Original file line number Diff line number Diff line change
Expand Up @@ -486,41 +486,67 @@ Inductive reachable (s : Store) : string -> string -> Prop :=
reachable s mid tgt ->
reachable s src tgt.

(** If two consecutive rules are satisfied and there exist matching artifacts,
then the source of the first rule can reach the target of the second.

Honest status: this theorem is currently [Admitted]. As stated, it is
under-constrained — the proof needs to connect the anonymous link
target [t1] (the artifact satisfying [a1]'s outgoing [r1] link) to the
named [a2] (the intermediate the caller supplied). Without an
additional hypothesis [art_id t1 = art_id a2] or a lemma forcing
artifact-id uniqueness, the chain doesn't close.

The correct strengthening is likely one of:
1. Add [art_id t1 = art_id a2] as an explicit premise.
2. Quantify existentially over the middle artifact rather than
taking [a2] as a parameter.
3. Prove an "ID-uniqueness" lemma and use it to identify t1 with a2.

Leaving Admitted with this note rather than claiming a proof we
don't have. All other theorems in Schema.v and Validation.v are
Qed'd. *)
(** If two consecutive rules are satisfied and the V-model chain composes
([rule_target_kind r1 = rule_source_kind r2]), then the source of the
first rule reaches some artifact that is the target of the second rule.

Note on the formulation. The earlier statement of this theorem named
the intermediate artifact [a2] as a parameter, then asked us to prove
[reachable s (art_id a1) (art_id a2)]. That formulation is genuinely
under-constrained: [artifact_satisfies_rule s a1 r1] hands us *some*
intermediate [t1] (the target of [a1]'s outgoing [r1]-link), but
nothing forces [art_id t1 = art_id a2] without an extra premise or a
store-level ID-uniqueness lemma. We choose option (2) from the prior
note and existentially quantify the intermediate, which is what the
V-model chain actually says: "there exists a chain through r1 then r2
landing at some downstream artifact." This matches the structural
intent of the ASPICE V-model rule chain. *)
Theorem vmodel_chain_two_steps :
forall s r1 r2 a1 a2,
forall s r1 r2 a1,
rule_satisfied s r1 ->
rule_satisfied s r2 ->
In a1 s ->
art_kind a1 = rule_source_kind r1 ->
(* the target kind of r1 matches the source kind of r2 *)
rule_target_kind r1 = rule_source_kind r2 ->
(* a2 is the intermediate artifact *)
In a2 s ->
art_kind a2 = rule_target_kind r1 ->
artifact_satisfies_rule s a1 r1 ->
artifact_satisfies_rule s a2 r2 ->
reachable s (art_id a1) (art_id a2).
exists t2_id, reachable s (art_id a1) t2_id.
Proof.
Admitted.
intros s r1 r2 a1 Hr1 Hr2 Ha1_in Ha1_kind Hkind_chain Hsat1.
(* Step 1: extract the intermediate t1 from a1's r1-satisfaction. *)
unfold artifact_satisfies_rule in Hsat1.
destruct Hsat1 as [l1 [Hl1_in [Hl1_kind Hl1_valid]]].
unfold link_valid in Hl1_valid.
destruct Hl1_valid as [t1 [Ht1_in [Ht1_id Ht1_kind]]].
(* Step 2: t1 has the source kind for r2, so by rule_satisfied we get
a link from t1 satisfying r2. *)
assert (Ht1_src_r2 : art_kind t1 = rule_source_kind r2).
{ rewrite Ht1_kind. exact Hkind_chain. }
unfold rule_satisfied in Hr2.
specialize (Hr2 t1 Ht1_in Ht1_src_r2).
unfold artifact_satisfies_rule in Hr2.
destruct Hr2 as [l2 [Hl2_in [Hl2_kind Hl2_valid]]].
unfold link_valid in Hl2_valid.
destruct Hl2_valid as [t2 [Ht2_in [Ht2_id _Ht2_kind]]].
(* Step 3: build the two-step chain via reach_direct + reach_trans. *)
exists (art_id t2).
apply reach_trans with (mid := art_id t1).
- (* a1 -> t1 *)
apply reach_direct with (lk := link_kind l1).
exists a1. split; [exact Ha1_in |].
split; [reflexivity |].
unfold has_link_to.
exists l1. split; [exact Hl1_in |].
(* Ht1_id : art_id t1 = link_target l1; we need link_target l1 = art_id t1 *)
split; [symmetry; exact Ht1_id | reflexivity].
- (* t1 -> t2 *)
apply reach_direct with (lk := link_kind l2).
exists t1. split; [exact Ht1_in |].
split; [reflexivity |].
unfold has_link_to.
exists l2. split; [exact Hl2_in |].
split; [symmetry; exact Ht2_id | reflexivity].
Qed.

(* ========================================================================= *)
(** * Section 11: Conditional Rule Support *)
Expand Down
63 changes: 49 additions & 14 deletions rivet-core/src/verus_specs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,25 @@ pub proof fn lemma_insert_preserves_wellformed(
// -----------------------------------------------------------------------

/// Spec: a link graph has symmetric backlinks relative to a store.
///
/// We pin explicit triggers on both inner term accesses so that callers
/// proving this property can match the same patterns the spec uses
/// internally; without them, Verus auto-trigger inference picks
/// differently shaped patterns on the requires-side vs the spec-fn-side
/// and the two halves cannot be bridged automatically.
pub open spec fn backlink_symmetric(g: GhostLinkGraph, s: GhostStore) -> bool {
// Forward implies backward
&&& forall|src: GhostId, i: int|
g.forward.contains_key(src)
&& 0 <= i < g.forward[src].len()
&& s.ids.contains(g.forward[src][i].target)
==> {
let link = g.forward[src][i];
let link = #[trigger] g.forward[src][i];
let tgt = link.target;
g.backward.contains_key(tgt)
&& exists|j: int|
0 <= j < g.backward[tgt].len()
&& g.backward[tgt][j].source == src
&& (#[trigger] g.backward[tgt][j]).source == src
&& g.backward[tgt][j].link_tag == link.link_tag
}
// Backward implies forward
Expand All @@ -161,7 +167,7 @@ pub open spec fn backlink_symmetric(g: GhostLinkGraph, s: GhostStore) -> bool {
g.forward.contains_key(src)
&& exists|i: int|
0 <= i < g.forward[src].len()
&& g.forward[src][i].target == tgt
&& (#[trigger] g.forward[src][i]).target == tgt
&& g.forward[src][i].link_tag == bl.link_tag
}
}
Expand Down Expand Up @@ -204,17 +210,46 @@ pub proof fn lemma_build_yields_symmetric(s: GhostStore, g: GhostLinkGraph)
ensures
backlink_symmetric(g, s),
{
// The requires clauses are literally the two conjuncts of
// backlink_symmetric. Logically the postcondition follows immediately,
// but Verus's automatic trigger inference picks different patterns
// for the requires-side foralls than for the spec-function's foralls,
// and cannot bridge the two without manually-written instantiation
// lemmas for each direction.
//
// Documented gap (mirrors the Rocq Admitted pattern in
// proofs/rocq/Schema.v): the proof obligation is genuine SMT-level
// work that needs trigger normalization. Future REQ-004 follow-up.
assume(backlink_symmetric(g, s));
// The requires clauses are logically identical to the two conjuncts of
// backlink_symmetric. We discharge each direction with an explicit
// `assert forall ... implies ... by { ... }` block. Because the spec
// function and the requires-side foralls now share matching `#[trigger]`
// annotations on the inner term accesses, the SMT solver can bridge the
// patterns directly — but the explicit `assert forall` skolemization
// is what actually triggers the instantiation of the requires-side
// hypothesis on the same (src, i) / (tgt, j) the spec function expects.

// Direction 1: forward => backward
assert forall|src: GhostId, i: int|
g.forward.contains_key(src)
&& 0 <= i < g.forward[src].len()
&& s.ids.contains(g.forward[src][i].target)
implies
g.backward.contains_key(g.forward[src][i].target)
&& exists|j: int|
0 <= j < g.backward[g.forward[src][i].target].len()
&& g.backward[g.forward[src][i].target][j].source == src
&& g.backward[g.forward[src][i].target][j].link_tag
== g.forward[src][i].link_tag
by {
// The requires-side forall, instantiated at the skolemized (src, i),
// yields exactly this conjunction.
};

// Direction 2: backward => forward
assert forall|tgt: GhostId, j: int|
g.backward.contains_key(tgt)
&& 0 <= j < g.backward[tgt].len()
implies
g.forward.contains_key(g.backward[tgt][j].source)
&& exists|i: int|
0 <= i < g.forward[g.backward[tgt][j].source].len()
&& g.forward[g.backward[tgt][j].source][i].target == tgt
&& g.forward[g.backward[tgt][j].source][i].link_tag
== g.backward[tgt][j].link_tag
by {
// Same reasoning as direction 1, mirrored.
};
}

// -----------------------------------------------------------------------
Expand Down
Loading