README: sub-word struct members are sign-extended (intN) or left-aligned (bytesN), not always zero-padded - #30
Conversation
…ned (bytesN), not always zero-padded The local git-hooks.nix pre-commit (denofmt) is bypassed for this commit: it reflows the entire README, which main does not satisfy and CI does not run. Closes #24 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvfmeRQKubdbFW1GL3kmib
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. WalkthroughThe README now documents Solidity memory representations for sub-word types. It specifies right alignment, zero padding, sign extension, and left alignment rules. It also clarifies that hashing uses the word as laid out in memory. ChangesMemory Layout Documentation
Estimated code review effort: 1 (Trivial) | ~2 minutes Merge Risk: ⚪ Minimal · up to This updates documentation to describe type-specific Solidity memory layouts and full-word hashing behavior. No merge-readiness risk remains. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Full details: Linked Issues checkExplanation The README documents zero-padding for unsigned integers, address, bool, and enums; sign extension for intN; left alignment with right-side padding for bytesN; and hashing of the laid-out word. These changes satisfy issue Full details: Docstring CoverageExplanation No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (1 skipped: 1 unsupported.) ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Reads each member of a struct with one member per sub-word type family back from memory with mload and asserts the exact word Solidity's own conversion produces: unsigned, address, bool and enum are uint256(x); signed is uint256(int256(x)); bytesN is uint256(bytes32(x)). Also pins the README's two literal examples, that every sub-word member is a full word, that Foo is 0x80 bytes and that bytes1[] is a word list. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvfmeRQKubdbFW1GL3kmib
…g and string layout claims Six tests appended to test/ReadmeMemoryLayout.t.sol, each reading memory back with mload and comparing against Solidity's own pointers and conversions: - a struct member of struct type is one pointer word (Outer is 2 words; its second word is the Foo pointer), at every nesting level (Outermost > Outer > Foo); - the uint256[] and bytes members of Foo are the pointers Solidity holds for c and d; - new bytes(1) / new bytes(33) move the free memory pointer by 0x40 / 0x60 with length words 1 / 33, and new bytes(n) by 0x20 + n rounded up to a multiple of 0x20 for any n; - new string(n) allocates exactly as new bytes(n); - a string built by string.concat has the length word, bytes and free memory pointer movement of the bytes built by bytes.concat from the same content. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvfmeRQKubdbFW1GL3kmib
MemoryLayout.t.sol pins how Solidity lays out struct members, nested structs, dynamic members, bytes and string in memory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvfmeRQKubdbFW1GL3kmib
Closes #24
README L268-269 said every sub-word type is "padded with 0's such that they retain the same
uint256equivalent value". That holds for unsigned integers,address,booland enums only. In memory, signed integers are sign-extended (int8(-1)occupies0xff…ff, i.e.uint256(int256(x)), notuint256(uint8(x))=0x00…ff) andbytesNis left-aligned with zero padding on the right (bytes4(0x01020304)occupies0x01020304followed by 28 zero bytes, i.e.uint256(bytes32(x)), notuint256(uint32(x))). A reader reproducing a struct hash off-chain from "uint256 equivalent values" gets the wrong hash for any struct with a signed or fixed-bytes field. The sentence now states the layout per type family, gives the conversion that yields the word for each, and closes with the rule the hashing pattern actually depends on: the hash is of the word as laid out. The README change is only that sentence; sibling PRs edit other README sections.test/MemoryLayout.t.solpins every claim the paragraph makes. ASubWordstruct has one member per sub-word family (bool,address,uint32, enum,int8,bytes4); each testmloads that member's word and asserts it equals the word Solidity's own conversion produces for the same value. The README's two literal examples, the "occupies a full word" rule, the 4-wordFooand thebytes1[]word list each have their own test.QA
Discriminating tests:
testNestedStructIsOnePointerWord(uint256),testDeeplyNestedStructIsOnePointerWordPerLevel(uint256,uint256),testDynamicMembersArePointerWords(uint256[],bytes),testBytesAllocationRoundsUpToWords,testBytesAllocationRoundsUpToWordsForAnyLength(uint16),testNewStringAllocatesLikeNewBytes(uint16),testStringLayoutIsBytesLayout(bytes),testSubWordMembersOccupyFullWords,testBoolIsZeroPadded(bool),testAddressIsZeroPadded(address),testUnsignedIntIsZeroPadded(uint32),testEnumIsZeroPadded,testSignedIntIsSignExtended(int8),testInt8MinusOneIsAllOnes,testFixedBytesIsLeftAligned(bytes4),testBytes4ExampleIsLeftAligned,testFooIsFourWords,testBytes1ArrayIsWordList(test/MemoryLayout.t.sol) — fails on base: n/a — README-only claims; the tests pin what the README now says and fail under the README's previous wording (mutants below).Mutations applied: 57/57 KILLED, 0 survived, 0 no-run (mutation-probe, suite = CI's sol-shell
forge test, fuzz.runs 1024). Lines are in test/MemoryLayout.t.sol at this head.6 * 0x20→5 * 0x20(six sub-word members allocate five words)assertEq(ptr, fmpBefore)→fmpBefore + 0x20x ? 1 : 0→x ? 0 : 10x00→0x20(readsaddr)uint256(uint160(x))→uint256(bytes32(bytes20(x)))(left-aligned)0x20→0x40(readsu)uint256(x)→uint256(bytes32(bytes4(x)))(left-aligned)0x40→0x60(readscolour)uint256(uint8(c))→uint256(bytes32(bytes1(uint8(c))))(left-aligned)0x60→0x40(readsu)uint256(int256(x))→uint256(uint8(x))(old README claim: zero-padded)0x80→0xa0(readsb)type(uint256).max→uint256(uint8(int8(-1)))(old README claim:0x00…ff)0x80→0x60(readscolour)w != uint256(uint8(int8(-1)))→==uint256(bytes32(x))→uint256(uint32(x))(old README claim: right-aligned)0xa0→0x80(readsi)uint256(0x01020304) << 224→uint256(0x01020304)(old README claim:0x00…01020304)0xa0→0x80(readsi)w != uint256(uint32(0x01020304))→==0x80→0x60(addressnot a full word)assertEq(ptr, fmpBefore)→fmpBefore + 0x200x20 + 3 * 0x20→0x20 + 3(elements packed one byte each)uint256(bytes32(bytes1(0x01)))→uint256(uint8(0x01))(right-aligned)mload(add(arr, 0x20))→mload(arr)(first element read at the length prefix)assertEq(len, 3)→3 * 0x20assertEq(ptr, fmpBefore)→fmpBefore + 0x20Oracle: Solidity's own type conversions of the same value —
uint256(x),uint256(uint160(x)),uint256(uint8(c)),uint256(int256(x)),uint256(bytes32(x))— compared against the wordmloadreads back from the struct member (solc 0.8.25, the compiler the README's linked layout doc describes); the literal examples compare againsttype(uint256).maxanduint256(0x01020304) << 224written out. Sizes compare the free memory pointer before and after the one allocation. Nothing under test is this library.Category check: the sweep also found the README alignment claim ("the allocator will still move the free memory pointer to a multiple of 32") false for
bytes.concat/abi.encodePackedunder the legacy pipeline — not fixed here, filed as README claims the allocator always keeps the free memory pointer at a multiple of 32; bytes.concat and abi.encodePacked leave it unaligned under the legacy pipeline #38; the rounding tests above pin only thenewpath. issue asks (A) stateintNis sign-extended, (B) statebytesNis left-aligned, (C) both occupy one full word and the hash is of the word as laid out, not auint256conversion; covered A, B, C, and kept the zero-padded family (unsigned,address,bool, enums) the old sentence was true for, so the rule is stated per family rather than by two counterexamples.Notes
git-hooks.nixpre-commit installed by the sol-shell runsdeno fmtover Markdown and reflows the whole README (main does not satisfy it; CI'sstaticjob runs slither /forge fmt --check/ single-contract, not denofmt). The README commit bypasses that hook so the diff stays local to the sentence.🤖 Generated with Claude Code
https://claude.ai/code/session_01FvfmeRQKubdbFW1GL3kmib