-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathInflate.lean
More file actions
1644 lines (1515 loc) · 84.6 KB
/
Copy pathInflate.lean
File metadata and controls
1644 lines (1515 loc) · 84.6 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
import ZipCommon.Spec.BitReaderInvariant
import Zip.Spec.Huffman
import Zip.Native.CopyWithin
import Zip.Native.ExtendWithin
/-!
Pure Lean DEFLATE decompressor (RFC 1951).
Supports all three block types:
- Type 0: Stored (uncompressed)
- Type 1: Fixed Huffman codes
- Type 2: Dynamic Huffman codes
This is a reference implementation prioritizing correctness over speed.
-/
namespace Zip.Native
open ZipCommon (BitReader)
/-- Allocation-light `BitReader.readBits`: thread the cursor `(pos, bitOff)` as
unboxed `Nat`s and build the `BitReader` struct **once** at the end, instead
of allocating a fresh `BitReader` (+ `Except`/pair) on every bit as the
`readBit`-looping `BitReader.readBits` does. Same per-bit byte reads, but a
whole `n`-bit field now costs O(1) heap allocations instead of O(n). Proven
equal to `BitReader.readBits` (`readBitsFast_eq`); used on the hot
extra-bits path in `decodeHuffmanFast`. -/
def readBitsFast (br : BitReader) (n : Nat) : Except String (UInt32 × BitReader) :=
go br.pos br.bitOff 0 0 n
where
go (pos bitOff : Nat) (acc : UInt32) (shift : Nat) :
Nat → Except String (UInt32 × BitReader)
| 0 => .ok (acc, { br with pos := pos, bitOff := bitOff })
| k + 1 =>
if h : pos ≥ br.data.size then .error "BitReader: unexpected end of input"
else
let bit : UInt32 := (((br.data[pos]'(by omega)).toUInt32 >>> bitOff.toUInt32) &&& 1)
if bitOff + 1 ≥ 8 then
go (pos + 1) 0 (acc ||| (bit <<< shift.toUInt32)) (shift + 1) k
else
go pos (bitOff + 1) (acc ||| (bit <<< shift.toUInt32)) (shift + 1) k
/-- A Huffman tree for decoding DEFLATE symbols.
Leaf holds a symbol value; Node branches on 0 (left) vs 1 (right). -/
inductive HuffTree where
| leaf (symbol : UInt16)
| node (zero : HuffTree) (one : HuffTree)
| empty
namespace HuffTree
/-- Insert a symbol into the tree at the given code/length. -/
def insert (tree : HuffTree) (code : UInt32) (len : Nat) (symbol : UInt16) : HuffTree :=
go tree len
where
go (t : HuffTree) : Nat → HuffTree
| 0 => .leaf symbol
| n + 1 =>
let bit := (code >>> n.toUInt32) &&& 1
match t with
| .empty =>
if bit == 0 then .node (go .empty n) .empty
else .node .empty (go .empty n)
| .node z o =>
if bit == 0 then .node (go z n) o
else .node z (go o n)
| .leaf s => .leaf s -- shouldn't happen in valid data
/-- Build a Huffman tree by sequentially inserting symbols from an array of
code lengths, starting from index `start`. Returns the tree and updated
nextCode array. Used by `fromLengths` for the final insertion pass. -/
def insertLoop (lengths : Array UInt8) (nextCode : Array UInt32)
(start : Nat) (tree : HuffTree) : HuffTree × Array UInt32 :=
if h : start < lengths.size then
let len := lengths[start]
if len > 0 then
if hlen : len.toNat < nextCode.size then
let c := nextCode[len.toNat]
let tree' := tree.insert c len.toNat start.toUInt16
let nextCode' := nextCode.set! len.toNat (c + 1)
insertLoop lengths nextCode' (start + 1) tree'
else
insertLoop lengths nextCode (start + 1) tree
else
insertLoop lengths nextCode (start + 1) tree
else (tree, nextCode)
termination_by lengths.size - start
/-- The Huffman tree built by the canonical construction (RFC 1951 §3.2.2),
without input validation. -/
def fromLengthsTree (lengths : Array UInt8) (maxBits : Nat := 15) : HuffTree :=
let lsList := lengths.toList.map UInt8.toNat
let blCount := Huffman.Spec.countLengths lsList maxBits
let ncNat := Huffman.Spec.nextCodes blCount maxBits
let nextCode : Array UInt32 := ncNat.map fun n => n.toUInt32
(insertLoop lengths nextCode 0 .empty).1
/-- Build a Huffman tree from an array of code lengths (indexed by symbol).
Symbols with length 0 have no code. Uses the canonical Huffman algorithm
from RFC 1951 §3.2.2. Validates that all lengths are ≤ maxBits and the
Kraft inequality is satisfied (codes are not oversubscribed). -/
def fromLengths (lengths : Array UInt8) (maxBits : Nat := 15) :
Except String HuffTree :=
if lengths.any (fun l => l.toNat > maxBits) then
.error "Inflate: code length exceeds maximum"
else
let lsList := lengths.toList.map UInt8.toNat
let kraft := (lsList.filter (· != 0)).foldl
(fun acc l => acc + 2 ^ (maxBits - l)) 0
if kraft > 2 ^ maxBits then
.error "Inflate: oversubscribed Huffman code"
else
.ok (fromLengthsTree lengths maxBits)
/-- Decode one symbol from the bit reader using this Huffman tree. -/
def decode (tree : HuffTree) (br : BitReader) :
Except String (UInt16 × BitReader) :=
go tree br 0
where
go : HuffTree → BitReader → Nat → Except String (UInt16 × BitReader)
| .leaf s, br, _ => .ok (s, br)
| .empty, _, _ => .error "Inflate: invalid Huffman code"
| .node z o, br, n =>
if n > 20 then .error "Inflate: Huffman decode exceeded max depth"
else do
let (bit, br') ← br.readBit
if bit == 0 then go z br' (n + 1) else go o br' (n + 1)
/-! ## Table-driven fast decode (RFC 1951, "fast bits")
The bit-by-bit tree walk in `decode` descends one node per bit. The standard
DEFLATE speedup is a **lookup table**: peek `fastBits` bits and read the
`(symbol, codeLen)` directly from a `2^fastBits`-entry table, then consume
`codeLen` bits in one step. Codes longer than `fastBits` (rare; DEFLATE allows
up to 15) fall back to the tree walk. `decode` stays the canonical spec;
`decodeWithTable` is *proven equal* to it (`Zip.Spec.InflateTable`,
`decodeWithTable_eq`) and the fast block loop `decodeHuffmanFast` is proven
equal to `decodeHuffman` — there is no `@[implemented_by]` trust gap. -/
/-- Number of bits the fast decode table indexes on (the common "fast bits").
Widened to libdeflate's `LITLEN_TABLEBITS = 11`: fewer codewords spill to the
slow/subtable path, and the canonical O(2^bits) table build (#2671) keeps the
wider table cheap to construct. -/
def fastBits : Nat := 11
/-- Walk `tree` using the low bits of `idx` (LSB first, matching `readBit`),
for up to `fastBits` steps. Returns `(symbol, codeLen)` if a leaf is reached
within `fastBits` bits, or `(0, 0)` (a sentinel meaning "fall back to the
tree walk") for the `empty` slot or a code longer than `fastBits`. -/
def tableEntry (tree : HuffTree) (idx : Nat) : UInt16 × UInt8 :=
go tree idx 0
where
go (t : HuffTree) (bits depth : Nat) : UInt16 × UInt8 :=
match t with
| .leaf s => (s, depth.toUInt8)
| .empty => (0, 0)
| .node z o =>
if depth ≥ fastBits then (0, 0)
else if bits % 2 == 0 then go z (bits / 2) (depth + 1)
else go o (bits / 2) (depth + 1)
/-- Pack a slot's `(symbol, codeLen)` into one `UInt32` word: the codeword length
in the low byte (bits 0–7), the 16-bit symbol in bits 8–23. libdeflate-style
single-word entry (libdeflate also uses a `u32`). The hot decode loops read the
slot **once** per symbol via `entryAt` (a single guarded `packed[idx]` load) and
extract `len`/`sym` from that one word with `unpackLen`/`unpackSym` (a low-byte
truncation and a shift, register-only), so a per-symbol decode lowers to one
`lean_array_get` plus register ops. The
compiler does **not** CSE separate `lenAt`/`symAt` calls — each `dite` read
lowers to its own `lean_array_get_size` + `lean_nat_dec_lt` — so binding the
entry once in the source is what collapses the four reads (see `entryAt`). This
is one read of one tagged-scalar array, never the two reads into the parallel
`syms`/`lens` arrays of #2650 (which also touched two cache lines).
**`UInt32`, not `UInt64`.** On a 64-bit target `lean_box_uint32` is a tagged
scalar stored inline in the array (no allocation), exactly like the old
`Array UInt8`/`Array UInt16`; `lean_box_uint64` instead heap-allocates a boxed
word per entry, so an `Array UInt64` would pointer-chase through `lean_ctor_get`
on every read — re-introducing the very boxing #2650 removed. A `UInt32` holds
len (8 bits) + symbol (16 bits) with room to spare.
**The length is the low byte deliberately.** Only the length flows into the
well-founded recursions' termination measure (`cnt - len`), and extracting it
must avoid a shift: a `>>>` on the stuck array read `packed[idx]!` forces
`Nat.shiftRight`/`Nat.div` during the equation compiler's `whnf`, which loops
on the opaque value — the #2650 hazard. The low byte extracts with `toUInt8`
(a width truncation, no `Nat.div`), so `unpackLen` stays inert under `whnf`.
The symbol uses a shift, but it never enters the measure, so its `whnf` is
never forced during termination checking. -/
@[inline] def packEntry (sym : UInt16) (len : UInt8) : UInt32 :=
len.toUInt32 ||| (sym.toUInt32 <<< 8)
/-- The symbol field (bits 8–23) of a packed entry. -/
@[inline] def unpackSym (e : UInt32) : UInt16 := (e >>> 8).toUInt16
/-- The codeword-length field (low byte) of a packed entry. Shift-free so it stays
inert under the `whnf` the well-founded recursions force (see `packEntry`). -/
@[inline] def unpackLen (e : UInt32) : UInt8 := e.toUInt8
/-- A de-boxed fast-decode table: each slot's `(symbol, codeLen)` packed into one
`UInt32` word (`packEntry`), stored in a single scalar `Array UInt32`. Storing
`Array (UInt16 × UInt8)` boxes every slot as a heap pair, so each per-symbol
read pointer-chases through `lean_ctor_get` twice; #2650 split the fields into
two parallel scalar arrays to de-box; this folds them back into one packed
word so a per-symbol read is a single `lean_array_fget` plus shifts (via
`entryAt`, bound once) touching one tagged-scalar array / one cache line instead
of two. Reading through `lenAt`/`symAt` separately does **not** collapse to one
load — the compiler does not CSE the two `dite` reads — so the loops bind the
word once with `entryAt`.
The only packed field on the well-founded recursions' termination measure is
the length, and `lenAt` extracts it shift-free (`toUInt8` of the low byte), so
it stays inert under the `whnf` those recursions force during equation
compilation — the `Nat.div` loop #2650 warned of (a `>>>` on the stuck
`packed[idx]!` reducing to `Nat.shiftRight`/`Nat.div`) does not fire. `symAt`
does shift, but the symbol never enters a measure, so its `whnf` is never
forced (see `packEntry`). The packed table is kept behind an equivalence to
the split projections
(`buildTable_lenAt` / `buildTable_symAt` recover `(tableEntry …).2` /
`(tableEntry …).1`), so every decode proof transfers. -/
structure DecodeTable where
packed : Array UInt32
/-- The codeword length of slot `idx`: the low byte of the packed entry. One
scalar-array read plus a width truncation (no shift — see `packEntry` for why
the length is the low byte, so this stays inert under the well-founded
recursions' `whnf`). -/
@[inline] def DecodeTable.lenAt (t : DecodeTable) (idx : Nat) : UInt8 :=
if h : idx < t.packed.size then unpackLen (t.packed[idx]'h) else 0
/-- The symbol of slot `idx`: bits 8–23 of the packed entry. One scalar-array read
plus a shift; the symbol never enters a termination measure, so the shift is
never `whnf`-forced. -/
@[inline] def DecodeTable.symAt (t : DecodeTable) (idx : Nat) : UInt16 :=
if h : idx < t.packed.size then unpackSym (t.packed[idx]'h) else 0
theorem DecodeTable.lenAt_def (t : DecodeTable) (idx : Nat) :
t.lenAt idx = unpackLen t.packed[idx]! := by
unfold DecodeTable.lenAt
split
· rw [getElem!_pos t.packed idx ‹_›]
· rw [getElem!_neg t.packed idx ‹_›]; rfl
theorem DecodeTable.symAt_def (t : DecodeTable) (idx : Nat) :
t.symAt idx = unpackSym t.packed[idx]! := by
unfold DecodeTable.symAt
split
· rw [getElem!_pos t.packed idx ‹_›]
· rw [getElem!_neg t.packed idx ‹_›]; rfl
/-- The raw packed word of slot `idx` (out-of-bounds → `0`). The hot decode loops
bind this **once** per symbol and read `len`/`sym` from the single word
(`unpackLen` is a low-byte truncation, `unpackSym` a shift — both register-only,
no memory), so the compiled loop performs one
`lean_array_get` plus one bounds check per literal instead of the four the
`lenAt`/`symAt` guards lowered to (each `dite` read is a separate, un-CSE'd
`lean_array_get_size` + `lean_nat_dec_lt`). `lenAt`/`symAt` factor through this
(`lenAt_eq_unpackLen_entryAt` / `symAt_eq_unpackSym_entryAt`), so every existing
decode proof transfers. -/
@[inline] def DecodeTable.entryAt (t : DecodeTable) (idx : Nat) : UInt32 :=
if h : idx < t.packed.size then t.packed[idx]'h else 0
theorem DecodeTable.lenAt_eq_unpackLen_entryAt (t : DecodeTable) (idx : Nat) :
t.lenAt idx = unpackLen (t.entryAt idx) := by
unfold DecodeTable.lenAt DecodeTable.entryAt
split <;> rfl
theorem DecodeTable.symAt_eq_unpackSym_entryAt (t : DecodeTable) (idx : Nat) :
t.symAt idx = unpackSym (t.entryAt idx) := by
unfold DecodeTable.symAt DecodeTable.entryAt
split <;> rfl
/-- `entryAt` with the bounds proof supplied by the caller: an unchecked `uget` on
a `USize` index, so the hot loop pays **no** per-symbol bounds check and no
`USize→Nat` index round-trip. The `2^fastBits`-slot invariant on `packed`
(`buildTableCanonicalFastWithCount_size`) plus `bitBuf &&& 0x7FF < 2^fastBits`
discharge the proof once at the call site. Equal to `entryAt` in bounds
(`entryAtU_eq_entryAt`), so every decode proof still transfers. -/
@[inline] def DecodeTable.entryAtU (t : DecodeTable) (i : USize)
(h : i.toNat < t.packed.size) : UInt32 :=
t.packed.uget i h
theorem DecodeTable.entryAtU_eq_entryAt (t : DecodeTable) (i : USize)
(h : i.toNat < t.packed.size) : t.entryAtU i h = t.entryAt i.toNat := by
unfold DecodeTable.entryAtU DecodeTable.entryAt
rw [dif_pos h]; rfl
/-- The `fastBits`-bit window index, taken as a `USize`, indexes the `2^fastBits`
packed slots. `bitBuf &&& 0x7FF ≤ 0x7FF = 2^fastBits − 1`, and the `USize`
round-trip can only shrink it (`mod`), so the bound survives. -/
theorem and_0x7FF_toUSize_toNat_lt (bitBuf : UInt64) :
((bitBuf &&& 0x7FF).toUSize).toNat < 2 ^ fastBits := by
have hlt : (bitBuf &&& 0x7FF).toNat < 2 ^ fastBits := by
simp only [fastBits]
rw [UInt64.toNat_and]
exact Nat.lt_of_le_of_lt Nat.and_le_right (by decide)
exact Nat.lt_of_le_of_lt (by rw [UInt64.toNat_toUSize]; exact Nat.mod_le _ _) hlt
/-- The `USize` window index round-trips to its `Nat` value: `bitBuf &&& 0x7FF` is
far below `USize.size`. -/
theorem and_0x7FF_toUSize_toNat_eq (bitBuf : UInt64) :
((bitBuf &&& 0x7FF).toUSize).toNat = (bitBuf &&& 0x7FF).toNat := by
rw [UInt64.toNat_toUSize, ← USize.size_eq_two_pow]
refine Nat.mod_eq_of_lt ?_
have h1 : (bitBuf &&& 0x7FF).toNat ≤ 2047 := by
rw [UInt64.toNat_and]; exact Nat.le_trans Nat.and_le_right (by decide)
have h2 : (2047 : Nat) < USize.size := Nat.lt_of_lt_of_le (by decide) USize.le_size
omega
/-- The native-word spelling of the 11-bit fast-table window is in bounds. -/
theorem toUSize_and_0x7FF_toNat_lt (bitBuf : UInt64) :
(bitBuf.toUSize &&& 0x7FF).toNat < 2 ^ fastBits := by
rw [USize.toNat_and]
apply Nat.and_lt_two_pow
rw [USize.toNat_ofNat_of_lt (Nat.lt_of_lt_of_le (by decide) USize.le_size)]
decide
theorem and_0x7FF_toUSize_eq_toUSize_and (bitBuf : UInt64) :
(bitBuf &&& 0x7FF).toUSize = bitBuf.toUSize &&& 0x7FF := by
apply USize.toNat_inj.mp
rw [and_0x7FF_toUSize_toNat_eq, USize.toNat_and,
USize.toNat_ofNat_of_lt (Nat.lt_of_lt_of_le (by decide) USize.le_size)]
rw [UInt64.toNat_and]
have hm64 : (0x7FF : UInt64).toNat = 2047 := rfl
rw [hm64]
have hm : (2047 : Nat) = 2 ^ 11 - 1 := by decide
rw [hm, Nat.and_two_pow_sub_one_eq_mod]
rw [UInt64.toNat_toUSize]
change bitBuf.toNat % 2 ^ 11 = (bitBuf.toNat % USize.size) &&& (2 ^ 11 - 1)
rw [Nat.and_two_pow_sub_one_eq_mod, USize.size_eq_two_pow, Nat.mod_mod_of_dvd]
exact Nat.pow_dvd_pow 2 (Nat.le_trans (by omega) System.Platform.le_numBits)
/-- Reading the `fastBits`-bit window slot by `uget` on the `USize` index equals the
guarded `entryAt` read on the `Nat` index — the bridge that carries the tree-free
loop's `uget` optimization back to the boxed reference's `entryAt`/`lenAt`. -/
@[simp] theorem DecodeTable.entryAtU_window_eq (t : DecodeTable) (bitBuf : UInt64)
(h : ((bitBuf &&& 0x7FF).toUSize).toNat < t.packed.size) :
t.entryAtU (bitBuf &&& 0x7FF).toUSize h = t.entryAt (bitBuf &&& 0x7FF).toNat := by
rw [entryAtU_eq_entryAt, and_0x7FF_toUSize_toNat_eq]
theorem DecodeTable.entryAtU_native_window_eq (t : DecodeTable) (bitBuf : UInt64)
(h : (bitBuf.toUSize &&& 0x7FF).toNat < t.packed.size) :
t.entryAtU (bitBuf.toUSize &&& 0x7FF) h = t.entryAt (bitBuf &&& 0x7FF).toNat := by
rw [entryAtU_eq_entryAt]
congr 1
exact (congrArg USize.toNat (and_0x7FF_toUSize_eq_toUSize_and bitBuf).symm).trans
(and_0x7FF_toUSize_toNat_eq bitBuf)
/-- Build the `2^fastBits`-entry decode table for `tree`: slot `i` holds
`packEntry sym codeLen` for the `(sym, codeLen)` reached by walking `tree` on
the bits of `i`, stored in a single packed scalar array so each per-symbol read
is one de-boxed `UInt32` load instead of two reads / a heap-pair pointer-chase.
Built once per Huffman tree; cheap relative to the symbols decoded with it. -/
def buildTable (tree : HuffTree) : DecodeTable where
packed := Array.ofFn (n := 2 ^ fastBits) (fun i : Fin (2 ^ fastBits) =>
packEntry (tableEntry tree i.val).1 (tableEntry tree i.val).2)
/-- The tree-built decode table has exactly `2^fastBits` slots (`Array.ofFn`). -/
@[simp] theorem buildTable_size (tree : HuffTree) :
(buildTable tree).packed.size = 2 ^ fastBits := by
simp only [buildTable, Array.size_ofFn]
/-! ## Canonical O(n) table construction (libdeflate `build_decode_table`)
`buildTable` walks `tree` once per slot — `O(2^fastBits · depth)` with a tree
that itself costs an allocation per block. The libdeflate construction fills the
table directly from the code lengths, with no tree: assign each symbol its
canonical codeword (RFC 1951 §3.2.2, the same `nextCode` recurrence the tree
build uses), then write that symbol's `(sym, len)` into every table slot whose
low `len` bits are the codeword read LSB-first. A length-`len` codeword owns a
contiguous arithmetic progression of `2^(fastBits - len)` slots at stride
`2^len` starting from the bit-reversed codeword, so the whole fill is
`O(num_syms + 2^fastBits)`.
`buildCanonicalLoop` mirrors `HuffTree.insertLoop` step for step — same
`nextCode` threading, same per-symbol code `c` — so the table it fills equals
`buildTable (fromLengthsTree lengths)`: the two constructions assign identical
codewords, and filling the slots of a codeword is the table-side image of
inserting its leaf. The formal equality theorem (`buildTableCanonical_eq`) is the
format-independent core of #2671 and lands in a follow-up — its supporting lemmas
(bit-reversal, the `cwOf`/`bitReverse` slot bridge, the `fillSlots`
characterization) are already in `Zip.Spec.InflateCanonical`, and a differential
conformance test witnesses the equality at runtime across every code-length
regime. The canonical build is not yet on the decode path, so there is no trust
gap; wiring it in waits on the equality proof. -/
/-- Reverse the low `n` bits of `x` (bit `j` of the result is bit `n-1-j` of `x`).
A length-`len` canonical codeword is written MSB-first into the bitstream, so
the decoder — which peeks LSB-first — sees `bitReverse code len 0`. -/
def bitReverse (x : Nat) : Nat → Nat → Nat
| 0, acc => acc
| n + 1, acc => bitReverse (x / 2) n (acc * 2 + x % 2)
/-- Write `entry` into the `count` slots `base, base + stride, …` of `packed`
(the slots one length-`len` codeword owns: `stride = 2^len`,
`count = 2^(fastBits - len)`). Linear in `count`; in-place when `packed` is
uniquely referenced. -/
def fillSlots (packed : Array UInt32) (base stride count : Nat) (entry : UInt32) :
Array UInt32 :=
if count = 0 then packed
else fillSlots (packed.set! base entry) (base + stride) stride (count - 1) entry
termination_by count
/-- Unchecked variant of `fillSlots`: writes each slot with the proof-carrying
`Array.set` (compiles to an unchecked store, no per-slot bounds branch) rather
than the checked `set!`. The precondition
`0 < count → base + (count-1)·stride < packed.size` states the last slot the
fill touches is in range; it is preserved by the recursion because the last
slot never moves as `base` advances by `stride` and `count` shrinks. Equal to
`fillSlots` (`HuffTree.fillSlotsU_eq`, below), so
`buildCanonicalLoop` can call it behind a single per-codeword bounds guard
instead of paying a bounds check on every one of a codeword's `2^(fastBits-len)`
slot writes. -/
def fillSlotsU (packed : Array UInt32) (base stride count : Nat) (entry : UInt32)
(hb : 0 < count → base + (count - 1) * stride < packed.size) : Array UInt32 :=
if h : count = 0 then packed
else
have hbase : base < packed.size :=
Nat.lt_of_le_of_lt (Nat.le_add_right _ _) (hb (Nat.pos_of_ne_zero h))
fillSlotsU (packed.set base entry hbase) (base + stride) stride (count - 1) entry (by
intro _
rw [Array.size_set]
have hlt := hb (by omega)
have hmul : (count - 1 - 1) * stride = (count - 1) * stride - stride := Nat.sub_one_mul _ _
have hge : stride ≤ (count - 1) * stride := Nat.le_mul_of_pos_left stride (by omega)
omega)
termination_by count
/-- The canonical table-fill loop: for each symbol from `start`, look up its
canonical code `c = nextCode[len]` (advancing `nextCode[len]`, exactly as
`HuffTree.insertLoop` does), and — for codes that fit the `fastBits` window —
fill its slots. Codes longer than `fastBits` advance `nextCode` but fill no
slot (they reach the table as the sentinel `0`, the long-code fallback), and
`len = 0` symbols are skipped entirely. -/
def buildCanonicalLoop (lengths : Array UInt8) (nextCode : Array UInt32)
(start : Nat) (packed : Array UInt32) : Array UInt32 :=
if h : start < lengths.size then
let len := lengths[start]
if hlen : 0 < len.toNat ∧ len.toNat < nextCode.size then
let c := nextCode[len.toNat]'hlen.2
let nextCode' := nextCode.set! len.toNat (c + 1)
if len.toNat ≤ fastBits then
let base := bitReverse c.toNat len.toNat 0
let stride := 1 <<< len.toNat
let count := 1 <<< (fastBits - len.toNat)
let entry := packEntry start.toUInt16 len
let packed' :=
if hb : base + (count - 1) * stride < packed.size then
fillSlotsU packed base stride count entry (fun _ => hb)
else fillSlots packed base stride count entry
buildCanonicalLoop lengths nextCode' (start + 1) packed'
else
buildCanonicalLoop lengths nextCode' (start + 1) packed
else
buildCanonicalLoop lengths nextCode (start + 1) packed
else packed
termination_by lengths.size - start
/-- `fillSlots` preserves the array size. -/
@[simp] theorem fillSlots_size (packed : Array UInt32) (base stride count : Nat)
(entry : UInt32) :
(fillSlots packed base stride count entry).size = packed.size := by
induction count generalizing packed base with
| zero => simp [fillSlots]
| succ n ih =>
rw [fillSlots]
simp only [Nat.succ_ne_zero, ↓reduceIte, Nat.add_sub_cancel]
rw [ih (packed.set! base entry) (base + stride)]
simp
/-- A proof-carrying `Array.set` in bounds is the checked `set!`. -/
private theorem set_eq_set! {α : Type _} {a : Array α} {i : Nat} {v : α} (h : i < a.size) :
a.set i v h = a.set! i v := by
rw [Array.set!_eq_setIfInBounds, Array.setIfInBounds_def, dif_pos h]
/-- The unchecked fill `fillSlotsU` equals the checked `fillSlots`: each
proof-carrying `Array.set` in bounds is the corresponding `set!`. -/
theorem fillSlotsU_eq (packed : Array UInt32) (base stride count : Nat) (entry : UInt32)
(hb : 0 < count → base + (count - 1) * stride < packed.size) :
fillSlotsU packed base stride count entry hb = fillSlots packed base stride count entry := by
rw [fillSlotsU, fillSlots]
by_cases hc : count = 0
· rw [dif_pos hc, if_pos hc]
· rw [dif_neg hc, if_neg hc]
simp only [set_eq_set!]
exact fillSlotsU_eq (packed.set! base entry) (base + stride) stride (count - 1) entry _
termination_by count
/-- `buildCanonicalLoop` preserves the array size: every fill (`fillSlots` /
`fillSlotsU`) is size-preserving and the recursion only threads `packed`. -/
theorem buildCanonicalLoop_size (lengths : Array UInt8) (nextCode : Array UInt32)
(start : Nat) (packed : Array UInt32) :
(buildCanonicalLoop lengths nextCode start packed).size = packed.size := by
unfold buildCanonicalLoop
split
· dsimp only []
split
· split
· rw [buildCanonicalLoop_size]
split
· rw [fillSlotsU_eq, fillSlots_size]
· rw [fillSlots_size]
· rw [buildCanonicalLoop_size]
· rw [buildCanonicalLoop_size]
· rfl
termination_by lengths.size - start
/-- Build the `2^fastBits`-entry decode table directly from the code lengths,
libdeflate-style — canonical fill, no Huffman tree, no per-slot tree walk.
Equal to `buildTable (fromLengthsTree lengths)` (formal theorem
`buildTableCanonical_eq` forthcoming; witnessed at runtime by the
`InflateTable` canonical conformance test), so once proven it is a drop-in for
the tree-built table with every decode proof transferring unchanged. -/
def buildTableCanonical (lengths : Array UInt8) (maxBits : Nat := 15) : DecodeTable where
packed :=
let lsList := lengths.toList.map UInt8.toNat
let blCount := Huffman.Spec.countLengths lsList maxBits
let nextCode : Array UInt32 := (Huffman.Spec.nextCodes blCount maxBits).map (·.toUInt32)
buildCanonicalLoop lengths nextCode 0 (Array.replicate (2 ^ fastBits) (packEntry 0 0))
/-! ### Fast array-based canonical build inputs
`buildTableCanonical` routes the `nextCode` setup through the proof-oriented
`Huffman.Spec.countLengths` / `nextCodes`, which allocate a `List` (`lengths.toList`)
and run a well-founded recursion — heavier than the per-slot `tableEntry` walk it
was meant to replace. `countLengthsFast` / `nextCodesFast` compute the same arrays
directly over the `Array`, no `List` allocation, as the fast inputs to
`buildCanonicalLoop`. `buildTableCanonicalFast` equals `buildTableCanonical`
(proven by `buildTableCanonicalFast_eq` in `Zip.Spec.InflateCanonical`, and
witnessed by the `InflateTable` conformance test), so it inherits
`buildTableCanonical_eq`. -/
/-- Code-length histogram over the `Array`, no `List` allocation: counts, for each
length `1..maxBits`, how many symbols carry it (length-0 / out-of-range
ignored). Fast form of `Huffman.Spec.countLengths`. -/
def countLengthsFast (lengths : Array UInt8) (maxBits : Nat) : Array Nat :=
go lengths maxBits 0 (Array.replicate (maxBits + 1) 0)
where
go (lengths : Array UInt8) (maxBits i : Nat) (count : Array Nat) : Array Nat :=
if h : i < lengths.size then
let ln := lengths[i].toNat
let count := if 0 < ln ∧ ln ≤ maxBits then count.set! ln (count[ln]! + 1) else count
go lengths maxBits (i + 1) count
else count
termination_by lengths.size - i
/-- Kraft sum `∑_{len=0}^{maxBits} count[len] · 2^(maxBits − len)` over the
per-length histogram, by an `O(maxBits)` tail loop with **no allocation** —
`count` is the same array `buildTableCanonicalFast` already builds. (The `len=0`
term is zero for a `countLengthsFast` histogram, which never sets index 0, so it
contributes nothing while keeping the recurrence aligned with the spec's
`kraftSumFrom` from `0`.) The Kraft check `validateLengths` runs is then this
sum against `2^maxBits`, replacing the per-block `lengths.toList`/`filter`
`List` allocation `fromLengths` uses. -/
def kraftSumFast (count : Array Nat) (maxBits : Nat) : Nat :=
go count maxBits 0 0
where
go (count : Array Nat) (maxBits b acc : Nat) : Nat :=
if h : b ≤ maxBits then
go count maxBits (b + 1) (acc + count[b]! * (1 <<< (maxBits - b)))
else acc
termination_by maxBits + 1 - b
/-- The code-length validity check that `fromLengths` performs, factored out so a
decoder that builds no tree (the canonical tree-free path) can reject exactly
the malformed length sets `fromLengths` rejects, with the same error messages:
`"Inflate: code length exceeds maximum"` for any length `> maxBits`, and
`"Inflate: oversubscribed Huffman code"` when the Kraft sum overflows. The Kraft
sum is computed from the per-length `count` histogram (`countLengthsFast` —
the same array the canonical table build needs) via the allocation-free
`kraftSumFast`, rather than the per-block `List` (`lengths.toList`/`filter`)
`fromLengths` allocates; the over-bound check is a plain `Array.any`. No tree is
built. `fromLengths = (validateLengths …).map (fun _ => fromLengthsTree …)`
(`Zip.Spec.InflateTreeFreeCorrect.fromLengths_eq_validate`). -/
def validateLengths (lengths : Array UInt8) (maxBits : Nat := 15) :
Except String Unit :=
if lengths.any (fun l => l.toNat > maxBits) then
.error "Inflate: code length exceeds maximum"
else if kraftSumFast (countLengthsFast lengths maxBits) maxBits > 1 <<< maxBits then
.error "Inflate: oversubscribed Huffman code"
else
.ok ()
/-- First canonical code per length (RFC 1951 §3.2.2 step 2), as a `UInt32` array
computed by a direct `1..maxBits` loop — fast form of
`(Huffman.Spec.nextCodes count maxBits).map (·.toUInt32)`. -/
def nextCodesFast (count : Array Nat) (maxBits : Nat) : Array UInt32 :=
go count maxBits 1 0 (Array.replicate (maxBits + 1) 0)
where
go (count : Array Nat) (maxBits bits code : Nat) (nc : Array UInt32) : Array UInt32 :=
if h : bits ≤ maxBits then
let code := (code + count[bits - 1]!) * 2
go count maxBits (bits + 1) code (nc.set! bits code.toUInt32)
else nc
termination_by maxBits + 1 - bits
/-- Build the canonical decode table with the fast array-based `nextCode` setup
(`countLengthsFast` / `nextCodesFast`), avoiding the `List` allocation and
well-founded recursion of `buildTableCanonical`'s spec-function inputs. Equal
to `buildTableCanonical` — same `buildCanonicalLoop`, same `nextCode` array —
so it is a drop-in carrying `buildTableCanonical_eq` once the array/spec
equality is proven (witnessed now by the `InflateTable` conformance test). -/
def buildTableCanonicalFast (lengths : Array UInt8) (maxBits : Nat := 15) : DecodeTable where
packed :=
let count := countLengthsFast lengths maxBits
let nextCode := nextCodesFast count maxBits
buildCanonicalLoop lengths nextCode 0 (Array.replicate (2 ^ fastBits) (packEntry 0 0))
/-- `buildTableCanonicalFast` taking a precomputed length histogram, so a per-block
decoder building both the fast table and the long-code structures can share a
single `countLengthsFast` pass over the length vector. Definitionally equal to
`buildTableCanonicalFast lengths maxBits` when `count = countLengthsFast lengths
maxBits` (`buildTableCanonicalFastWithCount_eq`). -/
def buildTableCanonicalFastWithCount (lengths : Array UInt8) (count : Array Nat)
(maxBits : Nat := 15) : DecodeTable where
packed :=
let nextCode := nextCodesFast count maxBits
buildCanonicalLoop lengths nextCode 0 (Array.replicate (2 ^ fastBits) (packEntry 0 0))
/-- The canonical fast table has exactly `2^fastBits` packed slots — the initial
`Array.replicate (2^fastBits)` that `buildCanonicalLoop` size-preserves. This is
the invariant the tree-free loop's `uget` indexing needs (`goTreeFreeU`'s `hlp`). -/
@[simp] theorem buildTableCanonicalFastWithCount_size (lengths : Array UInt8)
(count : Array Nat) (maxBits : Nat) :
(buildTableCanonicalFastWithCount lengths count maxBits).packed.size = 2 ^ fastBits := by
simp only [buildTableCanonicalFastWithCount, buildCanonicalLoop_size, Array.size_replicate]
/-- Bits remaining in the reader from its current `(pos, bitOff)`. -/
def bitsAvail (br : BitReader) : Nat :=
if br.pos ≥ br.data.size then 0 else (br.data.size - br.pos) * 8 - br.bitOff
/-- Peek the next `fastBits` bits at `(pos, bitOff)`, LSB-first, without
consuming. Reads 3 bytes: a `≤ 7`-bit `bitOff` plus the 11-bit window spans
bits `0 … 17`, so three bytes (bits `0 … 23`) always cover it. Bytes past the
end of the stream read as zero; the caller uses `bitsAvail` to decide whether
the looked-up code actually fits. -/
def peekFast (br : BitReader) : UInt32 :=
let b0 : UInt32 := if h : br.pos < br.data.size then (br.data[br.pos]'h).toUInt32 else 0
let b1 : UInt32 := if h : br.pos + 1 < br.data.size then (br.data[br.pos + 1]'h).toUInt32 else 0
let b2 : UInt32 := if h : br.pos + 2 < br.data.size then (br.data[br.pos + 2]'h).toUInt32 else 0
((b0 ||| (b1 <<< 8) ||| (b2 <<< 16)) >>> br.bitOff.toUInt32) &&& 0x7FF
/-- Table-driven single-symbol decode. Peeks `fastBits` bits, reads the
`(symbol, codeLen)` from `table`, and consumes `codeLen` bits in one step.
Falls back to the canonical `decode` tree walk for long codes (sentinel
`codeLen = 0`) and when fewer than `codeLen` bits remain. Proven equal to
`decode` when `table = buildTable tree` (`Zip.Spec.InflateTable`). -/
def decodeWithTable (tree : HuffTree) (table : DecodeTable)
(br : BitReader) : Except String (UInt16 × BitReader) :=
-- `bitOff ≥ 8` is unreachable for a well-formed reader (every `readBit`
-- leaves `bitOff < 8`); the guard makes the equality with `decode`
-- unconditional, so the proof transfer needs no side conditions.
if br.bitOff ≥ 8 then tree.decode br
else
let idx := (peekFast br).toNat
let len := (table.lenAt idx).toNat
if len == 0 || len > bitsAvail br then
tree.decode br
else
let total := br.bitOff + len
.ok (table.symAt idx, { br with pos := br.pos + total / 8, bitOff := total % 8 })
end HuffTree
namespace Inflate
-- RFC 1951 §3.2.5: Fixed Huffman code lengths for lit/length (0–287)
def fixedLitLengths : Array UInt8 :=
.replicate 144 8 ++ .replicate 112 9 ++
.replicate 24 7 ++ .replicate 8 8
-- RFC 1951 §3.2.5: Fixed Huffman code lengths for distance (0–31)
def fixedDistLengths : Array UInt8 := .replicate 32 (5 : UInt8)
-- Length base values for codes 257–285 (RFC 1951 §3.2.5)
def lengthBase : Array UInt16 := #[
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258
]
-- Extra bits for length codes 257–285
def lengthExtra : Array UInt8 := #[
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0
]
@[simp] theorem lengthBase_size : lengthBase.size = 29 := by decide
@[simp] theorem lengthExtra_size : lengthExtra.size = 29 := by decide
-- Distance base values for codes 0–29
def distBase : Array UInt16 := #[
1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289,
16385, 24577
]
-- Extra bits for distance codes 0–29
def distExtra : Array UInt8 := #[
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13
]
@[simp] theorem distBase_size : distBase.size = 30 := by decide
@[simp] theorem distExtra_size : distExtra.size = 30 := by decide
/-- The per-byte back-reference copy worker: copy `length` bytes from `buf`
starting at `start`, repeating every `distance` bytes (LZ77 copy with
wrap-around). Defined as explicit recursion for proof tractability; this is
the reference semantics that `copyLoop` dispatches to. -/
def copyLoopGo (buf : ByteArray) (start distance : Nat)
(k length : Nat)
(hd_pos : distance > 0 := by omega) (hsd : start + distance ≤ buf.size := by omega) : ByteArray :=
if k < length then
have hidx : start + (k % distance) < buf.size := by
have := Nat.mod_lt k hd_pos; omega
copyLoopGo (buf.push buf[start + (k % distance)]) start distance (k + 1) length
hd_pos (by simp [ByteArray.size_push]; omega)
else buf
termination_by length - k
/-- Copy `length` bytes from `buf` starting at `start`, repeating every
`distance` bytes (LZ77 back-reference copy).
For the common **non-overlapping** back-reference (`k = 0 ∧ length ≤ distance`)
every index `start + (k % distance)` is just `start + k`, so the whole copy is
the contiguous slice `[start, start + length)`, appended in a single pass by
`ByteArray.copyWithin` (one `memcpy`, no intermediate allocation — its
reference body is exactly `buf ++ buf.extract start (start + length)`) instead
of `length` per-byte `push`es / bounds-checks / modular indices. For an
**overlapping** back-reference (`k = 0 ∧ length > distance`,
the RLE case) the copy is the periodic extension of the `distance`-byte
window, appended in a single allocation-free pass by `ByteArray.extendWithin`
(its reference body is exactly the `fillDouble`-based expression) instead of
`length` per-byte `push`es. A partial copy (`k ≠ 0`, never produced by the
decoders) falls back to the per-byte `copyLoopGo`. All three are proven equal
to `copyLoopGo` via `copyLoop_eq_ofFn`, so every decode correctness proof is
unaffected. -/
def copyLoop (buf : ByteArray) (start distance : Nat)
(k length : Nat)
(hd_pos : distance > 0 := by omega) (hsd : start + distance ≤ buf.size := by omega) : ByteArray :=
if k = 0 ∧ length ≤ distance then
buf.copyWithin start length
else if k = 0 then
buf.extendWithin start distance length
else
copyLoopGo buf start distance k length hd_pos hsd
-- Code length alphabet order for dynamic Huffman (RFC 1951 §3.2.7)
def codeLengthOrder : Array Nat := #[
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
]
@[simp] theorem codeLengthOrder_size : codeLengthOrder.size = 19 := by decide
/-- Fill `count` consecutive entries starting at `idx` with `val`,
stopping when `idx ≥ bound`. Returns updated array and new index. -/
def fillEntries (arr : Array UInt8) (idx count bound : Nat) (val : UInt8) :
Array UInt8 × Nat :=
if count = 0 ∨ idx ≥ bound then (arr, idx)
else fillEntries (arr.set! idx val) (idx + 1) (count - 1) bound val
termination_by count
private theorem fillEntries_snd_eq (arr : Array UInt8) (idx count bound : Nat) (val : UInt8)
(h : idx + count ≤ bound) :
(fillEntries arr idx count bound val).snd = idx + count := by
induction count generalizing arr idx with
| zero => simp [fillEntries]
| succ n ih =>
unfold fillEntries
simp only [Nat.succ_ne_zero, false_or, show ¬(idx ≥ bound) from by omega,
↓reduceIte, Nat.add_sub_cancel]
rw [ih (arr.set! idx val) (idx + 1) (by omega)]; omega
/-- Read code length code lengths: 3 bits each at permuted positions.
Defined as explicit recursion for proof tractability. -/
def readCLCodeLengths (br : BitReader) (clLengths : Array UInt8)
(i numCodeLen : Nat) : Except String (Array UInt8 × BitReader) :=
if i < numCodeLen then do
if h_i : i < codeLengthOrder.size then
let (v, br) ← br.readBits 3
readCLCodeLengths br (clLengths.set! (codeLengthOrder[i]) v.toUInt8) (i + 1) numCodeLen
else
throw "Inflate: code length index out of bounds"
else
.ok (clLengths, br)
termination_by numCodeLen - i
/-- Decode code lengths using the CL Huffman tree (RFC 1951 §3.2.7).
Processes symbols: 0–15 (literal length), 16 (repeat previous),
17 (repeat 0, short), 18 (repeat 0, long).
Defined as explicit recursion for proof tractability. -/
def decodeCLSymbols (clTree : HuffTree) (br : BitReader)
(codeLengths : Array UInt8) (idx totalCodes : Nat)
: Except String (Array UInt8 × BitReader) :=
if idx ≥ totalCodes then .ok (codeLengths, br)
else do
let (sym, br) ← clTree.decode br
if sym < 16 then
decodeCLSymbols clTree br (codeLengths.set! idx sym.toUInt8) (idx + 1) totalCodes
else if sym == 16 then
if idx == 0 then throw "Inflate: repeat code at start"
if h_cl : idx - 1 < codeLengths.size then do
let (rep, br) ← br.readBits 2
let prev := codeLengths[idx - 1]
let count := rep.toNat + 3
if idx + count > totalCodes then throw "Inflate: repeat code exceeds total"
decodeCLSymbols clTree br (fillEntries codeLengths idx count totalCodes prev).1
(idx + count) totalCodes
else throw "Inflate: repeat code index out of bounds"
else if sym == 17 then
let (rep, br) ← br.readBits 3
let count := rep.toNat + 3
if idx + count > totalCodes then throw "Inflate: repeat code exceeds total"
decodeCLSymbols clTree br (fillEntries codeLengths idx count totalCodes 0).1
(idx + count) totalCodes
else if sym == 18 then
let (rep, br) ← br.readBits 7
let count := rep.toNat + 11
if idx + count > totalCodes then throw "Inflate: repeat code exceeds total"
decodeCLSymbols clTree br (fillEntries codeLengths idx count totalCodes 0).1
(idx + count) totalCodes
else
throw s!"Inflate: invalid code length symbol {sym}"
termination_by totalCodes - idx
decreasing_by all_goals omega
/-- Decode dynamic Huffman trees from the bitstream (RFC 1951 §3.2.7). -/
def decodeDynamicTrees (br : BitReader) :
Except String (HuffTree × HuffTree × BitReader) := do
let (hlit, br) ← br.readBits 5
let (hdist, br) ← br.readBits 5
let (hclen, br) ← br.readBits 4
let numLitLen := hlit.toNat + 257
let numDist := hdist.toNat + 1
let numCodeLen := hclen.toNat + 4
let (clLengths, br) ← readCLCodeLengths br (.replicate 19 0) 0 numCodeLen
let clTree ← HuffTree.fromLengths clLengths 7
let totalCodes := numLitLen + numDist
let (codeLengths, br) ← decodeCLSymbols clTree br (.replicate totalCodes 0)
0 totalCodes
let litLenLengths := codeLengths.extract 0 numLitLen
let distLengths := codeLengths.extract numLitLen totalCodes
let litTree ← HuffTree.fromLengths litLenLengths
let distTree ← HuffTree.fromLengths distLengths
return (litTree, distTree, br)
/-- Decode a stored (uncompressed) block. -/
protected def decodeStored (br : BitReader) (output : ByteArray)
(maxOutputSize : Nat) : Except String (ByteArray × BitReader) := do
let (len, br) ← br.readUInt16LE
let (nlen, br) ← br.readUInt16LE
if len ^^^ nlen != 0xFFFF then
throw "Inflate: stored block length check failed"
if output.size + len.toNat > maxOutputSize then
throw "Inflate: output exceeds maximum size"
let (bytes, br) ← br.readBytes len.toNat
return (output ++ bytes, br)
/-- Decode a Huffman-coded block (fixed or dynamic).
Uses well-founded recursion on the remaining bits in the stream. -/
protected def decodeHuffman (br : BitReader) (output : ByteArray)
(litTree distTree : HuffTree) (maxOutputSize : Nat)
: Except String (ByteArray × BitReader) :=
go br.data.size br output
where
go (dataSize : Nat) (br : BitReader) (output : ByteArray)
: Except String (ByteArray × BitReader) := do
let (sym, br₁) ← litTree.decode br
if sym < 256 then
if output.size ≥ maxOutputSize then
throw "Inflate: output exceeds maximum size"
-- Guard: bit position must advance for WF termination
if _h₁ : br₁.bitPos ≤ br.bitPos then
throw "Inflate: no progress in Huffman decode"
else if _h₂ : dataSize * 8 < br₁.bitPos then
throw "Inflate: bit position out of range"
else
go dataSize br₁ (output.push sym.toUInt8)
else if sym == 256 then
.ok (output, br₁)
else
-- Length code 257–285
let idx := sym.toNat - 257
if h : idx ≥ lengthBase.size then
throw s!"Inflate: invalid length code {sym}"
else
let base := lengthBase[idx]
let extra := lengthExtra[idx]'(by simp [lengthExtra_size, lengthBase_size] at h ⊢; omega)
let (extraBits, br₂) ← br₁.readBits extra.toNat
let length := base.toNat + extraBits.toNat
-- Distance code
let (distSym, br₃) ← distTree.decode br₂
let dIdx := distSym.toNat
if h : dIdx ≥ distBase.size then
throw s!"Inflate: invalid distance code {distSym}"
else
let dBase := distBase[dIdx]
let dExtra := distExtra[dIdx]'(by simp [distExtra_size, distBase_size] at h ⊢; omega)
let (dExtraBits, br₄) ← br₃.readBits dExtra.toNat
let distance := dBase.toNat + dExtraBits.toNat
-- Copy from output buffer (LZ77 back-reference)
if hd0 : distance = 0 then
throw s!"Inflate: zero back-reference distance"
else if hds : distance > output.size then
throw s!"Inflate: distance {distance} exceeds output size {output.size}"
else if output.size + length > maxOutputSize then
throw "Inflate: output exceeds maximum size"
else
let start := output.size - distance
let out := copyLoop output start distance 0 length
(by omega) (by omega)
-- Guard: bit position must advance for WF termination
if _h₁ : br₄.bitPos ≤ br.bitPos then
throw "Inflate: no progress in Huffman decode"
else if _h₂ : dataSize * 8 < br₄.bitPos then
throw "Inflate: bit position out of range"
else
go dataSize br₄ out
termination_by dataSize * 8 - br.bitPos
decreasing_by all_goals omega
/-- Table-driven variant of `decodeHuffman`: identical to it except the two
Huffman symbol decodes go through `decodeWithTable` (fast-bits lookup table)
instead of the bit-by-bit `HuffTree.decode` walk. The literal/length and
distance tables are built once, up front, then reused for every symbol in
the block. Proven equal to `decodeHuffman` symbol-by-symbol through
`HuffTree.decodeWithTable_eq` (see `decodeHuffmanFastBR_eq`).
This is the **BitReader reference** decoder. The default `decodeHuffmanFast`
(below) delegates to the faster wide-buffer `InflateBuf.decodeHuffmanFastBuf`,
which is proven equal to *this* (`decodeHuffmanFastBuf_eq`); composing the two
equalities gives `decodeHuffmanFast = decodeHuffman`, so the bit-by-bit walk
stays the canonical spec and every inflate correctness proof transfers. -/
protected def decodeHuffmanFastBR (br : BitReader) (output : ByteArray)
(litTree distTree : HuffTree) (maxOutputSize : Nat)
: Except String (ByteArray × BitReader) :=
go litTree.buildTable distTree.buildTable br.data.size br output
where
go (litTable distTable : HuffTree.DecodeTable)
(dataSize : Nat) (br : BitReader) (output : ByteArray)
: Except String (ByteArray × BitReader) := do
let (sym, br₁) ← litTree.decodeWithTable litTable br
if sym < 256 then
if output.size ≥ maxOutputSize then
throw "Inflate: output exceeds maximum size"
else if _h₁ : br₁.bitPos ≤ br.bitPos then
throw "Inflate: no progress in Huffman decode"
else if _h₂ : dataSize * 8 < br₁.bitPos then
throw "Inflate: bit position out of range"
else
go litTable distTable dataSize br₁ (output.push sym.toUInt8)
else if sym == 256 then
.ok (output, br₁)
else
let idx := sym.toNat - 257
if h : idx ≥ lengthBase.size then
throw s!"Inflate: invalid length code {sym}"
else
let base := lengthBase[idx]
let extra := lengthExtra[idx]'(by simp [lengthExtra_size, lengthBase_size] at h ⊢; omega)
let (extraBits, br₂) ← readBitsFast br₁ extra.toNat
let length := base.toNat + extraBits.toNat
let (distSym, br₃) ← distTree.decodeWithTable distTable br₂
let dIdx := distSym.toNat
if h : dIdx ≥ distBase.size then
throw s!"Inflate: invalid distance code {distSym}"
else
let dBase := distBase[dIdx]
let dExtra := distExtra[dIdx]'(by simp [distExtra_size, distBase_size] at h ⊢; omega)
let (dExtraBits, br₄) ← readBitsFast br₃ dExtra.toNat
let distance := dBase.toNat + dExtraBits.toNat
if hd0 : distance = 0 then
throw s!"Inflate: zero back-reference distance"
else if hds : distance > output.size then
throw s!"Inflate: distance {distance} exceeds output size {output.size}"
else if output.size + length > maxOutputSize then
throw "Inflate: output exceeds maximum size"
else
let start := output.size - distance
let out := copyLoop output start distance 0 length
(by omega) (by omega)
if _h₁ : br₄.bitPos ≤ br.bitPos then
throw "Inflate: no progress in Huffman decode"
else if _h₂ : dataSize * 8 < br₄.bitPos then
throw "Inflate: bit position out of range"
else
go litTable distTable dataSize br₄ out
termination_by dataSize * 8 - br.bitPos
decreasing_by all_goals omega
end Inflate
/-!
## Wide-buffer Huffman decoder primitives (Track D, #2501)
Thread the bit cursor as unboxed scalars `(pos, bitBuf : UInt64, cnt)` — plus a
`bitpos` measure for termination — through the whole Huffman symbol loop,
refilling up to 57 bits at a time and consuming by shift, instead of allocating
a fresh `BitReader` per field read. `Zip.Spec.InflateBufCorrect` proves
`decodeHuffmanFastBuf` equal to the BitReader reference `Inflate.decodeHuffmanFastBR`