Repository navigation
Expand file tree
/
Copy pathVCTactic.lean
More file actions
3373 lines (3173 loc) · 160 KB
/
Copy pathVCTactic.lean
File metadata and controls
3373 lines (3173 loc) · 160 KB
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
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- LEGACY: statement target of pre-rebuild theorems; compiles, refuses what
-- it does not implement, gains no consumers; deleted when re-founded.
import LeanModels.Python.VC2
import LeanModels.Python.LoopTactic
/-!
# `py_vcgen` — the VC-generating walker (py_vcgen layer 3)
The tactic that turns the layer-1/2 rules (VC.lean/VC2.lean) into a proof
*surface*: from a `f(args) ==> v` (`CallsTo`) goal — or a `PyTriple` goal
whose precondition is `fun env => env = <literal>` — it bridges to the
whole-function-body triple (`PyTriple.callsTo`) and walks the statement list,
applying the structural rules and discharging every interpreter obligation by
captured symbolic execution, so that only mathematics remains.
Surface:
* `py_vcgen [prog]` / `py_vcgen [prog, aux…]` (extra idents are additional
constants to unfold during captured runs — hand-built `FunctionDefn`
constants of test modules) — walk with no loop clauses: each loop's
invariant and measure are left as *delayed goals* (mvcgen-style): for the
i-th loop in source order, goals `case inv<i>` and `case dec<i>` come
first, presenting the loop's *assigned* variables as a NAMED binder
telescope in the goal context — `total i : Int ⊢ Prop` resp. `⊢ Nat` —
so the clause is written as a bare proposition/measure over the Python
names (`case inv1 => exact 0 ≤ i`, `case dec1 => exact i.toNat`), NOT as
a lambda. (Design note: a function-typed goal `Int → ⋯ → Prop` would be
consumed *positionally* by `exact fun … => …`, silently cross-wiring
binders written in the wrong order — with the telescope in the context
there is no order to get wrong, and a stray lambda is a loud type
error.) Assigning them instantiates the math residuals that mention
them. A `break`-carrying loop additionally gets `case exit<i>` (same
telescope) — the exit-clause request the clause form takes as
`(exit<i> := …)`; without it the loop's exit fact would silently weaken
to the bare invariant.
* `py_vcgen [prog] (inv := fun (x y : Int) => …) (dec := fun (x y : Int) => …)`
— clause form: the i-th `(inv := …) (dec := …)` pair belongs to the i-th
`while` in source order (any label starting with `inv` resp. `dec` works:
`inv1`, `dec1`, …; loops are indexed by a pre-scan, so an `if`-fork that
reaches the same loop twice consumes the same pair twice). Binder names
must be the Python names of variables **assigned in that loop's body** and
present at loop entry — matched BY NAME, in any order (they select the
environment slots, exactly as
`py_loop`); unassigned variables stay pinned to their current symbolic
values, so the invariant refers to them directly (`n` in `tri`, `oa`/`ob`
in `extended_gcd` need no clause mention). A clause for an inner loop is
elaborated *at its loop's program point*, so it may also refer to the
enclosing loops' variables by name. An optional `(exit<i> := fun (x…) => P)`
clause (index = loop number, default 1; binders a subset of that loop's
`inv` binders) states the i-th loop's exit fact explicitly — REQUIRED for
a `break`-carrying loop whose continuation needs more than the bare
invariant: the test-false exit must then imply `P` (an `exit`-tagged
residual) and each `break` site must establish it (with its branch facts
`hif` in scope, which is the point).
The walk (each step is a layer-1/2 rule; nothing is proved by whole-program
execution):
* **straight-line segments** (assignments incl. tuple targets, `augAssign`,
`pass`, expression statements, and a terminating `return`/`break`/
`continue`) are discharged by one *captured* symbolic execution (`py_simp`'s
lemma set at fuel 64, all local `Prop` hypotheses available as rewrites)
spliced through `PyTriple.run_seq` or `PyTriple.of_exec`;
* **`if`** — `PyStmtTriple.ifStmt` with each branch walked under its test
fact (`hif`); the joined midcondition is the disjunction of the branches'
fall-through states, so the walk after the `if` forks per branch
(`PyTriple.or_pre`); a branch whose test fact normalizes to `False` is
closed without touching its body (`raise`-unreachability, the
`rsa_inverse.inverse` pattern);
* **`while`** — `PyStmtTriple.whileLoop` (VC2.lean) instantiated from the
clause pair: the invariant is
`fun env => ∃ x₁ … xₖ tail, env = <shape> ∧ inv x₁ … xₖ` where `<shape>`
pins unassigned slots and `tail` absorbs **environment growth** (variables
first assigned inside the body live behind the symbolic tail via
`Env.lookup_set_self`/`Env.lookup_set_ne`/`Env.set_set` — no hand-unrolled
first iteration, the `rsa_inverse` pain point); the measure reads its
variables back off the environment through `envInt`; the test-value
function `tv` is derived by symbolic evaluation of the test at the shape
(no Miller unification needed). Without an `exit` clause a body `break`
weakens the loop's exit fact to the bare invariant (no negated test);
* **`for`** — `PyStmtTriple.forLoop` (VC2.lean): the while recipe MINUS the
measure. The iterated values are captured before the loop begins, so the
invariant is indexed by the REMAINING elements and termination is
structural. Surface consequences: a `for` consumes an `inv` clause but NO
`dec`, and its `inv` binds the remaining-element list FIRST, then its Int
slots by name — `(inv := fun (rest : List Int) (best : Int) => …)`, or
delayed as `case inv1 => …` with `rest` in the context. The loop TARGET
is bound by the loop itself, once per element, and lives behind the
symbolic tail like any body-created variable. A `break`-carrying `for`
REQUIRES its `exit` clause: the invariant at the empty remainder is not
the exit fact after a `break`, and the while rule's silent weakening is
deliberately not repeated. The walker reads the iterable's captured value
as a boundary/value LIST whose elements are marshalled (`xs.map elt`) or
int literals; `for` over a heap list (H2's live index cursor), over a
`str` or a `range` refuse loudly and want the layer-2 rule by hand, and
a GENERATOR refuses with its own rule named (`PyStmtTriple.forGen`, and
`EvalsIn.genCall` for the call that binds one) — the rules exist, the
walker's invariant grammar does not reach them yet (docs/backlog.md §L3
TAIL LANDED). `for … else` has no rule at all — the interpreter refuses
it;
* **calls** — `x = f(…)` / `(a, b, c) = f(…)` (the call the whole
right-hand side, `PyStmtTriple.assign` ∘ `EvalsTo.call`) and
`return f(…)` (`PyStmtTriple.retExpr` ∘ `EvalsTo.call`, which is the
"any other expression position" that primitive was built for). Both go
through the same front half (`buildCallEvalsTo`): the callee fact is
found among local `CallsTo` hypotheses first — ∀-QUANTIFIED ones
included, which is what makes a recursion IH usable, since the natural
shape of one is `∀ b, f(…, b) ==> …` — then in the `@[py_spec]`
registry; spec preconditions are discharged by `assumption`/`omega`, or
appended as `side` goals. A return-position call to a BUILTIN stays
straight-line, as before.
Residual goals are pure mathematics over named atoms (`py_loop`
presentation: invariant conjuncts split into `hinv1`, `hinv2`, …; the loop
test as `hcont`; branch facts as `hif`; post-loop values primed), tagged
`init`, `preserve`, `dec`, `exit`, `ret`, `err`, `side`. When several
residuals share a tag (two `preserve` goals from split invariant
conjuncts), the first keeps the bare tag and the rest are numbered
(`preserve2`, `preserve3`, …) — `case preserve => …` would otherwise
silently take only the first. Anything the recipes cannot close is
*appended* as a goal, never dropped.
Entry forms: a `CallsTo` goal (`==>`/`⇓`; bridged via
`PyTriple.callsTo_arityOk`, so a body that can fall off the end leaves a
`v = None` residual); a `PyTriple` goal whose precondition reduces to
`fun env => env = <literal>`; or a relational existential
`∃ v, f(args) ==> v ∧ Φ v` — with a raw `Val`-typed binder used literally
(bridged via `PyTriple.exists_callsTo_arityOk`) or a marshalled spec-typed
binder (`∃ v : PyInt, f(args) ==> v ∧ Φ v`, the surface elaboration
putting `ToVal.toVal v` in the result slot; bridged via
`PyTriple.exists_callsTo_toVal_arityOk`). The `_arityOk` bridges are the
F1-defaults general forms: their arity-window side condition computes at
literal modules and closes by `rfl` whether the call supplies every
argument or omits trailing defaulted ones.
v1 restrictions (deliberate): loop clause variables are `Int`-valued and
must exist at loop entry; loop `orelse` is empty (the layer-2 rule's
restriction); `if`-branches are straight-line (terminators allowed; no
nested `if`/`while`/call); calls appear only as the whole right-hand side
of an assignment or the whole returned expression, with a fully literal
environment (not under an enclosing loop's symbolic tail); a loop test may
not read a variable first assigned
inside the loop body, and a `for`'s ITERABLE may not either (its value is
captured once, at loop entry); `Raises` (`==>!`) goals are not walked.
-/
namespace LeanModels.Python
/-! ## Environment-reading helpers (spec-side) -/
/-- Read an `Int`-valued variable off a frame's locals (`0` when absent or
non-`int`) — how a loop measure derived by `py_vcgen` reads its variables
back from the interpreter state (`μ := fun st => dec (envInt st "i") …`).
Reduces by `simp [envInt, Env.lookup]` at literal states. -/
def envInt (st : FrameState) (x : String) : Int :=
match Env.lookup st.locals x with
| some (.int i) => i
| _ => 0
/-- Lookup after `Env.set` at the same key. With `Env.lookup_set_ne` and
`Env.set_set`, this is what keeps a loop-body-created variable (living
behind the invariant's symbolic environment tail) readable and writable
without the tail ever becoming literal. Polymorphic (`Env.lookup`/`Env.set`
are since H1); used at `RVal`. -/
theorem Env.lookup_set_self {α} (e : List (String × α)) (k : String) (v : α) :
Env.lookup (Env.set e k v) k = some v := by
induction e with
| nil => simp [Env.set, Env.lookup]
| cons kv rest ih =>
obtain ⟨kk, kw⟩ := kv
by_cases hk : (kk == k) = true
· simp [Env.set, hk, Env.lookup]
· have hk' : (kk == k) = false := by simpa using hk
simp [Env.set, hk', Env.lookup, ih]
/-- Lookup after `Env.set` at a different key (see `Env.lookup_set_self`). -/
theorem Env.lookup_set_ne {α} (e : List (String × α)) {k k' : String}
(h : (k' == k) = false) (v : α) :
Env.lookup (Env.set e k v) k' = Env.lookup e k' := by
have hne : (k == k') = false := by
simp only [beq_eq_false_iff_ne] at h ⊢
exact fun he => h he.symm
induction e with
| nil => simp [Env.set, Env.lookup, hne]
| cons kv rest ih =>
obtain ⟨kk, kw⟩ := kv
by_cases hk : (kk == k) = true
· have hkv : (kk == k') = false := by
have : kk = k := by simpa using hk
simpa [this] using hne
simp [Env.set, hk, Env.lookup, hne, hkv]
· have hk' : (kk == k) = false := by simpa using hk
simp [Env.set, hk', Env.lookup, ih]
/-- Overwrite after `Env.set` at the same key (see `Env.lookup_set_self`). -/
theorem Env.set_set {α} (e : List (String × α)) (k : String) (v w : α) :
Env.set (Env.set e k v) k w = Env.set e k w := by
induction e with
| nil => simp [Env.set]
| cons kv rest ih =>
obtain ⟨kk, kw⟩ := kv
by_cases hk : (kk == k) = true
· have : kk = k := by simpa using hk
simp [Env.set, this]
· have hk' : (kk == k) = false := by simpa using hk
simp [Env.set, hk', ih]
/-! ## Marshalled-list indexing (spec-side)
The shape every symbolic subscript read leaves in a residual goal: the
interpreter reads a boundary list through `getElem?` and MAPS the
marshalling over the result, defaulting when out of range. Three example
proofs each carried their own copy of this lemma (`arrVal_getElem` in
`bench_bisect`, `bench_statistics`, `sf_bound_rec`, plus two copies of the
`getD`/`getElem` bridge); the content lives here now, and each site is one
instantiation. -/
/-- Mapping over an IN-RANGE `getElem?` and defaulting is the map applied at
the element. Unbounded this is FALSE: out of range the left side is `d` and
the right side is `f` of nothing. -/
theorem map_getElem?_getD {α β : Type} (f : α → β) (d : β) (l : List α)
(n : Nat) (h : n < l.length) :
(Option.map f l[n]?).getD d = f l[n] := by
rw [List.getElem?_eq_getElem h]; rfl
/-- The `getD`/`getElem` bridge at an in-range index, general form (core
states it the other way round). -/
theorem getD_eq_getElem_gen {α : Type} (l : List α) (n : Nat) (d : α)
(h : n < l.length) : l.getD n d = l[n] :=
(List.getElem_eq_getD d).symm
/-- The `getD`/`getElem` bridge on an int list at the `0` default — the
signature the example proofs already call (`getD_eq_getElem a n h`). -/
theorem getD_eq_getElem (l : List Int) (n : Nat) (h : n < l.length) :
l.getD n 0 = l[n] := getD_eq_getElem_gen l n 0 h
/-- The `arrVal_getElem` family at the surface marshalling, `getElem`
form. -/
theorem arrVal_getElem (l : List Int) (n : Nat) (h : n < l.length) :
(Option.map (RVal.thaw ∘ ToVal.toVal) l[n]?).getD RVal.none
= RVal.int l[n] :=
map_getElem?_getD _ _ l n h
/-- Python's negative-index fold, collapsed at a non-negative index. A
conditional REWRITE rather than an arithmetic decision on the `ite`
condition: the side condition `0 ≤ i` is a rewrite hypothesis, so
`captureDischarge`'s `omega` half sees it, and nothing has to decide
`i < 0` as a term. -/
theorem ifNeg_index (i : Int) (n : Nat) (h : 0 ≤ i) :
(if i < 0 then i + (n : Int) else i) = i := if_neg (by omega)
/-- The marshalled boundary-list subscript in the shape the interpreter
actually leaves — Python's negative-index fold still standing as an `ite` —
for an index the loop's own facts put in range. The side condition is
stated over `i` rather than over the folded `Nat`, which is what lets
`captureDischarge`'s `omega` half prove it straight from the invariant
(`0 ≤ i`) and the loop test. -/
theorem arrVal_indexVal (l : List Int) (i : Int)
(h : 0 ≤ i ∧ i < (l.length : Int)) :
(Option.map (RVal.thaw ∘ ToVal.toVal)
l[(if i < 0 then i + (l.length : Int) else i).toNat]?).getD RVal.none
= RVal.int l[i.toNat] := by
obtain ⟨h0, hl⟩ := h
rw [if_neg (by omega)]
exact arrVal_getElem l i.toNat (by omega)
/-- The same, `getD` form (what a loop invariant carrying `a.getD n 0`
needs). -/
theorem arrVal_getD (l : List Int) (n : Nat) (h : n < l.length) :
(Option.map (RVal.thaw ∘ ToVal.toVal) l[n]?).getD RVal.none
= RVal.int (l.getD n 0) := by
rw [arrVal_getElem l n h, getD_eq_getElem l n h]
/-! ## Strings as lists of characters (spec-side)
Every string operation the tier performs is defined THROUGH `String.toList`
— `strCharVals`, the `.str` arm of `indexVal`, `strContains` via
`strFindAux` — so none of Lean's `String.Pos`/UTF-8 machinery is on the
path, and a symbolic string reasons exactly like the `List Char` it wraps.
These are the bridges that say so. They are the string twins of the
`arrVal_getElem` family above: same shape, same in-range side condition,
same discharger.
(Landing L1 of docs/generator-tier-architecture.md. The rest of that memo
is owner-gated; this family is ordinary depth tooling and pays for any
symbolic-string proof, tier or no tier.) -/
/-- `normIndex` at a non-negative in-range index is the index itself.
Python's negative-index fold is the `if` inside it; this is the arm every
in-tier subscript takes. -/
theorem normIndex_of_nonneg (i : Int) (n : Nat) (h0 : 0 ≤ i)
(hlt : i < (n : Int)) : normIndex i n = some i.toNat := by
simp only [normIndex, if_neg (by omega : ¬ i < 0)]
rw [if_pos (by omega)]
/-- A string's Python length is its character-list length. -/
theorem strLength_eq_toList (s : String) : s.length = s.toList.length :=
String.length_toList.symm
/-- **The string subscript**, in the shape the interpreter leaves: reading
`s[i]` at an in-range non-negative `i` is the character list read. The side
condition is stated over `i`, so `captureDischarge`'s `omega` half proves
it from a loop invariant exactly as it does for `arrVal_indexVal`. -/
theorem strIndex_ok (s : String) (i : Int) (h0 : 0 ≤ i)
(hlt : i < (s.length : Int)) :
indexVal (.str s) (.int i)
= .ok (.str (String.singleton (s.toList.getD i.toNat ' '))) := by
simp only [indexVal, asInt, normIndex_of_nonneg i s.length h0 hlt]
/-- `strFindAux` for a ONE-character needle is list membership — the whole
of the membership family, because the guards `gen_moves` writes
(`q in " \nPNBRQK"`, `p in "PNK"`) are single characters against a literal
alphabet. -/
theorem strFindAux_singleton_isSome (l : List Char) (c : Char) :
(strFindAux l [c]).isSome = l.contains c := by
induction l with
| nil => simp [strFindAux]
| cons x xs ih =>
by_cases h : x = c
· simp [strFindAux, h]
· simp [strFindAux, ih, Ne.symm h]
/-- Membership of a single character in a string, as a list `contains`.
The reference enumeration's `inStr` is definitionally this, so the two
sides of a membership guard meet without a case split over the literal. -/
theorem strContains_singleton (s : String) (c : Char) :
strContains s (String.singleton c) = s.toList.contains c := by
rw [strContains, String.toList_singleton, strFindAux_singleton_isSome]
/-- Iterating a string yields its characters as one-character strings, in
the `String.singleton` spelling the subscript also produces (the definition
says `String.ofList [c]`; one spelling downstream is worth the lemma). -/
theorem ofList_singleton (c : Char) : String.ofList [c] = String.singleton c :=
String.toList_injective (by simp)
theorem strCharVals_eq_map (s : String) :
strCharVals s = s.toList.map (fun c => RVal.str (String.singleton c)) := by
simp only [strCharVals, ofList_singleton]
/-- Equality of two one-character strings is equality of the characters. -/
theorem valEq_singleton (a b : Char) :
valEq (.str (String.singleton a)) (.str (String.singleton b))
= .ok (a == b) := by
have hinj : (String.singleton a == String.singleton b) = (a == b) := by
by_cases h : a = b
· simp [h]
· have hne : String.singleton a ≠ String.singleton b := fun hs =>
h (by simpa using congrArg String.toList hs)
rw [beq_eq_false_iff_ne.mpr hne, beq_eq_false_iff_ne.mpr h]
show Res.ok (String.singleton a == String.singleton b) = Res.ok (a == b)
rw [hinj]
/-! ## Run splicing -/
/-- Append two decided runs: statements that fell through (`.next`) followed
by any decided run of the rest — the fuel bookkeeping is internal
(`execStmt_mono`/`execStmts_mono` at a summed witness). This is the engine
of `PyTriple.run_seq`. -/
private theorem execStmts_append_run {m : Module} {l₁ l₂ : List Stmt}
{st st' : FrameState} {r : Run FrameState RFlow}
(h1 : ∃ f, execStmts m f st l₁ = .ok st' .next)
(h2 : ∃ f, execStmts m f st' l₂ = r) (hr : r ≠ .timeout) :
∃ f, execStmts m f st (l₁ ++ l₂) = r := by
induction l₁ generalizing st with
| nil =>
obtain ⟨f, hf⟩ := h1
match f, hf with
| f + 1, hf =>
simp only [execStmts] at hf
have henv : st = st' := (Run.ok.inj hf).1
subst henv
simpa using h2
| cons s l₁' ih =>
obtain ⟨f, hf⟩ := h1
match f, hf with
| f + 1, hf =>
simp only [execStmts, Run.bind_eq_ok] at hf
obtain ⟨st₁, flow, hstep, htail⟩ := hf
cases flow with
| next =>
obtain ⟨g, hg⟩ := ih ⟨f, htail⟩
refine ⟨f + g + 1, ?_⟩
simp only [List.cons_append, execStmts]
rw [execStmt_mono hstep (by simp) (f + g) (by omega)]
simpa using execStmts_mono hg hr (f + g) (by omega)
| ret v => simp at htail
| brk => simp at htail
| cont => simp at htail
/-- Splice one captured straight-line run in front of a triple for the rest:
`pre` ran to `st'` with `.next` at some concrete fuel, so the triple for
`pre ++ rest` from `E` follows from the triple for `rest` from `st'`. This
is how `py_vcgen` discharges a straight-line segment before a control
point. -/
theorem PyTriple.run_seq {m : Module} {E E' : FrameState} {f : Nat}
{pre rest : List Stmt} {Q : PyPost}
(h1 : execStmts m f E pre = .ok E' .next)
(h2 : PyTriple m (fun st => st = E') rest Q) :
PyTriple m (fun st => st = E) (pre ++ rest) Q := by
rintro st rfl
obtain ⟨r, t, hr, hrun⟩ := h2.exec rfl
obtain ⟨g, hg⟩ := execStmts_append_run ⟨f, h1⟩ ⟨t, hrun t (Nat.le_refl t)⟩
(PyPost.holds_ne_timeout hr)
refine ⟨g, fun F hF => ?_⟩
rw [execStmts_mono hg (PyPost.holds_ne_timeout hr) F hF]
exact hr
/-! ## Precondition-normalization rules
`py_vcgen` constructs preconditions in a small grammar —
`fun env => env = E`, `∃ x, P`, `P ∧ H`, `P ∨ P`, `False` — and strips them
down to the `env = E` form the walker consumes with the rules below (each
is one `apply` + `intro`). -/
universe u
/-- Strip an existential from the precondition. -/
theorem PyTriple.exists_pre {α : Sort u} {m : Module} {ss : List Stmt}
{Q : PyPost} {P : α → FrameState → Prop}
(h : ∀ x, PyTriple m (P x) ss Q) :
PyTriple m (fun st => ∃ x, P x st) ss Q :=
fun st hP => hP.elim fun x hx => h x st hx
/-- Hoist an existential out of a conjunction in the precondition. -/
theorem PyTriple.exists_and_pre {α : Sort u} {m : Module} {ss : List Stmt}
{Q : PyPost} {P : α → FrameState → Prop} {H : FrameState → Prop}
(h : ∀ x, PyTriple m (fun st => P x st ∧ H st) ss Q) :
PyTriple m (fun st => (∃ x, P x st) ∧ H st) ss Q :=
fun st hP => hP.1.elim fun x hx => h x st ⟨hx, hP.2⟩
/-- Reassociate a conjunction in the precondition. -/
theorem PyTriple.and_assoc_pre {m : Module} {ss : List Stmt} {Q : PyPost}
{P H₁ H₂ : FrameState → Prop}
(h : PyTriple m (fun st => P st ∧ (H₁ st ∧ H₂ st)) ss Q) :
PyTriple m (fun st => (P st ∧ H₁ st) ∧ H₂ st) ss Q :=
fun st hP => h st ⟨hP.1.1, hP.1.2, hP.2⟩
/-- Move the pure part of an `st = E ∧ H st` precondition into hypothesis
position (instantiated at `E`) — after this the precondition is walkable. -/
theorem PyTriple.eq_and_pre {m : Module} {ss : List Stmt} {Q : PyPost}
{E : FrameState} {H : FrameState → Prop}
(h : H E → PyTriple m (fun st => st = E) ss Q) :
PyTriple m (fun st => st = E ∧ H st) ss Q :=
fun st hP => h (hP.1 ▸ hP.2) st hP.1
/-- Split a disjunctive precondition (an `if`-join). -/
theorem PyTriple.or_pre {m : Module} {ss : List Stmt} {Q : PyPost}
{P₁ P₂ : FrameState → Prop} (h1 : PyTriple m P₁ ss Q) (h2 : PyTriple m P₂ ss Q) :
PyTriple m (fun st => P₁ st ∨ P₂ st) ss Q :=
fun st hP => hP.elim (h1 st) (h2 st)
/-- A dead join point (both `if`-branches escaped, or an unreachable
branch: the `raise`-unreachability pattern). -/
theorem PyTriple.false_pre {m : Module} {ss : List Stmt} {Q : PyPost} :
PyTriple m (fun _ => False) ss Q :=
fun _ hP => hP.elim
/-! ## The relational bridge
`PyTriple.callsTo` (VC2.lean) requires the returned value fixed up front;
relational specs (`extended_gcd`'s Bezout coefficients) only know it
*exists*. This bridge closes the gap: prove the whole-body triple with an
arbitrary predicate `Φ` on the returned value (a `PyTriple` goal `py_vcgen`
walks directly), get `∃ v, CallsTo … v ∧ Φ v`. -/
/-- Triple → arrow, relational form, general-arity (F1 defaults): the
hypothesis is the arity window `arityOk f.params args.size` — trailing
defaulted arguments may be omitted at the call site, `mkCallEnv` fills
them. This is the bridge `py_vcgen` applies for a `∃ v, …` goal (the side
condition computes to `true = true` at literal modules and closes by
`rfl`). `PyTriple.exists_callsTo` is the exact-arity corollary. -/
theorem PyTriple.exists_callsTo_arityOk {m : Module} {fname : String}
{f : FunctionDefn} {args : Array Val} {Φ : Val → Prop}
(hf : findFunction m fname = some f)
(hargsOk : f.argsOk = true) (hlocalsOk : f.localsOk = true)
(hgen : f.isGenerator = false)
(harity : arityOk f.params args.size = true)
(h : PyTriple m
(fun st => st = ⟨initWorld m, mkCallEnv f.params (RVal.thawArgs args)⟩)
f.body.toList
{ next := fun _ => False
ret := fun rv _ => ∃ v, rv = RVal.thaw v ∧ Φ v }) :
∃ v, CallsTo m fname args v ∧ Φ v := by
obtain ⟨r, t, hr, hrun⟩ := h.exec rfl
have hrt := hrun t (Nat.le_refl t)
cases r with
| ok st' flow =>
cases flow with
| next => exact hr.elim
| ret rv =>
obtain ⟨v, rfl, hΦ⟩ := hr
refine ⟨v, ⟨t + 1, ?_⟩, hΦ⟩
unfold callFunction
rw [callIn]
simp [hf, hargsOk, hlocalsOk, hgen, harity, hrt, RVal.freezeB_thaw]
| brk => exact hr.elim
| cont => exact hr.elim
| exn st' e => exact hr.elim
| timeout => exact (PyPost.holds_ne_timeout hr rfl).elim
| unsupported msg => exact hr.elim
/-- Triple → arrow, relational form: a whole-body triple whose `ret` arm
asserts `Φ` of the (frozen) returned value (and whose `next` arm is
`False` — the body always returns) yields an existential `CallsTo`. -/
theorem PyTriple.exists_callsTo {m : Module} {fname : String}
{f : FunctionDefn} {args : Array Val} {Φ : Val → Prop}
(hf : findFunction m fname = some f)
(hargsOk : f.argsOk = true) (hlocalsOk : f.localsOk = true)
(hgen : f.isGenerator = false)
(harity : args.size = f.params.size)
(h : PyTriple m
(fun st => st = ⟨initWorld m, mkCallEnv f.params (RVal.thawArgs args)⟩)
f.body.toList
{ next := fun _ => False
ret := fun rv _ => ∃ v, rv = RVal.thaw v ∧ Φ v }) :
∃ v, CallsTo m fname args v ∧ Φ v :=
PyTriple.exists_callsTo_arityOk hf hargsOk hlocalsOk hgen
(by rw [harity]; exact arityOk_full f.params) h
/-- The relational bridge, marshalled form: `exists_callsTo` with the
existential ranging over a *spec type* `α` (`PyInt`, `PyBool`, …) rather
than raw `Val`. This is the shape the surface notation actually produces —
`∃ v : PyInt, f(args) ==> v ∧ Φ v` elaborates the returned-value slot as
`ToVal.toVal v`, not as the bound variable itself — so it is the form
`py_vcgen`'s existential entry bridges through for a non-`Val` binder. The
triple's `ret` arm asserts the returned RUNTIME value is the thawed
marshalling of some `x : α` satisfying `Φ` (the walker discharges the
`rv = RVal.thaw (ToVal.toVal x)` equation by unification at each captured
return site, leaving `Φ x` as the `ret` residual). -/
theorem PyTriple.exists_callsTo_toVal_arityOk {α : Type} [ToVal α]
{m : Module} {fname : String} {f : FunctionDefn} {args : Array Val}
{Φ : α → Prop}
(hf : findFunction m fname = some f)
(hargsOk : f.argsOk = true) (hlocalsOk : f.localsOk = true)
(hgen : f.isGenerator = false)
(harity : arityOk f.params args.size = true)
(h : PyTriple m
(fun st => st = ⟨initWorld m, mkCallEnv f.params (RVal.thawArgs args)⟩)
f.body.toList
{ next := fun _ => False
ret := fun rv _ => ∃ x : α, rv = RVal.thaw (ToVal.toVal x) ∧ Φ x }) :
∃ x : α, CallsTo m fname args (ToVal.toVal x) ∧ Φ x := by
obtain ⟨v, hv, x, rfl, hΦ⟩ :=
PyTriple.exists_callsTo_arityOk (Φ := fun v => ∃ x : α, v = ToVal.toVal x ∧ Φ x)
hf hargsOk hlocalsOk hgen harity
(h.consequence (fun _ hp => hp)
{ next := fun _ hfalse => hfalse.elim
ret := fun rv st hrv =>
hrv.elim fun x hx => ⟨ToVal.toVal x, hx.1, x, rfl, hx.2⟩
brk := fun _ hfalse => hfalse.elim
cont := fun _ hfalse => hfalse.elim
err := fun _ _ hfalse => hfalse.elim })
exact ⟨x, hv, hΦ⟩
@[inherit_doc PyTriple.exists_callsTo_toVal_arityOk]
theorem PyTriple.exists_callsTo_toVal {α : Type} [ToVal α] {m : Module}
{fname : String} {f : FunctionDefn} {args : Array Val} {Φ : α → Prop}
(hf : findFunction m fname = some f)
(hargsOk : f.argsOk = true) (hlocalsOk : f.localsOk = true)
(hgen : f.isGenerator = false)
(harity : args.size = f.params.size)
(h : PyTriple m
(fun st => st = ⟨initWorld m, mkCallEnv f.params (RVal.thawArgs args)⟩)
f.body.toList
{ next := fun _ => False
ret := fun rv _ => ∃ x : α, rv = RVal.thaw (ToVal.toVal x) ∧ Φ x }) :
∃ x : α, CallsTo m fname args (ToVal.toVal x) ∧ Φ x :=
PyTriple.exists_callsTo_toVal_arityOk hf hargsOk hlocalsOk hgen
(by rw [harity]; exact arityOk_full f.params) h
/-! ## The walker (meta level) -/
namespace PyVCGen
open Lean Elab Tactic Meta PyLoopTactic
/-- Fuel for every captured symbolic run (a depth bound, so a generous
constant covers everything straight-line). -/
def fuelK : Nat := 64
/-- Interpreter definitions unfolded during captured symbolic execution
(the `py_simp` list plus `envInt`). -/
def interpUnfolds : List Name :=
[``execStmts, ``execStmt, ``evalExpr, ``evalExprs, ``evalBoolChain,
``evalCompareChain, ``evalDictItems, ``findFunction, ``mkCallEnv, ``arityOk,
``defaultBindings, ``Env.lookup, ``Env.set,
``Const.toVal, ``Const.toRVal, ``truthy, ``truthyH, ``asInt, ``RVal.isNone,
``RVal.thaw, ``RVal.thawList, ``RVal.thawArgs, ``RVal.freeze,
``RVal.freezeList, ``RVal.freezeB, ``RVal.freezeListB,
``valEq, ``valEqList,
``intCmp, ``strCmp, ``evalCompareOp, ``evalCompareOpH, ``evalBinOp,
``strFormat,
``evalUnaryOp, ``evalUnaryOpH,
``lenVal, ``lenValH, ``sortedVal, ``asIntList, ``normIndex, ``indexVal,
``indexValH,
``hashableKey, ``hashableKeyList, ``keyEq, ``keyEqList, ``RVal.unhashName,
``keyHasInstanceRef, ``keyHasInstanceRefList, ``keyRefusal,
``dictFind, ``dictStore, ``dictBuild, ``heapIndex, ``heapStore, ``heapLen,
``heapContains, ``heapContainsScan, ``valContains, ``heapGet, ``heapAppend,
``heapPop, ``heapInsert,
``heapAttrStore, ``findClass, ``findClassAux, ``classAt, ``getClass?,
``attrReadPlan, ``attrReadResult, ``attrCallPlan, ``execAttrCall,
``endsWithUU, ``dunderShaped, ``hasExtraDunder,
``findNamedTuple, ``findNamedTupleAux, ``fieldIndex, ``ntupleProtoName,
``ntupleMethodName, ``ntupleAttr, ``ntupleCallPlan,
``sliceVal, ``strCallPlan,
``RVal.refFree, ``RVal.refFreeList,
``Val.listFree, ``Val.listFreeList, ``Val.listFreeArgs,
``Heap.get?, ``Heap.update, ``danglingMsg,
``targetNames, ``bindAll, ``assignTo, ``assignToH,
``unpackSeq, ``unpackStoreH,
``foldExtremum, ``extremumVal, ``extremumValH, ``sortedValH,
``absVal, ``intCastVal, ``strOfVal, ``strOfValH, ``ordVal, ``chrVal, ``isBuiltinName,
``enumStart, ``enumFrame, ``countArgs, ``genPlan, ``genBreak, ``genContinue,
``moduleGlobals, ``moduleInit, ``globalsFold, ``globalsStep, ``lookupG,
``globalsDirty, ``Stmt.g1Dirty, ``Stmt.g1Binds, ``Stmt.g1BindsList,
``Stmt.g1Stores, ``Stmt.g1StoresList, ``Expr.g1Primary,
``Expr.g1TargetStores, ``Expr.g1TargetStoresList,
``targetBindsG, ``targetBindsListG, ``benignImportBinds, ``isModuleDunder,
``resolvedG, ``resolvedGAux, ``targetNamesG, ``evalGlobalExpr, ``evalGlobalExprs,
``evalGlobalDictItems, ``globalFuel,
``sumArgs, ``sumFold, ``rangeLen, ``rangeValsAux, ``rangeVals, ``rangeMake,
``seqBudget, ``shiftBudget, ``tupleRepeat, ``seqSlice, ``seqSliceElems,
``g1DivGate, ``g1ExecCandidate, ``g1HeapPure, ``g1HeapPureList,
``Stmt.defFree, ``Stmt.defFreeList, ``topLevelDefFree,
``initFoldLive, ``initFoldStep, ``initExecStmt, ``initItemsLoop,
``initBodyStmts, ``flushInitLocals, ``initBindable, ``initExecFuel,
``stmtIsClockImport, ``moduleClockOk, ``clockRecvOk, ``isClockCall,
``envInt]
/-- Rewrite lemmas added to captured symbolic execution: the branch-collapse
of loop tests, the symbolic-tail environment lemmas, and the `sorted`
bridges (`sortInts`/`insertLe` stay frozen — symbolic goals keep the
compact `sortInts data` handle; these two lemmas let the run step through a
marshalled `sorted(data)` call and its downstream `len`/index bounds). -/
def interpLemmas : List Name :=
[``ite_ok_bool, ``Env.lookup_set_self, ``Env.lookup_set_ne, ``Env.set_set,
``asIntList_map_int, ``asIntList_map_toVal, ``asIntList_map_thaw_comp,
``sortInts_length,
-- the marshalled SUBSCRIPT read, conditional on the index being in
-- range: `captureDischarge` proves that side condition from the loop
-- invariant and the test (§the arithmetic discharger)
``arrVal_getElem, ``arrVal_getD, ``arrVal_indexVal, ``ifNeg_index,
-- the string twins (L1): a symbolic board reads as its character list
``strIndex_ok, ``strContains_singleton, ``valEq_singleton]
/-- The truthiness-normalization set: turns `truthy <captured value> = true`
facts into the clean arithmetic propositions residual goals should show. -/
def normLemmas : List Name :=
[``decide_eq_true_eq, ``decide_eq_false_iff_not, ``Bool.not_eq_true',
``Bool.not_eq_false', ``Bool.not_eq_eq_eq_not, ``Bool.not_true,
``Bool.not_false, ``beq_iff_eq, ``beq_eq_false_iff_ne, ``bne_iff_ne,
``Decidable.not_not, ``Res.ok.injEq]
/-- Definitions unfolded by the normalization set. -/
def normUnfolds : List Name := [``truthy, ``envInt, ``Env.lookup]
/-- Presentation lemmas for residual goals (marshalling peeled, `Val`
injectivity applied). -/
def presentLemmas : List Name :=
[``toVal_int, ``toVal_nat, ``toVal_bool, ``toVal_str, ``toVal_val,
``toVal_list, ``toVal_int_triple, ``Val.int.injEq, ``Val.bool.injEq,
``Val.str.injEq, ``Val.tuple.injEq, ``Val.list.injEq,
``RVal.int.injEq, ``RVal.bool.injEq, ``RVal.str.injEq,
``RVal.tuple.injEq, ``RVal.listV.injEq]
/-- The three simp contexts of a `py_vcgen` run: `exec` (symbolic
execution: default set + interpreter unfolds + program literal), `norm`
(truthiness normalization only), `present` (residual-goal cleanup). -/
structure SimpPack where
exec : Simp.Context
norm : Simp.Context
present : Simp.Context
procs : Simp.SimprocsArray
private def addAll (thms : SimpTheorems) (unfolds lemmas : List Name) :
MetaM SimpTheorems := do
let mut thms := thms
for n in unfolds do
thms ← thms.addDeclToUnfold n
for n in lemmas do
thms ← thms.addConst n
return thms
/-- Prove a proposition with `omega` from the accessible facts, or fail.
`Omega.omega` proves `False` from a fact list — it is not a goal-directed
entry point — so this mirrors what the `omega` TACTIC does around it:
`falseOrByContra` first (which puts the negated goal into the context),
then hand it every local hypothesis. Calling it on the goal directly looks
like it works and silently proves nothing, because the goal never enters
the constraint set. -/
def omegaProve (goal : Lean.Expr) : MetaM (Option Lean.Expr) := do
let g ← mkFreshExprSyntheticOpaqueMVar goal
try
let some g' ← g.mvarId!.falseOrByContra | return none
g'.withContext do
Lean.Elab.Tactic.Omega.omega (← getLocalHyps).toList g'
pure (some (← instantiateMVars g))
catch _ => pure none
/-- Is this side condition worth handing to `omega`? A cheap syntactic
gate: comparisons, equalities and their negations. Everything else fails in
`omega` anyway, and the gate keeps the common case (a `beq` guard already
settled by the default discharger) off the arithmetic path. -/
private def isArithSide (e : Lean.Expr) : Bool :=
match e.getAppFn with
| .const n _ =>
n == ``LT.lt || n == ``LE.le || n == ``GT.gt || n == ``GE.ge ||
n == ``Eq || n == ``Ne || n == ``Not || n == ``And
| _ => false
/-- Does this proposition come from the INTERPRETER's own index plumbing —
a subscript's range check or Python's negative-index fold — rather than
from the program's arithmetic?
Keyed on a LENGTH mention, deliberately: `Int.toNat` alone also matches a
loop MEASURE (`rsa_inverse`'s `(b - 1).toNat`), and deciding those atoms
sends `omega` down every comparison in the run — measured, it blew that
proof's simp step budget. The negative-index fold, whose condition `i < 0`
mentions no length, is handled by the `ifNeg_index` rewrite instead, whose
side condition goes to the discharger.
The gate matters: `decideArith` must not start deciding a loop's ordinary
comparisons, because those are the walker's residuals and every existing
proof is written against their shape. Keying on `Int.toNat` / `List.length`
/ `Array.size` (with a literal `0` bound for the fold) keeps the simprocs
where the subscript lives and out of `nested_flow`'s and `rsa_inverse`'s
arithmetic. -/
private def isIndexGuard (e : Lean.Expr) : Bool :=
let mentions (n : Name) : Bool := (e.find? (·.isConstOf n)).isSome
mentions ``List.length || mentions ``Array.size
/-- The discharger captured runs use for conditional rewrites: simp's own
default first, then `omega` over the accessible hypotheses.
The arithmetic half is what lets a SUBSCRIPT reduce. A loop body's
`xs[i]` captures as `(Option.map … xs[i.toNat]?).getD RVal.none`, whose
rewrite (`arrVal_getElem`) is conditional on `i.toNat < xs.length` — a fact
the loop invariant and the loop test do supply, but only to an arithmetic
prover. Without this the capture stops dead at the subscript, which is what
kept `sf_bound_loop`/`sf_bound_rec` on hand proofs. `omega` sees the local
context, so the invariant conjuncts (`0 ≤ i`, `i ≤ xs.length`) and the test
fact (`i < n`, with `n = xs.length` already substituted by the walker) are
exactly its premises. -/
def captureDischarge : Simp.Discharge := fun e => do
match ← Simp.dischargeDefault? e with
| some prf => return some prf
| none =>
let e ← instantiateMVars e
if e.hasExprMVar || !isArithSide e then return none
match ← omegaProve e with
| some prf => return some prf
| none => return none
/-- Decide an arithmetic atom from the accessible facts. When `omega`
proves the atom (or its negation) from the local context it collapses to
`True` (or `False`).
This is the half a DISCHARGER cannot do. Simp asks a discharger only about
the hypotheses of conditional rewrites; the interpreter's own guards are
`ite` CONDITIONS — a subscript's range check
`if (0 ≤ k) ∧ (k < len) then some … else none`, the negative-index fold
`if i < 0 then i + len else i` — and those are decided by simplifying the
condition, which needs arithmetic in the simp set itself. Without this a
captured `xs[i]` stops at the range check even though the loop invariant
says the index is in range, which is what kept `sf_bound_loop` and
`sf_bound_rec` on hand proofs. -/
def decideArith : Simp.Simproc := fun e => do
unless isArithSide e && isIndexGuard e do return .continue
let e ← instantiateMVars e
if e.hasExprMVar then return .continue
let tryOmega (goal : Lean.Expr) : SimpM (Option Lean.Expr) :=
liftM (omegaProve goal)
match ← tryOmega e with
| some prf =>
return .done { expr := Lean.mkConst ``True, proof? := ← mkAppM ``eq_true #[prf] }
| none =>
match ← tryOmega (Lean.mkApp (Lean.mkConst ``Not) e) with
| some prf =>
return .done { expr := Lean.mkConst ``False, proof? := ← mkAppM ``eq_false #[prf] }
| none => return .continue
-- one declaration per keyed shape: an unascribed `_ < _` elaborates at the
-- DEFAULT numeric type (`Nat`) and then never matches an `Int` comparison,
-- which is exactly the way this silently does nothing
simproc_decl decideArithLtInt ((_ : Int) < (_ : Int)) := decideArith
simproc_decl decideArithLeInt ((_ : Int) ≤ (_ : Int)) := decideArith
simproc_decl decideArithLtNat ((_ : Nat) < (_ : Nat)) := decideArith
simproc_decl decideArithLeNat ((_ : Nat) ≤ (_ : Nat)) := decideArith
simproc_decl decideArithAnd (_ ∧ _) := decideArith
/-- Build the simp contexts, `progs` the program constants to unfold. -/
def mkPack (progs : List Name) : MetaM SimpPack := do
let congr ← getSimpCongrTheorems
let execThms ← addAll (← getSimpTheorems)
(interpUnfolds ++ progs) interpLemmas
let normThms ← addAll (← getSimpTheorems) normUnfolds
(normLemmas ++ interpLemmas)
let presentThms ← addAll (← getSimpTheorems) (normUnfolds ++ progs)
(normLemmas ++ presentLemmas ++ interpLemmas)
-- the arithmetic simprocs ride with every captured run (see `decideArith`)
let mut procs ← Simp.getSimprocs
procs ← procs.add ``decideArithLtInt (post := true)
procs ← procs.add ``decideArithLeInt (post := true)
procs ← procs.add ``decideArithLtNat (post := true)
procs ← procs.add ``decideArithLeNat (post := true)
procs ← procs.add ``decideArithAnd (post := true)
return {
-- captured runs discharge more side conditions than they used to (the
-- arithmetic half), so the rewrite cascade behind one run is longer;
-- the default 100000-step budget is a walker-internal limit, not a
-- user-visible one, and a run that needs more is not a run that is
-- looping (measured on `rsa_inverse`, the longest one in the gallery)
exec := ← Simp.mkContext { maxSteps := 1000000 } #[execThms] congr
norm := ← Simp.mkContext {} #[normThms] congr
present := ← Simp.mkContext {} #[presentThms] congr
procs := #[procs] }
/-- All accessible `Prop`-typed hypotheses of the current local context —
supplied to every captured run as rewrite rules (this is how a loop-test
fact `hcont : b ≠ 0` decides the `ZeroDivisionError` guard of `%`, and a
branch fact `hif` decides a nested test). -/
def currentFacts : MetaM (Array FVarId) := do
let mut out := #[]
for d in ← getLCtx do
if d.isImplementationDetail then continue
if (← isProp d.type) then
out := out.push d.fvarId
return out
/-- Extend a simp context with hypothesis facts. -/
def addFacts (ctx : Simp.Context) (facts : Array FVarId) :
MetaM Simp.Context := do
let mut thms := ctx.simpTheorems
for fv in facts do
try
thms ← thms.addTheorem (.fvar fv) (mkFVar fv)
catch _ => pure ()
return ctx.setSimpTheorems thms
/-- Symbolically execute `e` (an interpreter term) with the local `Prop`
hypotheses as extra rewrites; returns the normal form and a proof `e = nf`. -/
def captureRun (pack : SimpPack) (e : Lean.Expr) :
MetaM (Lean.Expr × Lean.Expr) := do
let ctx ← addFacts pack.exec (← currentFacts)
let (r, _) ← Meta.simp e ctx pack.procs (discharge? := some captureDischarge)
let prf ← match r.proof? with
| some p =>
-- Pin the equation's syntactic form: simp folds definitional steps
-- away, so the proof's inferred type may show an unfolded lhs.
mkExpectedTypeHint p (← mkEq e r.expr)
| none => mkEqRefl e
return (r.expr, prf)
/-- Prove a PINNED equation `run = .ok E v` where `E` is the walker's known
mid-state: the captured normal form's out-state may have been rewritten by
local hypotheses (state is data since H1 — a post-loop `hcont : b' = 0`
rewrites the state's `RVal.int b'`), so instead of reusing the captured
proof, the pinned equation is proved as a GOAL — the same fact set then
normalizes BOTH sides to the drifted form and `rfl` closes. -/
def provePinned (pack : SimpPack) (ty : Lean.Expr) : MetaM Lean.Expr := do
let g ← mkFreshExprMVar ty .syntheticOpaque
let ctx ← addFacts pack.exec (← currentFacts)
let r? ← try
Prod.fst <$> Meta.simpGoal g.mvarId! ctx pack.procs (some captureDischarge)
catch _ => pure (some (#[], g.mvarId!))
match r? with
| none => return g
| some (_, g') =>
let closed ← g'.withContext do
let t ← instantiateMVars (← g'.getType)
match t.eq? with
| some (_, lhs, _) =>
try
let gs ← g'.apply (← mkEqRefl lhs)
pure gs.isEmpty
catch _ => pure false
| none => pure false
unless closed do
throwError "py_vcgen: could not pin a captured run at the walker's mid-state:{indentExpr ty}"
return g
/-- Apply a constant to optional arguments, filling `none` positions (and
missing trailing hypotheses) with fresh metavariables — the `apply`-ready
partial-application builder (`mkAppOptM` refuses unassigned mvars). -/
def appOpt (n : Name) (args : Array (Option Lean.Expr)) : MetaM Lean.Expr := do
let f ← mkConstWithFreshMVarLevels n
let mut e := f
let mut ty ← inferType f
for a in args do
ty ← whnf ty
let .forallE _ d b _ := ty
| throwError "py_vcgen: internal — too many arguments for {n}"
match a with
| some x =>
unless ← isDefEq d (← inferType x) do
throwError "py_vcgen: internal — argument type mismatch for {n}: expected{indentExpr d}\ngot{indentExpr (← inferType x)}"
e := mkApp e x
ty := b.instantiate1 x
| none =>
let mv ← mkFreshExprMVar d
e := mkApp e mv
ty := b.instantiate1 mv
return e
/-- Normalize a proposition with the truthiness set; returns the normal form
and (when changed) a proof `p = p'`. -/
def normProp (pack : SimpPack) (p : Lean.Expr) :
MetaM (Lean.Expr × Option Lean.Expr) := do
let (r, _) ← Meta.simp p pack.norm pack.procs
return (r.expr, r.proof?)
/-! ### Literal introspection -/
/-- `Array α` literal → its `List` literal. -/
def arrToList (a : Lean.Expr) : MetaM Lean.Expr := do
let a ← whnfR a
if a.isAppOfArity ``Array.mk 2 then return a.getArg! 1
else if a.isAppOfArity ``List.toArray 2 then return a.getArg! 1
else throwError "py_vcgen: expected a literal array:{indentExpr a}"
/-- Split a `List` literal into its element expressions and its terminator
(the trailing `List.nil` or any non-`cons` tail); sees through
`Array.toList` of literal arrays. -/
partial def parseListLit (e : Lean.Expr) : MetaM (Array Lean.Expr × Lean.Expr) := do
let e ← whnfR e
if e.isAppOfArity ``List.cons 3 then
let (rest, tail) ← parseListLit (e.getArg! 2)
return (#[e.getArg! 1] ++ rest, tail)
else if e.isAppOfArity ``Array.toList 2 then
parseListLit (← arrToList (e.getArg! 1))
else
return (#[], e)
/-- Is this expression the `List.nil` terminator? -/
def isNilTail (e : Lean.Expr) : Bool := e.isAppOfArity ``List.nil 1
/-- Rebuild a `List` literal from elements and a tail expression. -/
def mkListLit' (ty : Lean.Expr) (elems : Array Lean.Expr) (tail : Lean.Expr) :
Lean.Expr :=
elems.foldr (fun e acc =>
mkApp3 (Lean.mkConst ``List.cons [Level.zero]) ty e acc) tail
/-- The `String × RVal` pair type of environment entries (locals bind
runtime values since H1). -/
def entryTy : Lean.Expr :=
mkApp2 (Lean.mkConst ``Prod [Level.zero, Level.zero]) (Lean.mkConst ``String)
(Lean.mkConst ``RVal)
/-- The `List (String × RVal)` locals type. -/
def envTy : Lean.Expr := mkApp (Lean.mkConst ``List [Level.zero]) entryTy
/-- The `FrameState` type (the walker's state space since H1). -/
def stateTy : Lean.Expr := Lean.mkConst ``FrameState
/-- Build a `FrameState` literal from a world and a locals expression. -/
def mkState (w env : Lean.Expr) : Lean.Expr :=
mkApp2 (Lean.mkConst ``FrameState.mk) w env
/-- A frame-state shape: the pinned world (KNOWN in the stage-1 geometry —
the `initWorld m` term, never symbolic), then the locals as named literal
cons-entries, a spine of `Env.set` writes (outermost first — variables
that grew into the symbolic region), over a symbolic tail. The `sets`
region is where loop-body-created variables live (`Env.set (… tl …) "q"
v`), readable/writable through `Env.lookup_set_self`/`Env.set_set` without
the tail ever becoming literal. -/
structure EnvShape where
world : Lean.Expr
entries : Array (String × Lean.Expr)
sets : Array (String × Lean.Expr) := #[]
tail : Lean.Expr
/-- Parse a `FrameState` expression into its pinned world, its literal
cons-prefix, its `Env.set` tail spine, and its symbolic tail. -/
def parseEnvShape (e : Lean.Expr) : MetaM EnvShape := do
let e ← whnfR e
unless e.isAppOfArity ``FrameState.mk 2 do
throwError "py_vcgen: state is not a literal `⟨world, locals⟩`:{indentExpr e}"
-- reducible whnf: a `{ st with locals := … }` update (the `for` rule's
-- body precondition) leaves the world as a projection of a constructor
let world ← whnfR (e.getArg! 0)
let (elems, tail) ← parseListLit (e.getArg! 1)
let mut entries := #[]
for el in elems do
let el ← whnfR el
unless el.isAppOfArity ``Prod.mk 4 do
throwError "py_vcgen: environment entry is not a literal pair:{indentExpr el}"
let nameE ← whnfR (el.getArg! 2)
let .lit (.strVal name) := nameE
| throwError "py_vcgen: environment name is not a string literal:{indentExpr nameE}"
entries := entries.push (name, el.getArg! 3)
let mut sets := #[]
let mut base ← whnfR tail
for _ in [0:64] do
if base.isAppOfArity ``Env.set 4 then
let .lit (.strVal k) ← whnfR (base.getArg! 2)
| throwError "py_vcgen: `Env.set` key is not a string literal:{indentExpr base}"
sets := sets.push (k, base.getArg! 3)