Theory DG_Constraint_Programs
theory DG_Constraint_Programs
imports DG_State "Voblint_Solver.Strategy_Program_Fold"
begin
context
begin
text ‹
This theory states its observations at the program level --- ‹traverse_program›,
‹sides_of_program›, ‹dep_program› --- reaching for \<^const>‹sp_compile› only
where a statement must meet the vendored solver's own tree vocabulary,
and does not unfold it globally: the named observation
equations in \<^theory>‹Voblint_Solver.Strategy_Tree_Program› are stated
against ‹sp_compile› itself, and proofs meet those rather than the
compiler's representation.
›
section ‹What a right-hand side over the packed D/G carrier is›
text ‹
What a generator hands the solver, and how several of those combine. An edge
former takes its source as an address in the solver's own valuation space and
its published slot as an explicit key, and produces a strategy program the
solver can run: the program reads what it needs, publishes what it must, and
answers with a \<^type>‹dg_state›. A generator with several such programs for
one unknown --- several predecessors, several call sites --- folds them here,
and the fold's own answer, publications and dependencies are each the join,
respectively union, over the members.
\<^theory>‹Voblint_Framework.DG_State› supplies the value type and its lattice.
Nothing here inspects a domain, a variable, a global classifier, or a CFG: an
address and a key are all a former sees, which is why the same former reads a
contribution-only unknown and an equation-driven one.›
subsection ‹Low-level edge formers over a solution address›
text ‹
The two formers in this subsection take a step in the direct shape
‹'dl ⇒ 'dg ⇒ 'dg × 'dl›, which no analysis writes: a specification
supplies manager-native transfers, and ‹DG_Spec› compiles those. They
survive because the keyed-solver update-rule regression
needs a step it can vary directly, without a ‹dg_spec› in the way, and that
regression is their only consumer. Read them as the minimal thing a
right-hand side can be, not as the shape production equations take.
An edge or combine former takes its source as an ∗‹address› in the solver's
own valuation space \<^typ>‹'x + 'k› and its published slot as an explicit key,
rather than fixing the source to a local unknown and the slot to the
\<^typ>‹unit› key -- what lets a keyed generator target an arbitrary
activation-indexed unknown directly, with no separate address-rewriting pass.
A program point whose value is carried by a contribution-only unknown --
one with no equation of its own, so that its value is exactly the join of
what was published to it -- is then read by exactly the same former as one
carried by an equation-driven unknown, since \<^const>‹QueryL› and
\<^const>‹QueryG› project the same valuation.
\<^const>‹sp_read_at› itself (\<^theory>‹Voblint_Solver.Strategy_Tree_Program›) is a
generic solver-address dispatcher with no notion of ‹D›/‹G›; here it is
instantiated at ‹'d = ('dl, 'dg) dg_state›, so the value it returns is
already one of this file's packed ‹DG _ bot› / ‹DG bot _› slots, not a raw
‹D›. Reading through an address is therefore an operation on already-packed
DG state, not a general "query something of unknown kind" primitive.
›
definition dg_edge_program_at ::
"('dl::bounded_semilattice_sup_bot ⇒ 'dg::bounded_semilattice_sup_bot ⇒ 'dg × 'dl)
⇒ 'x + 'k ⇒ 'k
⇒ ('x, 'k, ('dl, 'dg) dg_state, ('dl, 'dg) dg_state) strategy_program"
where
"dg_edge_program_at step src gk =
(do {
d ← sp_read_at src;
g ← sp_read_global gk;
_ ← sp_publish gk (DG bot (fst (step (dg_local d) (dg_global g))));
sp_return (DG (snd (step (dg_local d) (dg_global g))) bot)
})"
definition dg_edge_contribution_program_at ::
"('dl::bounded_semilattice_sup_bot ⇒ 'dg::bounded_semilattice_sup_bot ⇒ 'dg × 'dl)
⇒ 'x + 'k ⇒ 'k
⇒ ('x, 'k, ('dl, 'dg) dg_state, ('dl, 'dg) dg_state) strategy_program"
where
"dg_edge_contribution_program_at step src gk =
(do {
d ← sp_read_at src;
g ← sp_read_global gk;
sp_return (DG (snd (step (dg_local d) (dg_global g))) (fst (step (dg_local d) (dg_global g))))
})"
lemma dep_dg_edge_program_at:
"dep_program tau (dg_edge_program_at step src gk) = {src, Inr gk}"
by (cases src) (simp_all add: dg_edge_program_at_def sp_compile_def)
lemma sp_wf_dg_edge_program_at [intro, simp]: "sp_wf (dg_edge_program_at step src gk)"
unfolding dg_edge_program_at_def by (intro sp_wf_bind) simp_all
lemma sp_wf_dg_edge_contribution_program_at [intro, simp]:
"sp_wf (dg_edge_contribution_program_at step src gk)"
unfolding dg_edge_contribution_program_at_def by (intro sp_wf_bind) simp_all
subsection ‹The framework boundary, as theorems›
text ‹
Two independent opaque domains. An analysis provides
‹step : D ⇒ G ⇒ G × D› --- transfer plus publication; the framework reads the
source address's ‹D› and the published slot's ‹G›, and transports the step's
‹Side : G› and ‹Answer : D›. Slot packing (‹DG _ bot› / ‹DG bot _›) encodes the
two-typed unknown space into the solver's single value type; the framework
never looks inside ‹D› or ‹G›. The two theorems say so for ∗‹every› step and
every address: an answer carries no ‹G›, a publication no ‹D›.
›
subsection ‹Why a Side-free analogue exists›
text ‹
\<^const>‹dg_edge_program_at› publishes its ‹G› contribution as soon as it is
evaluated (\<^const>‹Side›). When a generator folds several such programs into one
equation's right-hand side --- several intra predecessors, or several return
sites --- each one's ‹Side› is published at a different moment within the same
evaluation, and the vendored solver's warrowing/APINIS update rule gates
convergence per ∗‹origin›: repeated writes to one key from one origin can
destabilize the equation's own dependency and never converge.
\<^const>‹dg_edge_contribution_program_at› is the Side-free analogue. It answers the
∗‹unsplit› ‹(G, D)› result as one \<^type>‹dg_state› and publishes nothing, so a
caller folds several of them with \<^const>‹fold_rhs_program› --- whose own
\<^const>‹Side› is empty --- and splits the aggregate once, after every
contribution has been read. The two lemmas below are what makes that
substitution legitimate: each half of the contribution answer is exactly the
observation the Side form makes.
›
subsection ‹Folding a list of programs into one D/G value›
text ‹
One unknown can have several right-hand sides --- one per intra predecessor,
one per call site --- and the solver takes one tree per equation.
‹side_rhs_fold_dg› is that reduction: it runs the programs in order, joins their
answers into a running local accumulator, and answers ‹DG acc bot›, letting
each program's own publications pass through untouched. ‹side_acc_dg› is the
same accumulator read declaratively off a valuation, and it is what the
characterization lemmas below state the fold's three observations --- answer,
publications, dependencies --- in terms of.
Both this and \<^const>‹fold_rhs_program› are specializations of one
fold, \<^const>‹fold_rhs_program_projected›, and the choice of specialization is the
whole direct/buffered split. The identity specialization accumulates
complete answers, so a contribution's global half joins into the node's
answer; this one accumulates only \<^const>‹dg_local› and embeds the result as
‹DG d bot›, so a global half never reaches the answer and each program
publishes its own \<^const>‹Side› as it runs. The buffered generator wants the
first --- it gathers every global contribution into the answer precisely so
it can publish once per node --- and the direct generator wants the second.
Replacing either fold by the other would exchange the two generators, not
simplify them.
The narrow definitions here exist to keep the D/G vocabulary the generators
and the routed soundness development actually reason in, rather than
spelling out ‹fold_rhs_program_projected dg_local (λd. DG d bot)› in every statement.
Their algebra is inherited: each observation and monotonicity fact below is
a specialization of the generic one.
›
definition side_rhs_fold_dg ::
"'d::bounded_semilattice_sup_bot
⇒ ('x, 'g, ('d, 'h::bounded_semilattice_sup_bot) dg_state,
('d, 'h) dg_state) strategy_program list
⇒ ('x, 'g, ('d, 'h) dg_state, ('d, 'h) dg_state) strategy_program"
where
"side_rhs_fold_dg acc ps = fold_rhs_program_projected dg_local (λd. DG d bot) acc ps"
lemma side_rhs_fold_dg_simps [simp, code]:
"side_rhs_fold_dg acc [] = sp_return (DG acc bot)"
"side_rhs_fold_dg acc (p # ps) =
do {
res ← p;
side_rhs_fold_dg (acc ⊔ dg_local res) ps
}"
by (simp_all add: side_rhs_fold_dg_def)
lemma sp_wf_side_rhs_fold_dg [intro]:
"∀p ∈ set ps. sp_wf p ⟹ sp_wf (side_rhs_fold_dg acc ps)"
unfolding side_rhs_fold_dg_def by (rule sp_wf_fold_rhs_program_projected)
definition side_acc_dg ::
"'d::bounded_semilattice_sup_bot
⇒ ('x + 'g ⇒ ('d, 'h::bounded_semilattice_sup_bot) dg_state)
⇒ ('x, 'g, ('d, 'h) dg_state, ('d, 'h) dg_state) strategy_program list ⇒ 'd"
where
"side_acc_dg acc τ ps = fold_acc_projected dg_local acc τ ps"
lemma side_acc_dg_simps [simp, code]:
"side_acc_dg acc τ [] = acc"
"side_acc_dg acc τ (p # ps) =
side_acc_dg (acc ⊔ dg_local (traverse_program p τ)) τ ps"
by (simp_all add: side_acc_dg_def)
lemma traverse_side_rhs_fold_dg:
assumes wf: "∀p ∈ set ps. sp_wf p"
shows "traverse_program (side_rhs_fold_dg acc ps) τ = DG (side_acc_dg acc τ ps) bot"
unfolding side_rhs_fold_dg_def side_acc_dg_def
by (rule traverse_fold_rhs_program_projected_char[OF wf])
text ‹
‹side_rhs_fold_dg›'s accumulator only ever reaches the terminal ‹Answer›,
so its dependency set is independent of the accumulator: the recursion
skeleton over ‹ps› never branches on ‹acc›.
›
lemma dep_program_side_rhs_fold_dg_char:
assumes wf: "∀p ∈ set ps. sp_wf p"
shows "dep_program sigma (side_rhs_fold_dg acc ps) = (⋃p∈set ps. dep_program sigma p)"
unfolding side_rhs_fold_dg_def
by (rule dep_program_fold_rhs_program_projected_char[OF wf])
lemma dep_program_side_rhs_fold_dg_acc_indep:
assumes wf: "∀p ∈ set ps. sp_wf p"
shows "dep_program sigma (side_rhs_fold_dg acc1 ps)
= dep_program sigma (side_rhs_fold_dg acc2 ps)"
by (simp add: dep_program_side_rhs_fold_dg_char[OF wf])
text ‹
The two order-preservation facts the generic fold asks for before it will
carry its contributions' environment-monotonicity: the D/G instance's
projection and its embedding.
›
lemma mono_dg_local: "mono (dg_local :: ('d::order, 'h::order) dg_state ⇒ 'd)"
by (rule monoI) (simp add: less_eq_dg_state_def)
lemma mono_DG_bot:
"mono (λd. DG d bot :: ('d::order, 'h::order_bot) dg_state)"
by (rule monoI) simp
text ‹
Environment-monotonicity of the fold, given every folded program is itself
environment-monotone. The content is
‹traverse_fold_rhs_program_projected_mono›'s; this instance only supplies the two
monotonicity facts above.
›
lemma side_rhs_fold_dg_mono:
assumes wf: "∀p ∈ set ps. sp_wf p"
and prog_mono: "∀p ∈ set ps. ∀s1 s2. s1 ≤ s2 ⟶ traverse_program p s1 ≤ traverse_program p s2"
shows "⋀acc s1 s2. s1 ≤ s2 ⟹
traverse_program (side_rhs_fold_dg acc ps) s1
≤ traverse_program (side_rhs_fold_dg acc ps) s2"
using prog_mono
unfolding side_rhs_fold_dg_def
by (auto intro!: traverse_fold_rhs_program_projected_mono[OF wf mono_dg_local mono_DG_bot])
text ‹
The vendored solver's own @{const mono_deps} precondition only needs the
weaker, order-conditional guarantee @{const mono_tree_deps}, not the full
equality @{const env_indep_deps}; the generic fold already carries it, so
this instance is one line.
›
lemma side_rhs_fold_dg_mono_tree_deps:
assumes wf: "∀p ∈ set ps. sp_wf p"
and prog_mono: "⋀p. p ∈ set ps ⟹ mono_tree_deps (sp_compile p)"
shows "mono_tree_deps (sp_compile (side_rhs_fold_dg acc ps))"
unfolding side_rhs_fold_dg_def
by (rule mono_tree_deps_fold_rhs_program_projected[OF wf prog_mono])
text ‹
The fold's contributions are carried only by the per-contribution Side
nodes; the accumulator flows into the final ‹Answer› (whose own sides are
‹bot›), so the side map is acc-independent --- the same fact
@{thm dep_program_side_rhs_fold_dg_acc_indep} established for dependencies,
mirrored here for sides. This is what lets @{thm side_rhs_fold_dg_mono}'s
proof strategy repeat for @{const sides_of_rhs} without also needing
@{const traverse_rhs}-monotonicity as a hypothesis.
›
lemma sides_of_program_side_rhs_fold_dg_acc_indep:
assumes wf: "∀p ∈ set ps. sp_wf p"
shows "sides_of_program (side_rhs_fold_dg acc1 ps) sigma
= sides_of_program (side_rhs_fold_dg acc2 ps) sigma"
unfolding side_rhs_fold_dg_def
by (rule sides_of_program_fold_rhs_program_projected_acc_indep[OF wf])
text ‹
The declarative twin of \<^const>‹side_acc_dg›: ‹side_rhs_fold_dg›'s contribution
at any one key is a plain fold over each element's own \<^const>‹sides_of_rhs›, seeded at
‹bot› rather than the running local accumulator -- @{thm
sides_of_program_side_rhs_fold_dg_acc_indep} already shows the accumulator never affects a
side read, so unfolding \<^const>‹sp_lift_tree›'s side equation and re-seeding at ‹bot›
after each step is exact, not merely a bound.
›
lemma sides_of_program_side_rhs_fold_dg_char:
fixes ps :: "('x, 'k, ('d::bounded_semilattice_sup_bot, 'h::bounded_semilattice_sup_bot) dg_state,
('d, 'h) dg_state) strategy_program list"
assumes wf: "∀p ∈ set ps. sp_wf p"
shows "sides_of_program (side_rhs_fold_dg acc ps) τ z
= (⨆p←ps. sides_of_program p τ z)"
unfolding side_rhs_fold_dg_def
by (rule sides_of_program_fold_rhs_program_projected_char[OF wf])
lemma side_acc_dg_as_foldr:
"side_acc_dg acc τ ps = acc ⊔ (⨆p←ps. dg_local (traverse_program p τ))"
by (simp add: side_acc_dg_def fold_acc_projected_as_foldr)
text ‹The same equation with the accumulator left as the fold's seed, which is
how a proof that already has a right fold in hand meets it.›
lemma side_acc_dg_as_foldr_seeded:
"side_acc_dg acc τ ps = foldr (λp a. dg_local (traverse_program p τ) ⊔ a) ps acc"
by (simp only: side_acc_dg_as_foldr foldr_join_seed_out)
lemma side_rhs_fold_dg_sides_mono:
assumes wf: "∀p ∈ set ps. sp_wf p"
and prog_sides_mono:
"∀p ∈ set ps. ∀s1 s2. s1 ≤ s2 ⟶ sides_of_program p s1 ≤ sides_of_program p s2"
shows "⋀acc s1 s2. s1 ≤ s2 ⟹
sides_of_program (side_rhs_fold_dg acc ps) s1
≤ sides_of_program (side_rhs_fold_dg acc ps) s2"
using prog_sides_mono
unfolding side_rhs_fold_dg_def
by (auto intro!: sides_of_program_fold_rhs_program_projected_mono[OF wf])
subsection ‹Pushing a projection through a join-fold›
text ‹
A \<^type>‹dg_state› join is componentwise, so either projection commutes with
a join-fold. These are what let a proof that has folded whole states read
off one component afterwards, rather than carrying two folds along.
›
lemma dg_local_foldr_generic:
"dg_local (foldr (λt acc'. h t ⊔ acc') L
(acc::('d::bounded_semilattice_sup_bot, 'h::bounded_semilattice_sup_bot) dg_state))
= foldr (λt acc'. dg_local (h t) ⊔ acc') L (dg_local acc)"
by (induction L) simp_all
lemma dg_global_foldr_generic:
"dg_global (foldr (λt acc'. h t ⊔ acc') L
(acc::('d::bounded_semilattice_sup_bot, 'h::bounded_semilattice_sup_bot) dg_state))
= foldr (λt acc'. dg_global (h t) ⊔ acc') L (dg_global acc)"
by (induction L) simp_all
lemma dg_local_join_list:
"dg_local (⨆t←L. h t :: ('d::bounded_semilattice_sup_bot, 'h::bounded_semilattice_sup_bot) dg_state)
= (⨆t←L. dg_local (h t))"
by (induction L) simp_all
lemma dg_global_join_list:
"dg_global (⨆t←L. h t :: ('d::bounded_semilattice_sup_bot, 'h::bounded_semilattice_sup_bot) dg_state)
= (⨆t←L. dg_global (h t))"
by (induction L) simp_all
lemma foldr_dg_global_sides_char:
fixes L :: "('x, 'k, ('d::bounded_semilattice_sup_bot, 'h::bounded_semilattice_sup_bot) dg_state,
('d, 'h) dg_state) strategy_program list"
shows "(⨆t←L. dg_global (sides_of_program t τ z))
= dg_global (⨆t←L. sides_of_program t τ z)"
by (simp add: dg_global_foldr_generic foldr_join_list[symmetric])
lemma foldr_sides_dg_local_bot:
fixes L :: "('x, 'k, ('d::bounded_semilattice_sup_bot, 'h::bounded_semilattice_sup_bot) dg_state,
('d, 'h) dg_state) strategy_program list"
assumes "⋀t. t ∈ set L ⟹ dg_local (sides_of_program t τ z) = bot"
shows "dg_local (⨆t←L. sides_of_program t τ z) = bot"
using assms
by (induction L) simp_all
lemma DG_sup_bot_left:
"DG (bot::'d::bounded_semilattice_sup_bot) a ⊔ DG bot b = DG bot (a ⊔ b)"
by (simp add: sup_dg_state_def bot_dg_state_def)
subsection ‹What one folded program contributes›
text ‹
Lower bounds, the counterpart of the upper bounds above: every folded program's
answer and contribution is below the fold's, and the accumulator only
grows. A soundness proof reaches one selected contribution through these,
having reached the fold itself through the generator.
All three are the same fact about a join-fold --- ‹foldr_sup_member_le›,
proved once with the rest of that algebra where the fold itself is defined
--- read off the two characterizations above.
›
lemma side_acc_dg_ge_acc:
"acc ≤ side_acc_dg acc σ ts"
by (simp add: side_acc_dg_as_foldr)
lemma dg_local_traverse_le_side_acc_dg:
assumes "p ∈ set ps"
shows "dg_local (traverse_program p σ) ≤ side_acc_dg acc σ ps"
unfolding side_acc_dg_as_foldr
using foldr_sup_member_le[where h = "λp. dg_local (traverse_program p σ)", OF assms]
by (rule le_supI2)
lemma sides_le_side_rhs_fold_dg:
assumes wf: "∀p ∈ set ps. sp_wf p"
and mem: "p ∈ set ps"
shows "sides_of_program p σ k ≤ sides_of_program (side_rhs_fold_dg acc ps) σ k"
unfolding sides_of_program_side_rhs_fold_dg_char[OF wf]
by (rule foldr_sup_member_le[OF mem])
end
end