1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
import RoleFlow.Density
import RoleFlow.Infer

/-! # The inverse program (`docs/roleflow.tex`, `sec:inverse`)

The schedule is a forward closure (`fire`) over the recipes of the lines (`Prog.xrecs`):
starting from the weights and the outputs, it fires a recipe whose sources are all known, until
none is left, and emits each fired recipe as a line of the inverse program. A renaming maps the variables of `P` to those of the inverse
program. Firing keeps the renaming sort-preserving (`Good`) and, on a trace of `P`, the values
of the inverse program equal to those of the trace (`Agree`), since the recipes hold
(`xrecs_holds`). At the end the known set is
closed under the recipes (`fire_closed`), so by `Recov` at `Ret` it contains every input
(`Typed.known_xrecs`, `prop:schedule`).

`Prog.inverse` is the inverse program `P†`, a program of the same signature
(`Prog.inverse_wf`). **Concordance** (`Prog.inverse_sem`, `thm:conc`):
`⟦P†⟧_θ ∘ ⟦P⟧_θ = id`. With the right law, `⟦P⟧_θ` is bijective with inverse `⟦P†⟧_θ`
(`Prog.bijective`, `Prog.sem_inverse`, `Prog.equiv`), and `⟦P†⟧_θ` pushes `μ_O` back to `μ_I`
with the negated log-weight (`Prog.inverse_density`, `thm:invdensity`), which needs only the left
law.

Concordance needs less than the typing: `Prog.Invertible` asks only that the schedule reaches
every input, and the recipes hold under any roles. A program that is invertible but not typed
(say `ret x + b, x + b`) gets a left inverse, without a density. -/

open MeasureTheory

namespace RoleFlow

variable {S : Type} {Sg : Sig S}

/-- **Existence of the schedule**, the recoverability half: under `Recov`, every variable, in
scope or introduced by the lines, is known from the weights and the returned variables. -/
theorem Typed.known_xrecs {nw : ℕ} {O Γ : List S} {Λ : Finset ℕ} {R : List (XRecipe Sg)}
    {D : List (Finset ℕ)} {ls : List (Line Sg)} {xs : List ℕ} {ρ : List Role}
    (hT : Typed Sg O Γ Λ D ls xs ρ) (hrec : Recov nw Γ.length Λ R D)
    (hX : ∀ x ∈ xrecs Γ.length ls ρ, x ∈ R) :
    ∀ v < Γ.length + (ls.map fun l => l.bs.length).sum, Known R ({w | w < nw} ∪ ↑xs.toFinset) v := by
  refine Typed.induction_recov (nw := nw)
    (motive := fun Γ _ ls xs _ => ∀ v < Γ.length + (ls.map fun l => l.bs.length).sum,
      Known R ({w | w < nw} ∪ ↑xs.toFinset) v)
    ?_ ?_ ?_ hT hrec hX
  · intro Γ Λ D xs hR _ hxs _ v hv
    simp only [List.map_nil, List.sum_nil, Nat.add_zero] at hv
    exact (hR.2 v hv).2.mono
      (Set.union_subset_union_right _ (by rw [hxs]; exact Finset.coe_subset.2 (hR.2 v hv).1))
  · intro _ _ _ l ls _ _ _ _ ih v hv
    exact ih v (by simp only [List.map_cons, List.sum_cons] at hv; simp; omega)
  · intro _ _ _ l ls _ _ _ _ _ _ _ _ _ ih v hv
    exact ih v (by simp only [List.map_cons, List.sum_cons] at hv; simp; omega)

/-! ## The forward closure -/

/-- The first element of a list with a property, and the list without it. -/
def pickFirst {α : Type*} (p : α → Bool) : List α → Option (α × List α)
  | [] => none
  | a :: as => if p a then some (a, as) else (pickFirst p as).map fun r => (r.1, a :: r.2)

