Skip to content

feat: prove the JPEG frame walk, block painting and round trip up to the scan decode - #65

Merged
ngngardner merged 5 commits into
mainfrom
claude/skills-marketplace-setup-hlbfz7-wp10-jpeg-finish
Sep 25, 2026
Merged

ngngardner merged 5 commits into
mainfrom
claude/skills-marketplace-setup-hlbfz7-wp10-jpeg-finish

Conversation

@ngngardner

Copy link
Copy Markdown
Contributor

Summary

Both rows stay pending.

IMG-JPG-2: eight new direct laws

  • jpeg_block_cover_all: painting a block covers the right pixels for every factor pair, 4×4 included, on any plane.
    • Its proof is structural, so it no longer normalises 1024 writes.
  • jpeg_mcu_grid_comp: the A.2.3 grid holds for any scan component.
  • jpeg_refuse_factor1_any, jpeg_refuse_factor3_any and jpeg_refuse_count: bad factors are refused after fill bytes too, a length field longer than the components doesn't hide them, and component counts other than 1 or 3 are refused.
  • jpeg_points_at: the plane read-out returns point k at index k.
  • jpeg_walk_frame: decode.adv visits every MCU in raster order, and each component's units in T.81 order, with no counter wrapping.
  • jpeg_walk_count: the block count equals the length of that walk.
  • Still missing: Array.get-after-Array.set lemmas that join the painting laws to the read-out.
  • The row is false as worded for frames above 2^31 points: decode.plane's depth uses a shift that wraps, so points alias. The wording is a decision for the maintainer.

IMG-JPG-3: seven new direct laws

  • jpeg_enc_stuffed and jpeg_unstuff: the encoder's byte stuffing and the decoder's unstuffing are inverses.
  • jpeg_enc_header_walk: the marker walk over the encoder's header reaches the entropy-coded data with the right frame.
  • jpeg_ent_walk: the walk keeps stuffed data whole and stops at EOI.
  • jpeg_round_trip_scan: decode_jpeg(encode_jpeg(r)) equals decode.run of the encoder's bytes, in r's frame.
  • jpeg_run_sized and jpeg_round_trip_sized: the result is none or a raster of r's size.
  • Still missing: the Huffman round trip, meaning decode.run on those bytes is not none.

Code changes (byte-identical)

  • decode.splat paints by rows, columns and pixels. The Cursor type is removed.
  • decode.emit reads the points in order, as decode.points.
  • Byte tests use U32.is_eq.
  • The encoder stores bytes raw and stuffs them once, at the flush.
  • The block count is named decode.nblocks.
  • Proof-only edits outside the block: WP7's uses of decode.emit now call decode.points, and WP8's jpeg_block_cover gets the structural proof. No law statement changed.

Checks

  • bend PROOF.bend prints All terms check.
    • Gate time is about the same as main's; the faster block-cover proof offsets the new laws.
  • bolt v1.7.0 reports 0 errors. CI runs v1.8.0.
  • Pillow: 0 hard failures in 40 cases.
  • Probes: 224 of 224 identical against a driver rebuilt from main.
    • A further 48 crafted JPEGs also decode identically: fill bytes in the scan, markers mid-scan, truncation, and a trailing 255.
  • 14 planted mutations were each caught by their target law.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A1bVZYFbhKkn2BKHKthcVP


Generated by Claude Code

…ponent, overlong SOF0

Painting a block is now written as the law-side cover is (decode.splat by rows, columns and
pixels), so jpeg_block_cover_all proves it for every sample size, 4 by 4 included, and every
plane, and jpeg_block_cover gets a structural proof (the gate drops from about 140 s to 89 s).
jpeg_mcu_grid_comp states the A.2.3 grid for any scan component. jpeg_refuse_factor1_any,
jpeg_refuse_factor3_any and jpeg_refuse_count refuse a SOF0 with a bad factor or count after
any prefix ending at its marker (fill bytes included) and with any length longer than its
components. decode.nbits and the entropy walk compare bytes with U32.is_eq instead of literal
patterns. Decoded output is byte-identical on every probe and on crafted fill, marker and
truncation cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1bVZYFbhKkn2BKHKthcVP
decode.emit reads a plane point by point in order (decode.points), without the reversed
accumulator; the output is the same. jpeg_points_at: sample k of the points is the value
Array.get finds at index k, for every plane, through lemmas that a read hands its array back.
jpeg_walk_frame: from the scan's first block, the decoder's own decode.adv visits the frame's
MCUs in raster order, mw = ceil(w / 8 hmax) to a row, and in each MCU the scan's components and
their hi * vi data units in T.81 order, with the unit, component and column counters never
wrapping. Decoded output is byte-identical on every probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1bVZYFbhKkn2BKHKthcVP
LAWS.bend and PROOF.bend keep both blocks at their ends, main's wp9 block first, and PROOF.bend
imports both lemma files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1bVZYFbhKkn2BKHKthcVP
jpeg_enc_stuffed: the encoder's entropy-coded bytes have every 255 followed by a 0. The encoder
now keeps raw bytes and stuffs them once at the flush (encode.stuff.all), and dispatches on its
tag with U32.is_eq; its output is the same. jpeg_enc_header_walk: the decoder's walk over the
encoder's header reaches the data with the encoder's frame, scan and tables, for every size.
jpeg_ent_walk: the walk keeps stuffed data whole and stops at EOI. jpeg_round_trip_scan:
decode_jpeg(encode_jpeg(r)) is the scan decode of the encoder's own entropy-coded bytes in r's
frame. jpeg_run_sized and jpeg_round_trip_sized: that decode is none or a raster of r's size.
jpeg_walk_count: the decoder's U32 block count, now named decode.nblocks, is the length of
jpeg_walk_frame's order when it fits a U32. SPEC.md records what is left of IMG-JPG-2 and
IMG-JPG-3, and that IMG-JPG-2 is false for frames above 2^31 points (the plane depth wraps).
Every probe output identical, Pillow check 40 of 40.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1bVZYFbhKkn2BKHKthcVP
jpeg_unstuff: the decoder's bit reader, reading eight bits at a time from the encoder's stuffing
of any bytes (encode.stuff.all), reads the bytes back with the stuffed zeros dropped. The reader
ors each new bit in before the shifted accumulator, so a symbolic byte's bits come out as the
byte after one rewrite per bit; the value is the same. Decoded output is byte-identical on
every probe; a stuffed 255 read as 254 fails the law.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1bVZYFbhKkn2BKHKthcVP
@ngngardner
ngngardner merged commit 12dd918 into main Sep 25, 2026
1 check passed
@ngngardner
ngngardner deleted the claude/skills-marketplace-setup-hlbfz7-wp10-jpeg-finish branch September 25, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants