Repository navigation
Conversation
|
Mathlib CI status (docs):
|
|
Reference manual CI status:
|
96f608a to
61fa405
Compare
This PR adds little- and big-endian UInt16/UInt32/UInt64 load and store accessors to ByteArray, reading or writing a fixed-width integer at a byte offset in a single native load/store rather than through a boxed Array UInt32 or hand-written byte assembly. For each width and endianness there are defaulting (`!`), Nat-with-proof, and USize-with-proof variants mirroring the existing byte accessors; the defaulting variants are all-or-nothing on bounds. Hot loops should use the USize-indexed `uget*`/`uset*` forms, since the Nat-indexed variants box the index (the module docstring says so). Lemmas in Init.Data.ByteArray.Lemmas establish the proof-carrying variants as the `!` model, size preservation, and round-trip (`get* (set* a off v) off = v`, with the bit-recombination identity discharged by getLsbD extensionality, no bv_decide). Disjointness is stated at the byte level: a wide write changes only the bytes in its own window, so a read of any width/endianness outside that window is unaffected. Tests in tests/elab/bytearray_pack.lean check the @[extern] C against the Lean model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61fa405 to
ce55a45
Compare
|
I actually added these already in #8165; I'll update that to fix the merge conflicts (to be clear, I don't particularly like the approach of making a ton of independent functions, my PR instead adds a general simp normal form of |
Init.Data.String.Defs imports Init.Data.ByteArray.Lemmas, which places that module below the UInt/BitVec lemma layer; importing Init.Data.UInt and Init.Data.BitVec there closes an import cycle. Move the lemmas that need them into a new downstream module instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lean_copy_byte_array was removed from lean.h by leanprover#15052; follow the other ByteArray setters and use lean_sarray_ensure_exclusive. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@kim-em Landing this would be really valuable, but I think it could be made faster. I tested these storage externs on a fast implementation of NTT (number theoretic transform), where without them it was impossible to match existing Rust benchmarks. With this PR's C implementation it was still too slow, but slight changes (without affecting the API) made it fast enough, so I want to propose this as a change. I worked on this with Claude Opus 5.5 and not an expert in the technical details myself so the proposal that follows is written by Claude 🤖 1. Load and store with a fixed-size T r; memcpy(&r, lean_sarray_cptr(a) + i, sizeof r); /* + byte swap when host order differs */
2. After a store, tell the compiler the array is still exclusive. lean_obj_res r = lean_sarray_ensure_exclusive(a);
memcpy(lean_sarray_cptr(r) + i, &v, sizeof v);
LEAN_ASSUME(r->m_rc == 1); /* __builtin_assume / __assume / no-op */
Evidence.
|
This PR adds little- and big-endian
UInt16/UInt32/UInt64load and store accessors toByteArray, reading or writing a fixed-width integer at a byte offset in a single native load/store rather than through a boxedArray UInt32(a tagged load plus an unbox) or hand-written byte assembly.For each width and endianness there are three readers and three writers, mirroring the existing byte accessors
get!/get/ugetandset!/set/uset:getUInt32LE!/setUInt32LE!—Natoffset, no proof. All-or-nothing on bounds: a read whoseW/8-byte window does not fit returns0, and such a write leaves the array unchanged, matching the defaulting behaviour ofByteArray.get!/set!.getUInt32LE/setUInt32LE—Natoffset with an in-bounds proof.ugetUInt32LE/usetUInt32LE—USizeoffset with an in-bounds proof (the fast path).The offset is a byte position, so the same primitive serves both an array-of-
UInt32view (i = 4*k) and the read-a-UInt32-at-an-arbitrary-position case common in codecs. The@[extern]implementations arestatic inlineC inlean.hbuilt from the portable byte-shift idiom, which optimizing compilers typically fold to an efficient (possibly unaligned) load or store; the Lean definitions are the proof-level model and the externs are validated against them by tests.In a hot loop the
USize-indexedugetUInt*/usetUInt*forms are the ones to reach for: theNat-indexed variants (including the!forms) take a boxedNat, so the loop's index arithmetic runs boxed and measures noticeably slower. The module docstring says so.Lemmas accompany the API in
Init.Data.ByteArray.Lemmas: the proof-carrying variants are definitionally the!model, writes preservesize, and reads round-trip writes (get* (set* a off v) off = vunder bounds), for every width and endianness. The round-trip proofs reduce the read of the freshly-written bytes to a fixed-width bit-recombination identity discharged bygetLsbDextensionality (nobv_decide, so everything stays inInit). Disjointness is stated at the byte level —getElem!_setUIntWE!_of_outsidesays a wide write changes only the bytes in its own window, so a read of any byte (hence any width or endianness) outside that window is unaffected; same-width/endianness_of_disjointcorollaries are provided for convenience.tests/elab/bytearray_pack.leanadditionally checks that the@[extern]C implementations match the Lean model on concrete values, endianness, and the all-or-nothing out-of-bounds behaviour.This is motivated by performance work on pure-Lean codecs (see #14050 "feat: fast fixed-width integer load/store on ByteArray"): a data structure that is conceptually a dense array of fixed-width integers previously had no representation with a single-instruction element load.
ByteSliceforwarding accessors are a natural follow-up left out of this PR.🤖 Prepared with Claude Code