theorem pickFirst_none {α : Type*} {p : α → Bool} {as : List α} (h : pickFirst p as = none) :
    ∀ a ∈ as, p a = false := by
  induction as with
  | nil => simp
  | cons b as ih =>
    simp only [pickFirst] at h
    split_ifs at h with hb
    simp only [Option.map_eq_none_iff] at h
    intro a ha
    rcases List.mem_cons.1 ha with rfl | ha
    · simpa using hb
    · exact ih h a ha

theorem pickFirst_some {α : Type*} {p : α → Bool} {as bs : List α} {a : α}
    (h : pickFirst p as = some (a, bs)) :
    p a = true ∧ a ∈ as ∧ bs.length + 1 = as.length ∧ (∀ b ∈ bs, b ∈ as) ∧
      ∀ b ∈ as, b = a ∨ b ∈ bs := by
  induction as generalizing bs with
  | nil => simp [pickFirst] at h
  | cons c as ih =>
    simp only [pickFirst] at h
    split_ifs at h with hc
    · cases h
      exact ⟨hc, List.mem_cons_self .., rfl, fun b hb => List.mem_cons_of_mem _ hb,
        fun b hb => List.mem_cons.1 hb⟩
    · obtain ⟨⟨a', bs'⟩, h', e⟩ := Option.map_eq_some_iff.1 h
      cases e
      obtain ⟨h₁, h₂, h₃, h₄, h₅⟩ := ih h'
      refine ⟨h₁, List.mem_cons_of_mem _ h₂, by simp [h₃], fun b hb => ?_, fun b hb => ?_⟩
      · rcases List.mem_cons.1 hb with rfl | hb
        · exact List.mem_cons_self ..
        · exact List.mem_cons_of_mem _ (h₄ b hb)
      · rcases List.mem_cons.1 hb with rfl | hb
        · exact Or.inr (List.mem_cons_self ..)
        · exact (h₅ b hb).imp id (List.mem_cons_of_mem _)

/-- The renaming after firing a recipe with targets `tgt` at the next free position `m`. -/
def renStep (tgt : List ℕ) (m : ℕ) (ren : ℕ → ℕ) (v : ℕ) : ℕ :=
  if v ∈ tgt then m + tgt.idxOf v else ren v

