Skip to content

Commit dacc649

Browse files
committed
Rename shift/targetBits to discardedBitWidth/encodedBitWidth across library, tests, and docs
1 parent e3e02c1 commit dacc649

File tree

4 files changed

+115
-112
lines changed

4 files changed

+115
-112
lines changed

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
`uint-quantization-lib` is a pure-function Solidity library for shift-based `uint256` lossy compression. The core mechanism is floor quantization via right-shifting via the `UintQuantizationLib` library. A `Quant` value packs `(shift, targetBits)` into a single `uint16`, making the compression scheme explicit and reusable. The recommended pattern is `immutable` + `create(shift, targetBits)` for readability.
7+
`uint-quantization-lib` is a pure-function Solidity library for shift-based `uint256` lossy compression. The core mechanism is floor quantization via right-shifting via the `UintQuantizationLib` library. A `Quant` value packs `(discardedBitWidth, encodedBitWidth)` into a single `uint16`, making the compression scheme explicit and reusable. The recommended pattern is `immutable` + `create(discardedBitWidth, encodedBitWidth)` for readability.
88

99
## Commands
1010

@@ -37,13 +37,13 @@ forge test --match-path test/showcase/ShowcaseGas.t.sol --gas-report -vv
3737

3838
### Source: `src/`
3939

40-
- `UintQuantizationLib.sol`: UDT `Quant` packing `(shift, targetBits)` into `uint16` (bits 0-7 = shift, bits 8-15 = targetBits). All functions are `internal pure`. Errors are file-level (not inside the library) and attached to the type. `using UintQuantizationLib for Quant global` at the bottom of the file propagates method-call binding to all importers automatically.
40+
- `UintQuantizationLib.sol`: UDT `Quant` packing `(discardedBitWidth, encodedBitWidth)` into `uint16` (bits 0-7 = discardedBitWidth, bits 8-15 = encodedBitWidth). All functions are `internal pure`. Errors are file-level (not inside the library) and attached to the type. `using UintQuantizationLib for Quant global` at the bottom of the file propagates method-call binding to all importers automatically.
4141

4242
- `src/showcase/ShowcaseSolidityFixtures.sol`: Production-style showcase contracts demonstrating gas savings. `RawETHStakingShowcase` vs `QuantizedETHStakingShowcase` (real-life staking) and `RawExtremePackingShowcase` vs `QuantizedExtremePackingShowcase` (12 slots -> 1 slot). Used only for benchmarking. Both quantized contracts use `UintQuantizationLib`.
4343

4444
### Tests: `test/`
4545

46-
- `test/UintQuantizationLib.t.sol`: Foundry test file. Contains `QuantHarness` (exposes method-call syntax) and `UintQuantizationLibSmokeTest`. Smoke tests cover `create` validation and all revert paths. Fuzz tests use `uint8` for shift and targetBits and use `bound()` instead of `vm.assume` for value-in-range constraints.
46+
- `test/UintQuantizationLib.t.sol`: Foundry test file. Contains `QuantHarness` (exposes method-call syntax) and `UintQuantizationLibSmokeTest`. Smoke tests cover `create` validation and all revert paths. Fuzz tests use `uint8` for discardedBitWidth and encodedBitWidth and use `bound()` instead of `vm.assume` for value-in-range constraints.
4747

4848
- `test/showcase/ShowcaseGas.t.sol`: Benchmark assertions. Enforces quantized paths save >= 32% (real-life) and >= 80% (extreme) gas vs raw paths on zero-to-nonzero writes.
4949

README.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,32 +38,32 @@ The `Quant` value type is a `uint16` with the following bit layout:
3838

3939
| Bits | Field | Notes |
4040
|---|---|---|
41-
| 0-7 | `shift` | LSBs discarded during encoding |
42-
| 8-15 | `targetBits` | Bit-width of the encoded value |
41+
| 0-7 | `discardedBitWidth` | LSBs discarded during encoding |
42+
| 8-15 | `encodedBitWidth` | Bit-width of the encoded value |
4343

4444
### API
4545