/-- **The schedule**, as a forward closure: fire a recipe of `X` whose sources are all known,
emit it as a line at the next free position `m`, and continue; `ren` renames the variables of
`P` into the inverse program and `K` is the hash set of known variables. -/
def fire : ℕ → List (XRecipe Sg) → ℕ → (ℕ → ℕ) → Std.HashSet ℕ →
    List (Line Sg) × (ℕ → ℕ) × Std.HashSet ℕ
  | 0, _, _, ren, K => ([], ren, K)
  | f + 1, X, m, ren, K =>
    match pickFirst (fun x => x.line.args.all K.contains) X with
    | none => ([], ren, K)
    | some (x, X') =>
      let r := fire f X' (m + x.line.bs.length) (renStep x.tgt m ren) (insertAll K x.tgt)
      (⟨x.line.op, x.line.args.map ren⟩ :: r.1, r.2)

theorem fire_mono : ∀ (f : ℕ) (X : List (XRecipe Sg)) (m : ℕ) (ren : ℕ → ℕ) (K : Std.HashSet ℕ),
    ∀ v ∈ K, v ∈ (fire f X m ren K).2.2
  | 0, _, _, _, _ => fun _ h => h
  | f + 1, X, m, ren, K => by
    simp only [fire]
    split
    · exact fun _ h => h
    · exact fun v h => fire_mono f _ _ _ _ v (mem_insertAll.2 (.inr h))

/-- After enough rounds, the known set is closed under the recipes. -/
theorem fire_closed : ∀ (f : ℕ) (X : List (XRecipe Sg)) (m : ℕ) (ren : ℕ → ℕ) (K : Std.HashSet ℕ),
    X.length ≤ f → ∀ x ∈ X, (∀ a ∈ x.line.args, a ∈ (fire f X m ren K).2.2) →
      ∀ t ∈ x.tgt, t ∈ (fire f X m ren K).2.2
  | 0, X, _, _, _, hX, x, hx => by simp_all
  | f + 1, X, m, ren, K, hX, x, hx => by
    simp only [fire]
    split
    · next h =>
      intro hargs
      have := pickFirst_none h x hx
      simp only [List.all_eq_false] at this
      obtain ⟨a, ha, hn⟩ := this
      exact absurd (hargs a ha) (by simpa using hn)
    · next y X' h =>
      obtain ⟨_, _, hlen, _, hsplit⟩ := pickFirst_some h
      rcases hsplit x hx with rfl | hx'
      · intro _ t ht
        exact fire_mono _ _ _ _ _ _ (mem_insertAll.2 (.inl ht))
      · exact fire_closed f X' _ _ _ (by omega) x hx'

/-- The inverse program's known set contains everything `Known` from its start. -/
theorem known_fire {f : ℕ} {X : List (XRecipe Sg)} {m : ℕ} {ren : ℕ → ℕ} {K : Std.HashSet ℕ}
    (hf : X.length ≤ f) {B : Set ℕ} (hB : ∀ v ∈ B, v ∈ K) {v : ℕ}
    (hv : Known X B v) : v ∈ (fire f X m ren K).2.2 := by
  induction hv with
  | base hv => exact fire_mono _ _ _ _ _ _ (hB _ hv)
  | fire hx _ hv ih => exact fire_closed f X m ren K hf _ hx ih _ hv

/-- The converse of `known_fire`: everything the schedule adds is `Known` from its start. -/
theorem fire_known {X₀ : List (XRecipe Sg)} {B : Set ℕ} :
    ∀ (f : ℕ) (X : List (XRecipe Sg)) (m : ℕ) (ren : ℕ → ℕ) (K : Std.HashSet ℕ),
      (∀ x ∈ X, x ∈ X₀) → (∀ v ∈ K, Known X₀ B v) → ∀ v ∈ (fire f X m ren K).2.2, Known X₀ B v
  | 0, _, _, _, _, _, hK => hK
  | f + 1, X, m, ren, K, hX, hK => by
    simp only [fire]
    split
    · exact hK
    · next x X' h =>
      obtain ⟨hp, hx, _, hsub, _⟩ := pickFirst_some h
      have hargs : ∀ a ∈ x.line.args, a ∈ K := by simpa using hp
      refine fire_known f X' _ _ _ (fun y hy => hX y (hsub y hy)) fun v hv => ?_
      rcases mem_insertAll.1 hv with hv | hv
      · exact .fire (hX x hx) (fun w hw => hK w (hargs w hw)) hv
      · exact hK v hv

/-! ## Sorts and values along the schedule -/

/-- The renaming `ren` takes the known variables of `Γ` to positions of `Γ'` of the same sort. -/
def Good (Γ Γ' : List S) (ren : ℕ → ℕ) (K : Std.HashSet ℕ) : Prop :=
  ∀ v ∈ K, v < Γ.length ∧ Γ'[ren v]? = Γ[v]?

theorem Good.args {Γ Γ' : List S} {ren : ℕ → ℕ} {K : Std.HashSet ℕ} (hG : Good Γ Γ' ren K)
    {xs : List ℕ} {as : List S} (h : Args Γ xs as) (hK : ∀ x ∈ xs, x ∈ K) :
    Args Γ' (xs.map ren) as := by
  induction h with
  | nil => exact List.Forall₂.nil
  | cons h₁ _ ih =>
    refine List.Forall₂.cons ?_ (ih fun x hx => hK x (List.mem_cons_of_mem _ hx))
    rw [(hG _ (hK _ (List.mem_cons_self ..))).2]; exact h₁

theorem lt_of_good {Γ Γ' : List S} {ren : ℕ → ℕ} {K : Std.HashSet ℕ} (hG : Good Γ Γ' ren K) {v : ℕ}
    (hv : v ∈ K) : ren v < Γ'.length := by
  have := (hG v hv).2
  rw [List.getElem?_eq_getElem (hG v hv).1] at this
  exact (List.getElem?_eq_some_iff.1 this).1

theorem Good.step {Γ Γ' : List S} {ren : ℕ → ℕ} {K : Std.HashSet ℕ} (hG : Good Γ Γ' ren K)
    {tgt : List ℕ} {bs : List S} (h : Args Γ tgt bs) :
    Good Γ (Γ' ++ bs) (renStep tgt Γ'.length ren) (insertAll K tgt) := by
  intro v hv
  by_cases ht : v ∈ tgt
  · have hi := List.idxOf_lt_length_of_mem ht
    have hi' : tgt.idxOf v < bs.length := h.length_eq ▸ hi
    refine ⟨h.lt ht, ?_⟩
    rw [renStep, if_pos ht, List.getElem?_append_right (by omega), Nat.add_sub_cancel_left,
      List.getElem?_eq_getElem hi']
    have := h.getElem? _ hi
    rw [List.getElem_idxOf hi] at this
    exact this.symm
  · have hK : v ∈ K := (mem_insertAll.1 hv).resolve_left ht
    rw [renStep, if_neg ht, List.getElem?_append_left (lt_of_good hG hK)]
    exact hG v hK

section Agree
variable [Sorts S]

/-- On known variables, the value `u` of the inverse program agrees with the trace `t`. -/
def Agree {Γ Γ' : List S} (t : Val Γ) (u : Val Γ') (ren : ℕ → ℕ) (K : Std.HashSet ℕ) : Prop :=
  ∀ v ∈ K, ∀ (a : S) (h : Γ[v]? = some a) (h' : Γ'[ren v]? = some a), u.get (ren v) h' = t.get v h

theorem Agree.pick {Γ Γ' : List S} {t : Val Γ} {u : Val Γ'} {ren : ℕ → ℕ} {K : Std.HashSet ℕ}
    (hA : Agree t u ren K) {xs : List ℕ} {as : List S} (h : Args Γ xs as)
    (h' : Args Γ' (xs.map ren) as) (hK : ∀ x ∈ xs, x ∈ K) : u.pick _ h' = t.pick xs h := by
  ext j a hj
  rw [Val.get_pick, Val.get_pick]
  have key : ∀ (i k : ℕ), k ∈ xs → i = ren k → ∀ (hi : Γ'[i]? = some a) (hk : Γ[k]? = some a),
      u.get i hi = t.get k hk := by
    rintro _ k hk rfl hi hk'; exact hA k (hK k hk) a hk' hi
  exact key _ _ (List.getElem_mem _) (List.getElem_map ..) _ _

theorem Agree.step {Γ Γ' : List S} {t : Val Γ} {u : Val Γ'} {ren : ℕ → ℕ} {K : Std.HashSet ℕ}
    (hA : Agree t u ren K) (hG : Good Γ Γ' ren K) {tgt : List ℕ} {bs : List S}
    (h : Args Γ tgt bs) : Agree t (u.append (t.pick tgt h)) (renStep tgt Γ'.length ren)
      (insertAll K tgt) := by
  intro v hv a hva hva'
  by_cases ht : v ∈ tgt
  · have hi := List.idxOf_lt_length_of_mem ht
    have e : renStep tgt Γ'.length ren v = Γ'.length + tgt.idxOf v := by rw [renStep, if_pos ht]
    rw [Val.get_congr _ e _ (e ▸ hva'), Val.get_append_right, Val.get_pick]
    exact Val.get_congr _ (List.getElem_idxOf hi) _ _
  · have hK : v ∈ K := (mem_insertAll.1 hv).resolve_left ht
    have e : renStep tgt Γ'.length ren v = ren v := by rw [renStep, if_neg ht]
    have hlt := lt_of_good hG hK
    rw [Val.get_congr _ e _ (e ▸ hva'), Val.get_append_left _ _ hlt]
    exact hA v hK a hva _

end Agree

/-- The lines emitted by the schedule are well formed, and the renaming stays good. -/
theorem fire_wf {Γ : List S} {X₀ : List (XRecipe Sg)}
    (hX : ∀ x ∈ X₀, Args Γ x.line.args x.line.as ∧ Args Γ x.tgt x.line.bs) :
    ∀ (f : ℕ) (X : List (XRecipe Sg)), (∀ x ∈ X, x ∈ X₀) → ∀ (Γ' : List S) (m : ℕ),
      Γ'.length = m → ∀ (ren : ℕ → ℕ) (K : Std.HashSet ℕ), Good Γ Γ' ren K →
      LinesWF Γ' (fire f X m ren K).1 ∧
        Good Γ (ctxAfter Γ' (fire f X m ren K).1) (fire f X m ren K).2.1 (fire f X m ren K).2.2
  | 0, _, _, _, _, _, _, _, hG => ⟨trivial, hG⟩
  | f + 1, X, hX', Γ', m, hm, ren, K, hG => by
    subst hm
    simp only [fire]
    split
    · exact ⟨trivial, hG⟩
    · next x X' h =>
      obtain ⟨hp, hx, _, hsub, _⟩ := pickFirst_some h
      have hK : ∀ a ∈ x.line.args, a ∈ K := by simpa using hp
      have hwf := hX x (hX' x hx)
      have ih := fire_wf hX f X' (fun y hy => hX' y (hsub y hy)) (Γ' ++ x.line.bs) (Γ'.length + x.line.bs.length) (by simp)
        (renStep x.tgt Γ'.length ren) (insertAll K x.tgt) (hG.step hwf.2)
      exact ⟨⟨hG.args hwf.1 hK, ih.1⟩, ih.2⟩

section FireAgree
variable [Sorts S] (M : Model Sg)

/-- On a trace on which the recipes hold, the inverse program reproduces the trace on every
known variable. -/
theorem fire_agree {Γ : List S} (t : Val Γ) {X₀ : List (XRecipe Sg)}
    (hX : ∀ x ∈ X₀, Args Γ x.line.args x.line.as ∧ Args Γ x.tgt x.line.bs)
    (hH : ∀ x ∈ X₀, ∀ (h₁ : Args Γ x.line.args x.line.as) (h₂ : Args Γ x.tgt x.line.bs),
      M.sem x.line.op (t.pick _ h₁) = t.pick _ h₂) :
    ∀ (f : ℕ) (X : List (XRecipe Sg)), (∀ x ∈ X, x ∈ X₀) → ∀ (Γ' : List S) (m : ℕ),
      Γ'.length = m → ∀ (ren : ℕ → ℕ) (K : Std.HashSet ℕ), Good Γ Γ' ren K →
      ∀ (hwf : LinesWF Γ' (fire f X m ren K).1) (u : Val Γ'), Agree t u ren K →
      Agree t (trace M _ hwf u) (fire f X m ren K).2.1 (fire f X m ren K).2.2
  | 0, _, _, _, _, _, _, _, _, _, _, hA => hA
  | f + 1, X, hX', Γ', m, hm, ren, K, hG, hwf, u, hA => by
    subst hm
    generalize hr : fire (f + 1) X Γ'.length ren K = r at hwf ⊢
    rcases hp : pickFirst (fun x => x.line.args.all K.contains) X with _ | ⟨x, X'⟩
    · simp only [fire, hp] at hr; subst hr; exact hA
    · simp only [fire, hp] at hr; subst hr
      obtain ⟨hp, hx, _, hsub, _⟩ := pickFirst_some hp
      have hK : ∀ a ∈ x.line.args, a ∈ K := by simpa using hp
      have hx₀ := hX x (hX' x hx)
      have hstep : stepLine M ⟨x.line.op, x.line.args.map ren⟩ hwf.1 u =
          u.append (t.pick x.tgt hx₀.2) := by
        rw [stepLine]
        congr 1
        rw [hA.pick hx₀.1 _ hK]
        exact hH x (hX' x hx) _ _
      change Agree t (trace M _ hwf.2 (stepLine M ⟨x.line.op, x.line.args.map ren⟩ hwf.1 u)) _ _
      rw [hstep]
      exact fire_agree t hX hH f X' (fun y hy => hX' y (hsub y hy)) (Γ' ++ x.line.bs) (Γ'.length + x.line.bs.length) (by simp)
        (renStep x.tgt Γ'.length ren) (insertAll K x.tgt) (hG.step hx₀.2) hwf.2
        (u.append (t.pick x.tgt hx₀.2)) (hA.step hG hx₀.2)

end FireAgree

/-! ## The inverse program -/

namespace Prog

/-- The schedule of `P`: the forward closure from the weights and the outputs. The outputs
get the positions after the weights, so the renaming starts as `renStep P.ret P.nw id`. -/
def schedule (P : Prog Sg) (ρ : List Role) : List (Line Sg) × (ℕ → ℕ) × Std.HashSet ℕ :=
  fire (P.xrecs ρ).length (P.xrecs ρ) (P.nw + P.outs.length) (renStep P.ret P.nw id)
    (insertAll (insertAll ∅ (List.range P.nw)) P.ret)

/-- **The inverse program** `P† = (Θ, O, e†)`: it fires the schedule and returns the inputs. -/
def inverse (P : Prog Sg) (ρ : List Role) : Prog Sg where
  wts := P.wts
  ins := P.outs
  outs := P.ins
  lines := (P.schedule ρ).1
  ret := (List.range' P.nw P.ins.length).map (P.schedule ρ).2.1

variable {P : Prog Sg} {ρ : List Role}

theorem length_ctx₀_le (P : Prog Sg) : P.ctx₀.length ≤ P.ctx.length := by
  obtain ⟨Δ, e⟩ := ctxAfter_append P.ctx₀ P.lines
  show _ ≤ (ctxAfter P.ctx₀ P.lines).length; rw [e]; simp

/-- The weights are known at the start, at their own positions. -/
theorem good_wts (P : Prog Sg) : Good P.ctx P.wts id (insertAll ∅ (List.range P.nw)) :=
    fun v hv => by
  have hlen := P.length_ctx₀_le
  have hv : v < P.wts.length := by simpa [mem_insertAll] using hv
  refine ⟨by simp at hlen; omega, ?_⟩
  rw [id, getElem?_ctxAfter _ (by simp; omega), List.getElem?_append_left hv]

theorem good_init (h : P.WF) :
    Good P.ctx (P.wts ++ P.outs) (renStep P.ret P.nw id)
      (insertAll (insertAll ∅ (List.range P.nw)) P.ret) :=
  P.good_wts.step h.2

theorem inputs_known (hT : P.Typed ρ) : ∀ v < P.ctx₀.length, v ∈ (P.schedule ρ).2.2 := by
  intro v hv
  have hk := hT.known_xrecs (P.recov_init _) (fun _ hx => hx) v (Nat.lt_add_right _ hv)
  exact known_fire le_rfl (fun w hw => by simpa [mem_insertAll, or_comm] using hw) hk

theorem args_inputs : Args P.ctx (List.range' P.nw P.ins.length) P.ins :=
  (Args.range' P.wts P.ins).ctxAfter P.lines

theorem inputs_mem (hT : P.Typed ρ) : ∀ x ∈ List.range' P.nw P.ins.length,
    x ∈ (P.schedule ρ).2.2 := fun x hx =>
  P.inputs_known hT x (by simp only [List.mem_range'_1, Prog.nw] at hx; simp; omega)

/-- `P` is invertible under the roles `ρ`: it is well formed and the schedule reaches every
input. Decidable, and implied by the typing (`Typed.invertible`). -/
def Invertible (P : Prog Sg) (ρ : List Role) : Prop :=
  P.WF ∧ ∀ x ∈ List.range' P.nw P.ins.length, x ∈ (P.schedule ρ).2.2

instance [DecidableEq S] (P : Prog Sg) (ρ : List Role) : Decidable (P.Invertible ρ) :=
  inferInstanceAs (Decidable (_ ∧ _))

theorem Typed.invertible (hT : P.Typed ρ) : P.Invertible ρ := ⟨hT.wf, inputs_mem hT⟩

/-- **Invertibility is `Known`**: the schedule reaches exactly the variables that are `Known`
from the weights and the returned variables (`known_fire`, `fire_known`). -/
theorem invertible_iff_known : P.Invertible ρ ↔ P.WF ∧ ∀ x ∈ List.range' P.nw P.ins.length,
    Known (P.xrecs ρ) ({w | w < P.nw} ∪ ↑P.ret.toFinset) x := by
  have hB : ∀ v, v ∈ ({w | w < P.nw} ∪ ↑P.ret.toFinset : Set ℕ) ↔
      v ∈ insertAll (insertAll ∅ (List.range P.nw)) P.ret := fun v => by
    simp [mem_insertAll, or_comm]
  refine and_congr_right fun _ => forall₂_congr fun x _ => ⟨fun h => ?_, fun h => ?_⟩
  · exact fire_known _ _ _ _ _ (fun _ hx => hx) (fun v hv => .base ((hB v).2 hv)) x h
  · exact known_fire le_rfl (fun v hv => (hB v).1 hv) h

/-- **The inverse program is well formed** (a program over the same signature). -/
theorem Invertible.inverse_wf (hI : P.Invertible ρ) : (P.inverse ρ).WF := by
  have hwf := fire_wf (wf_xrecs (ρ := ρ) hI.1) (P.xrecs ρ).length (P.xrecs ρ) (fun _ hx => hx) _
    (P.nw + P.outs.length)
    (by simp) _ _ (good_init hI.1)
  exact ⟨hwf.1, hwf.2.args args_inputs hI.2⟩

theorem inverse_wf (hT : P.Typed ρ) : (P.inverse ρ).WF := hT.invertible.inverse_wf

section Sem
variable [Sorts S] (M : Model Sg)

/-- At the start, the weights of the inverse program agree with the trace. -/
theorem agree_wts (h : P.WF) (θ : Val P.wts) (x : Val P.ins) :
    Agree (P.tr M h θ x) θ id (insertAll ∅ (List.range P.nw)) := fun v hv a ha ha' => by
  have hv : v < P.wts.length := by simpa [mem_insertAll] using hv
  show θ.get v ha' = (trace M P.lines h.1 (θ.append x)).get v ha
  rw [get_trace M _ _ _ _ (by simp; omega), Val.get_append_left _ _ hv]

/-- **Concordance** (`thm:conc`): the inverse program undoes the program. -/
theorem Invertible.inverse_sem (hI : P.Invertible ρ) (θ : Val P.wts) (x : Val P.ins) :
    (P.inverse ρ).sem M hI.inverse_wf θ (P.sem M hI.1 θ x) = x := by
  have h := hI.1
  set t := P.tr M h θ x
  have hH : ∀ y ∈ P.xrecs ρ, ∀ (h₁ : Args P.ctx y.line.args y.line.as)
      (h₂ : Args P.ctx y.tgt y.line.bs), M.sem y.line.op (t.pick _ h₁) = t.pick _ h₂ :=
    xrecs_holds M h.1 ρ rfl (θ.append x)
  have hA : Agree t (trace M (P.schedule ρ).1 hI.inverse_wf.1 (θ.append (t.pick P.ret h.2)))
      (P.schedule ρ).2.1 (P.schedule ρ).2.2 :=
    fire_agree M t (wf_xrecs (ρ := ρ) h) hH (P.xrecs ρ).length (P.xrecs ρ) (fun _ hx => hx) _
      (P.nw + P.outs.length) (by simp) _ _ (good_init h) hI.inverse_wf.1 _
      ((agree_wts M h θ x).step P.good_wts h.2)
  calc (P.inverse ρ).sem M hI.inverse_wf θ (P.sem M h θ x)
      = (trace M (P.schedule ρ).1 hI.inverse_wf.1 (θ.append (t.pick P.ret h.2))).pick
          ((List.range' P.nw P.ins.length).map (P.schedule ρ).2.1) hI.inverse_wf.2 := rfl
    _ = t.pick _ args_inputs := hA.pick args_inputs _ hI.2
    _ = x := (pick_trace M P.lines h.1 (θ.append x) (Args.range' P.wts P.ins) _).trans
      (Val.pick_range'_append θ x)

theorem inverse_sem (hT : P.Typed ρ) (θ : Val P.wts) (x : Val P.ins) :
    (P.inverse ρ).sem M (inverse_wf hT) θ (P.sem M hT.wf θ x) = x :=
  hT.invertible.inverse_sem M θ x

/-- The inverse program is measurable at fixed weights. -/
theorem Invertible.measurable_inverse (hI : P.Invertible ρ) (θ : Val P.wts) :
    Measurable ((P.inverse ρ).sem M hI.inverse_wf θ) :=
  ((P.inverse ρ).measurable_sem M hI.inverse_wf).comp (measurable_const.prodMk measurable_id)

theorem measurable_inverse (hT : P.Typed ρ) (θ : Val P.wts) :
    Measurable ((P.inverse ρ).sem M (inverse_wf hT) θ) :=
  hT.invertible.measurable_inverse M θ

/-- **Density of the inverse** (`thm:invdensity`): `⟦P†⟧_θ` pushes `μ_O` back to
`μ_I` with the negated log-weight at the recomputed inputs. Only the left law is used. -/
theorem inverse_density (hT : P.Typed ρ) (θ : Val P.wts) :
    Cov (vol P.outs) (vol P.ins) ((P.inverse ρ).sem M (inverse_wf hT) θ)
      fun y => expw (-P.logw M hT.wf ρ θ ((P.inverse ρ).sem M (inverse_wf hT) θ y)) :=
  Cov.inv (measurable_inverse M hT θ)
    ((P.measurable_logw M hT.wf ρ).comp (measurable_const.prodMk measurable_id))
    (inverse_sem M hT θ) (P.density M hT θ)

/-- **Model density** (`cor:modeldensity`): `e^{L_θ}` is the density of `(⟦P†⟧_θ)_* μ_O` with
respect to `μ_I`, by the density theorem and the left inverse. Here and not in `Density.lean`,
since it mentions `P†`. -/
theorem map_inverse (hT : P.Typed ρ) (θ : Val P.wts) :
    (vol P.outs).map ((P.inverse ρ).sem M (inverse_wf hT) θ) =
      (vol P.ins).withDensity fun x => expw (P.logw M hT.wf ρ θ x) :=
  Cov.map_eq (measurable_inverse M hT θ) (inverse_sem M hT θ) (P.density M hT θ)

variable {M}

/-- With the right law, the inverse program is also a right inverse. -/
theorem sem_inverse (hM : M.RightLaw) (hT : P.Typed ρ) (θ : Val P.wts) (y : Val P.outs) :
    P.sem M hT.wf θ ((P.inverse ρ).sem M (inverse_wf hT) θ y) = y := by
  obtain ⟨x, rfl⟩ := P.surjective M hM hT θ y
  rw [inverse_sem M hT]

/-- **Bijectivity** (`thm:bij`): with the right law, `⟦P⟧_θ` is a bijection. -/
theorem bijective (hM : M.RightLaw) (hT : P.Typed ρ) (θ : Val P.wts) :
    Function.Bijective (P.sem M hT.wf θ) :=
  ⟨Function.LeftInverse.injective (inverse_sem M hT θ), P.surjective M hM hT θ⟩

/-- `⟦P⟧_θ` as a measurable equivalence, with inverse `⟦P†⟧_θ`. -/
def equiv (hM : M.RightLaw) (hT : P.Typed ρ) (θ : Val P.wts) : Val P.ins ≃ᵐ Val P.outs where
  toFun := P.sem M hT.wf θ
  invFun := (P.inverse ρ).sem M (inverse_wf hT) θ
  left_inv := inverse_sem M hT θ
  right_inv := sem_inverse hM hT θ
  measurable_toFun := (P.measurable_sem M hT.wf).comp (measurable_const.prodMk measurable_id)
  measurable_invFun := measurable_inverse M hT θ

end Sem

end Prog

end RoleFlow