4646
| Function | Description |
4747
|---|---|
48-
| `UintQuantizationLib.create(shift, targetBits)` | Creates a `Quant` scheme from readable parameters. Reverts with `BadConfig` when shift >= 256, targetBits == 0, targetBits >= 256, or shift + targetBits > 256. |
49-
| `q.shift()` | Number of low bits discarded during encoding (set at creation). |
50-
| `q.targetBits()` | Bit-width of the encoded value (set at creation). |
51-
| `q.encode(value)` | Floor-encodes `value`. Reverts with `Overflow` when `value > max(q)`. |
52-
| `q.encode(value, true)` | Strict mode: also reverts with `NotAligned` when `value` is not step-aligned. |
53-
| `q.decode(encoded)` | Left-shifts `encoded` by shift, restoring discarded bits as zeros (lower bound). |
54-
| `q.decodeMax(encoded)` | Like `decode` but fills discarded bits with ones (upper bound within the step). |
55-
| `q.fits(value)` | Returns `true` when `value <= max(q)`. |
48+
| `UintQuantizationLib.create(discardedBitWidth, encodedBitWidth)` | Creates a `Quant` scheme. Reverts with `BadConfig` on invalid parameters. |
49+
| `q.discardedBitWidth()` | Number of low bits discarded during encoding (set at creation). |
50+
| `q.encodedBitWidth()` | Bit-width of the encoded value (set at creation). |
51+
| `q.encode(value)` | Compresses `value` by discarding the low bits (floor). Reverts with `Overflow` if `value > max(q)`. |
52+
| `q.encode(value, true)` | Same as `encode(value)`, but also reverts with `NotAligned` if `value` is not step-aligned. |
53+
| `q.decode(encoded)` | Decompresses `encoded` back to the original scale. Discarded bits are restored as zeros (lower bound). |
54+
| `q.decodeMax(encoded)` | Like `decode`, but fills discarded bits with ones (upper bound within the step). |
55+
| `q.fits(value)` | True if `value` fits within the scheme's representable range. |
5656
| `q.floor(value)` | Rounds `value` down to the nearest step boundary. |
5757
| `q.ceil(value)` | Rounds `value` up to the nearest step boundary. |
58-
| `q.remainder(value)` | Returns discarded low bits (`value mod stepSize`). |
59-
| `q.isAligned(value)` | Returns `true` when `value` is exactly representable (step-aligned). |
60-
| `q.stepSize()` | Returns `2^shift`. |
61-
| `q.max()` | Returns the maximum original value representable: `(2^targetBits - 1) << shift`. |
58+
| `q.remainder(value)` | Resolution lost if `value` were floor-encoded (`value mod stepSize`). |
59+
| `q.isAligned(value)` | True if `value` is step-aligned (no resolution loss on encode). |
60+
| `q.stepSize()` | Smallest non-zero value the scheme can represent (`2^discardedBitWidth`). |
61+
| `q.max()` | Largest value the scheme can represent: `(2^encodedBitWidth - 1) << discardedBitWidth`. |
6262

6363
### Errors
6464

6565
```solidity
66-
error BadConfig(uint256 shift, uint256 targetBits);
66+
error BadConfig(uint256 discardedBitWidth, uint256 encodedBitWidth);
6767
error Overflow(uint256 value, uint256 max);
6868
error NotAligned(uint256 value, uint256 stepSize);
6969
```
@@ -132,8 +132,8 @@ contract StakingVault {
132132
```
133133

134134
> `encode(value)` and `encode(value, true)` return `uint256` due to Solidity type constraints. The encoded
135-
> result is guaranteed to fit in `2^targetBits - 1`, so store it using the matching `uintN` for
136-
> your scheme (for example, `uint16` for `targetBits=16`, `uint24` for `targetBits=24`). Using a
135+
> result is guaranteed to fit in `2^encodedBitWidth - 1`, so store it using the matching `uintN` for
136+
> your scheme (for example, `uint16` for `encodedBitWidth=16`, `uint24` for `encodedBitWidth=24`). Using a
137137
> smaller type will silently truncate.
138138
139139
## Which encode function should I use?

src/UintQuantizationLib.sol

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ pragma solidity ^0.8.25;
77
* @custom:security-contact ferit@cryptolab.net
88
* @notice Pure-function library for shift-based uint256 compression using a bundled config type.
99
*
10-
* The `Quant` value type packs a `(shift, targetBits)` scheme into a single `uint16`,
10+
* The `Quant` value type packs a `(discardedBitWidth, encodedBitWidth)` scheme into a single `uint16`,
1111
* allowing callers to define the compression config once and invoke methods on it.
1212
* Type layout (uint16):
13-
* bits 0-7 → shift (LSBs discarded during encoding)
14-
* bits 8-15 → targetBits (bit-width of the encoded value)
13+
* bits 0-7 → discardedBitWidth (LSBs discarded during encoding)
14+
* bits 8-15 → encodedBitWidth (bit-width of the encoded value)
1515
*
1616
* Usage:
1717
* ```solidity
@@ -31,8 +31,8 @@ error Overflow(uint256 value, uint256 max);
3131
/// @notice Thrown by `encode` (precise mode) when a value is not aligned to the step size.
3232
error NotAligned(uint256 value, uint256 stepSize);
3333

34-
/// @notice Thrown by `create` when the (shift, targetBits) pair is invalid.
35-
error BadConfig(uint256 shift, uint256 targetBits);
34+
/// @notice Thrown by `create` when the (discardedBitWidth, encodedBitWidth) pair is invalid.
35+
error BadConfig(uint256 discardedBitWidth, uint256 encodedBitWidth);
3636

3737
library UintQuantizationLib {
3838
string internal constant VERSION = "1.1.0";
@@ -41,44 +41,47 @@ library UintQuantizationLib {
4141
// Factory
4242
// -------------------------------------------------------------------------
4343

44-
/// @notice Creates a `Quant` scheme from shift and targetBits.
45-
/// @dev Reverts when shift >= 256, targetBits == 0, targetBits >= 256, or
46-
/// shift + targetBits > 256. Any of these conditions would produce a scheme
44+
/// @notice Creates a `Quant` scheme from discardedBitWidth and encodedBitWidth.
45+
/// @dev Reverts when discardedBitWidth >= 256, encodedBitWidth == 0, encodedBitWidth >= 256, or
46+
/// discardedBitWidth + encodedBitWidth > 256. Any of these conditions would produce a scheme
4747
/// where the computed max overflows or the step size is undefined.
48-
function create(uint256 shift_, uint256 targetBits_) internal pure returns (Quant) {
49-
if (shift_ >= 256 || targetBits_ == 0 || targetBits_ >= 256 || shift_ + targetBits_ > 256) {
50-
revert BadConfig(shift_, targetBits_);
48+
function create(uint256 discardedBitWidth_, uint256 encodedBitWidth_) internal pure returns (Quant) {
49+
if (
50+
discardedBitWidth_ >= 256 || encodedBitWidth_ == 0 || encodedBitWidth_ >= 256
51+
|| discardedBitWidth_ + encodedBitWidth_ > 256
52+
) {
53+
revert BadConfig(discardedBitWidth_, encodedBitWidth_);
5154
}
52-
// casting to uint16 is safe: create guard above ensures shift_ < 256 and targetBits_ < 256,
53-
// so (targetBits_ << 8) | shift_ <= 0xFF00 | 0xFF = 0xFFFF, which fits in uint16.
55+
// casting to uint16 is safe: create guard above ensures discardedBitWidth_ < 256 and encodedBitWidth_ < 256,
56+
// so (encodedBitWidth_ << 8) | discardedBitWidth_ <= 0xFF00 | 0xFF = 0xFFFF, which fits in uint16.
5457
// forge-lint: disable-next-line(unsafe-typecast)
55-
return Quant.wrap(uint16((targetBits_ << 8) | shift_));
58+
return Quant.wrap(uint16((encodedBitWidth_ << 8) | discardedBitWidth_));
5659
}
5760

5861
// -------------------------------------------------------------------------
5962
// Accessors
6063
// -------------------------------------------------------------------------
6164

62-
/// @notice Returns the shift component of the scheme (bits 0-7).
63-
function shift(Quant q) internal pure returns (uint256) {
65+
/// @notice Returns the discardedBitWidth component of the scheme (bits 0-7).
66+
function discardedBitWidth(Quant q) internal pure returns (uint256) {
6467
return uint256(Quant.unwrap(q)) & 0xFF;
6568
}
6669

67-
/// @notice Returns the targetBits component of the scheme (bits 8-15).
68-
function targetBits(Quant q) internal pure returns (uint256) {
70+
/// @notice Returns the encodedBitWidth component of the scheme (bits 8-15).
71+
function encodedBitWidth(Quant q) internal pure returns (uint256) {
6972
return uint256(Quant.unwrap(q)) >> 8;
7073
}
7174

72-
/// @notice Returns 2^shift: the quantization step size.
75+
/// @notice Returns 2^discardedBitWidth: the quantization step size.
7376
function stepSize(Quant q) internal pure returns (uint256) {
74-
return uint256(1) << shift(q);
77+
return uint256(1) << discardedBitWidth(q);
7578
}
7679

7780
/// @notice Returns the maximum original value representable by this scheme.
78-
/// @dev Safe when the scheme was created via `create`: shift + targetBits <= 256 and
79-
/// targetBits < 256 guarantee the result fits in uint256.
81+
/// @dev Safe when the scheme was created via `create`: discardedBitWidth + encodedBitWidth <= 256 and
82+
/// encodedBitWidth < 256 guarantee the result fits in uint256.
8083
function max(Quant q) internal pure returns (uint256) {
81-
return ((uint256(1) << targetBits(q)) - 1) << shift(q);
84+
return ((uint256(1) << encodedBitWidth(q)) - 1) << discardedBitWidth(q);
8285
}
8386

8487
// -------------------------------------------------------------------------
@@ -89,15 +92,15 @@ library UintQuantizationLib {
8992
function encode(Quant q, uint256 value) internal pure returns (uint256) {
9093
uint256 m = max(q);
9194
if (value > m) revert Overflow(value, m);
92-
return value >> shift(q);
95+
return value >> discardedBitWidth(q);
9396
}
9497

9598
/// @notice Encodes `value`. When `precise` is true, reverts if value is not step-aligned.
9699
/// Always reverts if value exceeds `max(q)`.
97100
function encode(Quant q, uint256 value, bool precise) internal pure returns (uint256) {
98101
uint256 m = max(q);
99102
if (value > m) revert Overflow(value, m);
100-
uint256 s = shift(q);
103+
uint256 s = discardedBitWidth(q);
101104
if (precise) {
102105
uint256 step = uint256(1) << s;
103106
if (value & (step - 1) != 0) revert NotAligned(value, step);
@@ -109,21 +112,21 @@ library UintQuantizationLib {
109112
// Decoding
110113
// -------------------------------------------------------------------------
111114

112-
/// @notice Left-shifts `encoded` by shift, restoring discarded bits as zeros (lower bound).
113-
/// @dev The caller must ensure `encoded < 2**targetBits(q)`. Passing a larger value
115+
/// @notice Left-shifts `encoded` by discardedBitWidth, restoring discarded bits as zeros (lower bound).
116+
/// @dev The caller must ensure `encoded < 2**encodedBitWidth(q)`. Passing a larger value
114117
/// produces a result that may silently wrap or exceed the scheme's representable range.
115118
/// Values returned by `encode` always satisfy this constraint.
116119
function decode(Quant q, uint256 encoded) internal pure returns (uint256) {
117120
unchecked {
118-
return encoded << shift(q);
121+
return encoded << discardedBitWidth(q);
119122
}
120123
}
121124

122125
/// @notice Like `decode` but fills the discarded bits with ones (upper bound within the step).
123-
/// @dev Same precondition as `decode`: `encoded` must be less than `2**targetBits(q)`.
126+
/// @dev Same precondition as `decode`: `encoded` must be less than `2**encodedBitWidth(q)`.
124127
function decodeMax(Quant q, uint256 encoded) internal pure returns (uint256) {
125128
unchecked {
126-
uint256 s = shift(q);
129+
uint256 s = discardedBitWidth(q);
127130
return (encoded << s) | ((uint256(1) << s) - 1);
128131
}
129132
}
@@ -134,7 +137,7 @@ library UintQuantizationLib {
134137

135138
/// @notice Returns the bits discarded during floor encoding (value mod stepSize).
136139
function remainder(Quant q, uint256 value) internal pure returns (uint256) {
137-
return value & ((uint256(1) << shift(q)) - 1);
140+
return value & ((uint256(1) << discardedBitWidth(q)) - 1);
138141
}
139142

140143
/// @notice Returns true when `value` is exactly representable (step-aligned).
@@ -147,17 +150,17 @@ library UintQuantizationLib {
147150
return value <= max(q);
148151
}
149152

150-
/// @notice Rounds `value` down to the nearest step boundary (clears low `shift` bits).
153+
/// @notice Rounds `value` down to the nearest step boundary (clears low `discardedBitWidth` bits).
151154
function floor(Quant q, uint256 value) internal pure returns (uint256) {
152-
return value & ~((uint256(1) << shift(q)) - 1);
155+
return value & ~((uint256(1) << discardedBitWidth(q)) - 1);
153156
}
154157

155158
/// @notice Rounds `value` up to the nearest step boundary. Returns `value` unchanged when
156-
/// shift is 0 or `value` is already aligned.
159+
/// discardedBitWidth is 0 or `value` is already aligned.
157160
/// @dev Callers must ensure `value + stepSize - 1 <= type(uint256).max` to avoid overflow
158161
/// on non-aligned inputs. This function does not perform that check.
159162
function ceil(Quant q, uint256 value) internal pure returns (uint256) {
160-
uint256 s = shift(q);
163+
uint256 s = discardedBitWidth(q);
161164
if (s == 0) return value;
162165
uint256 mask = (uint256(1) << s) - 1;
163166
if (value & mask == 0) return value;

0 commit comments

Comments
 (0)