Skip to content

Commit d147e31

Browse files
committed
docs: refine planner with domain-driven inputs and ETH supply walkthrough
- Change first input from bit width to domain max value (user knows their cap/supply, not the bit count) - Derive field width from domain max (log2 -> byte boundary) - Add intro walkthrough using ETH total supply as the single example - Fix input validation: mutual constraints via HTML min/max, byte-aligned discarded bits (step=8), field width can't drop below discarded+8 - Add hex notation throughout (inputs, results) - Prompt is now a template question for AI to derive the scheme, not a pre-computed answer - Fix per-slot to use Solidity type width, not raw encoded bits - Warn when max representable < field max (Overflow gap)
1 parent 8761cd0 commit d147e31

File tree

1 file changed

+96
-55
lines changed

1 file changed

+96
-55
lines changed

playground.html

Lines changed: 96 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@
3939
.input-field input:focus { border-color: var(--accent); }
4040
.input-field input.invalid { border-color: var(--err); }
4141
.input-field .hint { font-size: 10px; color: var(--text-dim); margin-top: 3px; }
42+
.hex-readout { font-family: var(--mono); font-size: 11px; color: var(--text-dim); margin-top: 2px; word-break: break-all; }
43+
/* Intro */
44+
.intro { margin-bottom: 32px; line-height: 1.7; }
45+
.intro h2 { font-size: 16px; font-weight: 600; color: var(--text-bright); margin-bottom: 12px; }
46+
.intro p { font-size: 13px; color: var(--text); margin-bottom: 12px; }
47+
.intro ol { font-size: 13px; color: var(--text); padding-left: 20px; margin-bottom: 12px; }
48+
.intro li { margin-bottom: 10px; }
49+
.intro li strong { color: var(--text-bright); }
50+
.intro code { font-family: var(--mono); font-size: 12px; color: var(--accent); background: var(--surface-2); padding: 1px 5px; border-radius: 3px; }
51+
.intro sup { font-size: 9px; }
52+
4253
.error-msg { color: var(--err); font-size: 11px; font-family: var(--mono); margin-top: 4px; min-height: 16px; }
4354

4455
/* Output */
@@ -135,16 +146,31 @@ <h1>uint-quantization-lib</h1>
135146
</div>
136147

137148
<div class="pg">
149+
<div class="intro">
150+
<h2>Thinking through a quantization scheme</h2>
151+
<p>Say you are building a staking contract and need to store ETH balances. ETH has 18 decimals, so 1 ETH = 10<sup>18</sup> wei. Where do you start?</p>
152+
<ol>
153+
<li><strong>What is the largest value I will ever store?</strong> The total ETH supply is ~120M ETH = 1.2 &times; 10<sup>26</sup> wei. No single balance can exceed that, so it is a safe domain max. If the field is an accumulator (e.g., total staked across all users), estimate the worst-case accumulated total instead.</li>
154+
<li><strong>How many bits does that need?</strong> log<sub>2</sub>(1.2 &times; 10<sup>26</sup>) &asymp; 86.6, so 87 bits minimum. Next byte boundary: <code>uint88</code>.</li>
155+
<li><strong>How much resolution can I sacrifice?</strong> Do you need wei-level resolution? Probably not for staking. If you accept rounding to the nearest 65,536 wei (2<sup>16</sup>), that is roughly 0.000000000000065 ETH: invisible to any user. So discarding 16 bits is safe.</li>
156+
<li><strong>What is the result?</strong> With <code>uint88</code> and 16 discarded bits: 88 &minus; 16 = 72 encoded bits, stored as <code>uint72</code>. The call is <code>create(16, 72)</code>.</li>
157+
<li><strong>What do I lose?</strong> Max error per value is 2<sup>16</sup> &minus; 1 = 65,535 wei &asymp; 0.000000000000065 ETH. Values within 65,535 of <code>uint88.max</code> will revert with <code>Overflow</code>. Both are negligible for ETH staking. If you want zero error, enforce step-aligned inputs: the library provides <code>requireAligned()</code> and <code>encode(value, true)</code> which revert on non-aligned values, guaranteeing lossless round-trips.</li>
158+
</ol>
159+
<p>Enter your domain max below to run this reasoning for your own field.</p>
160+
</div>
161+
138162
<div class="input-row">
139163
<div class="input-field">
140164
<label for="maxInput">Domain maximum</label>
141-
<input type="text" id="maxInput" value="1000000000000000000" placeholder="e.g., 1e21" spellcheck="false" autocomplete="off">
142-
<div class="hint">largest value this field will ever hold</div>
165+
<input type="text" id="maxInput" value="120000000e18" placeholder="e.g., 120000000e18" spellcheck="false" autocomplete="off">
166+
<div class="hint">your cap, worst-case accumulation, or supply bound in base units</div>
167+
<div class="hex-readout" id="maxHex"></div>
143168
</div>
144169
<div class="input-field">
145-
<label for="stepInput">Acceptable step size</label>
146-
<input type="text" id="stepInput" value="65536" placeholder="e.g., 1e9" spellcheck="false" autocomplete="off">
147-
<div class="hint">smallest meaningful difference (1 = exact)</div>
170+
<label for="stepInput">Discarded bits</label>
171+
<input type="number" id="stepInput" value="16" min="0" max="248" step="8">
172+
<div class="hint">low-order bits to discard, multiples of 8 (0 = none)</div>
173+
<div class="hex-readout" id="stepHex"></div>
148174
</div>
149175
</div>
150176
<div class="error-msg" id="errorMsg"></div>
@@ -188,47 +214,39 @@ <h1>uint-quantization-lib</h1>
188214
return '~' + s[0] + '.' + s.slice(1, 5) + 'e' + (s.length - 1);
189215
}
190216
function fmtFull(n) { return n.toString(); }
217+
function fmtHex(n) { return '0x' + n.toString(16); }
191218

192219
function bitLength(n) {
193220
if (n <= 0n) return 0;
194221
return n.toString(2).length;
195222
}
196223

197224
// --- Derive scheme ---
198-
function derive(maxVal, resolution) {
199-
if (maxVal <= 0n) return { valid: false, error: 'Domain max must be > 0' };
200-
if (resolution <= 0n) return { valid: false, error: 'Step size must be > 0' };
201-
if (maxVal > UINT256_MAX) return { valid: false, error: 'Exceeds uint256' };
202-
203-
// d = floor(log2(resolution))
204-
const d = resolution > 1n ? bitLength(resolution) - 1 : 0;
205-
const step = 1n << BigInt(d);
206-
207-
// e = bits needed for ceil(maxVal / step)
208-
const dn = BigInt(d);
209-
const encodedMax = (maxVal + step - 1n) >> dn; // ceiling division
210-
const e = encodedMax > 0n ? bitLength(encodedMax) : 1;
225+
function derive(domainMax, dBits) {
226+
if (domainMax <= 0n) return { valid: false, error: 'Domain max must be > 0' };
227+
if (domainMax > UINT256_MAX) return { valid: false, error: 'Exceeds uint256' };
211228

212-
if (d + e > 256) return { valid: false, error: `Needs ${d + e} bits (exceeds 256). Increase step size or reduce domain max.` };
229+
// Derive field width from domain max
230+
const rawBits = bitLength(domainMax);
231+
const nativeBits = Math.ceil(rawBits / 8) * 8; // byte-aligned
232+
const nativeType = 'uint' + nativeBits;
233+
const fieldMax = (1n << BigInt(nativeBits)) - 1n;
213234

214-
// Native (no quantization) comparison
215-
const nativeBits = maxVal > 0n ? bitLength(maxVal) : 1;
216-
const nativeType = 'uint' + (Math.ceil(nativeBits / 8) * 8);
235+
if (dBits < 0) return { valid: false, error: 'Discarded bits must be >= 0' };
236+
if (dBits >= nativeBits) return { valid: false, error: 'Discarded bits (' + dBits + ') must be < derived field width (' + nativeBits + ')' };
217237

218-
const actualMax = ((1n << BigInt(e)) - 1n) << dn;
238+
const d = dBits;
239+
const e = nativeBits - dBits;
240+
const step = 1n << BigInt(d);
241+
const actualMax = ((1n << BigInt(e)) - 1n) << BigInt(d);
219242
const uintBits = Math.ceil(e / 8) * 8;
220-
const uintType = uintBits > 256 ? 'uint256' : 'uint' + uintBits;
221-
const perSlot = Math.floor(256 / e);
222-
const slotWaste = 256 - perSlot * e;
243+
const uintType = 'uint' + uintBits;
223244
const maxError = step - 1n;
224245
const identity = d === 0;
225246

226247
return {
227-
valid: true, d, e, step, actualMax, uintType, uintBits,
228-
nativeBits, nativeType,
229-
perSlot, slotWaste, maxError,
230-
totalBits: d + e, identity,
231-
coversMax: actualMax >= maxVal
248+
valid: true, d, e, step, actualMax, fieldMax, domainMax, uintType, uintBits,
249+
rawBits, nativeBits, nativeType, maxError, identity
232250
};
233251
}
234252

@@ -242,20 +260,39 @@ <h1>uint-quantization-lib</h1>
242260

243261
// --- Render ---
244262
function update() {
245-
const maxVal = parseValue(maxInput.value);
246-
const resolution = parseValue(stepInput.value);
247-
248-
maxInput.classList.toggle('invalid', maxInput.value.trim() && maxVal === null);
249-
stepInput.classList.toggle('invalid', stepInput.value.trim() && resolution === null);
263+
const domainMax = parseValue(maxInput.value);
264+
const dBits = parseInt(stepInput.value);
265+
266+
const maxValid = domainMax !== null && domainMax > 0n && domainMax <= UINT256_MAX;
267+
const dValid = !isNaN(dBits) && dBits >= 0 && dBits % 8 === 0;
268+
maxInput.classList.toggle('invalid', maxInput.value.trim() && !maxValid);
269+
stepInput.classList.toggle('invalid', stepInput.value.trim() && !dValid);
270+
271+
if (maxValid) {
272+
const rawBits = bitLength(domainMax);
273+
const nb = Math.ceil(rawBits / 8) * 8;
274+
const nt = 'uint' + nb;
275+
const fm = (1n << BigInt(nb)) - 1n;
276+
document.getElementById('maxHex').textContent = 'needs ' + rawBits + ' bits \u2192 ' + nt + ' (max ' + fmtHex(fm) + ')';
277+
stepInput.max = nb - 8;
278+
} else {
279+
document.getElementById('maxHex').textContent = '';
280+
}
281+
if (dValid) {
282+
const step = 1n << BigInt(dBits);
283+
document.getElementById('stepHex').textContent = 'step = ' + fmtHex(step) + ' (' + fmt(step) + ')';
284+
} else {
285+
document.getElementById('stepHex').textContent = '';
286+
}
250287

251-
if (maxVal === null || resolution === null) {
288+
if (!maxValid || !dValid) {
252289
errorMsg.textContent = '';
253290
output.classList.remove('visible');
254291
promptText.textContent = '';
255292
return;
256293
}
257294

258-
const r = derive(maxVal, resolution);
295+
const r = derive(domainMax, dBits);
259296

260297
if (!r.valid) {
261298
errorMsg.textContent = r.error;
@@ -270,40 +307,35 @@ <h1>uint-quantization-lib</h1>
270307
if (r.identity) {
271308
output.innerHTML = `
272309
<div class="no-quant">
273-
<strong>No quantization needed.</strong> Your field fits natively in <strong>${r.nativeType}</strong> (${r.nativeBits} bits).
274-
Store it directly without the library.
275-
</div>
276-
<div class="code-wrap">
277-
<div class="code-block"><span class="cm">// No quantization needed: ${r.nativeType} covers your domain max</span>
278-
<span class="ty">${r.nativeType}</span> myField;</div>
279-
<button class="code-copy" onclick="copyCode(this)">Copy</button>
310+
<strong>0 bits discarded:</strong> domain max ${fmt(r.domainMax)} needs ${r.rawBits} bits, stored as <strong>${r.nativeType}</strong>. No compression.
311+
Increase discarded bits to reduce the encoded width.
280312
</div>
281313
`;
282-
promptText.textContent = `Field with domain max ${fmt(maxVal)}: no quantization needed. Use ${r.nativeType} natively (${r.nativeBits} bits).`;
314+
promptText.textContent = `Domain max ${fmt(r.domainMax)} needs ${r.rawBits} bits (${r.nativeType}), 0 bits discarded: no compression.`;
283315
return;
284316
}
285317

286-
const saved = Math.ceil(r.nativeBits / 8) * 8 - r.uintBits;
318+
const saved = r.nativeBits - r.uintBits;
287319

288320
output.innerHTML = `
289321
<div class="compare">
290322
<div class="compare-box before">
291323
<div class="type">${r.nativeType}</div>
292-
<div class="sub">native (${r.nativeBits} bits)</div>
324+
<div class="sub">${r.rawBits} bits needed, ${r.nativeBits}-bit type</div>
293325
</div>
294326
<div class="compare-arrow">&rarr;</div>
295327
<div class="compare-box after">
296328
<div class="type">${r.uintType}</div>
297-
<div class="sub">quantized (${r.e} bits encoded)</div>
329+
<div class="sub">${r.nativeBits} &minus; ${r.d} = ${r.e} bits encoded</div>
298330
</div>
299331
${saved > 0 ? `<div class="compare-save">&minus;${saved} bits</div>` : ''}
300332
</div>
301333
<table class="result-table">
302-
<tr><td>Scheme</td><td>create(${r.d}, ${r.e})<span class="sub">discardedBitWidth = ${r.d}, encodedBitWidth = ${r.e}</span></td></tr>
303-
<tr><td>Actual step</td><td>${fmt(r.step)}<span class="sub">2^${r.d}${r.step <= resolution ? ' (within your tolerance)' : ''}</span></td></tr>
304-
<tr><td>Max representable</td><td>${fmt(r.actualMax)}<span class="sub ${r.coversMax ? 'ok' : 'warn'}">${r.coversMax ? 'covers your domain max' : 'does NOT cover domain max'}</span></td></tr>
305-
<tr><td>Max error</td><td>${fmt(r.maxError)}<span class="sub">worst-case loss per value (step &minus; 1)</span></td></tr>
306-
<tr><td>Per slot</td><td>${r.perSlot} value${r.perSlot === 1 ? '' : 's'}<span class="sub">${r.slotWaste > 0 ? r.slotWaste + ' bits unused per slot' : 'exact fit'}</span></td></tr>
334+
<tr><td>Domain max</td><td>${fmt(r.domainMax)}<span class="sub">${fmtHex(r.domainMax)} (needs ${r.rawBits} bits &rarr; ${r.nativeType})</span></td></tr>
335+
<tr><td>Scheme</td><td>create(${r.d}, ${r.e})<span class="sub">discard ${r.d} low bits, keep ${r.e} bits</span></td></tr>
336+
<tr><td>Step size</td><td>${fmtHex(r.step)}<span class="sub">2^${r.d} = ${fmt(r.step)}</span></td></tr>
337+
<tr><td>Max representable</td><td>${fmtHex(r.actualMax)}<span class="sub">${fmt(r.actualMax)}${r.actualMax < r.fieldMax ? ' (values above this revert with Overflow)' : ''}</span></td></tr>
338+
<tr><td>Max error</td><td>${fmtHex(r.maxError)}<span class="sub">${fmt(r.maxError)} (step &minus; 1)</span></td></tr>
307339
</table>
308340
<div class="code-wrap">
309341
<div class="code-block"><span class="kw">import</span> {<span class="ty">Quant</span>, <span class="ty">UintQuantizationLib</span> <span class="kw">as</span> QuantLib} <span class="kw">from</span> <span class="fn">"uint-quantization-lib/src/UintQuantizationLib.sol"</span>;
@@ -319,7 +351,14 @@ <h1>uint-quantization-lib</h1>
319351
</div>
320352
`;
321353

322-
promptText.textContent = `Field with domain max ${fmt(maxVal)} and acceptable step ${fmt(resolution)}: use create(${r.d}, ${r.e}). Stores in ${r.uintType} (down from ${r.nativeType}). Step size ${fmt(r.step)} (2^${r.d}), max representable ${fmt(r.actualMax)}, worst-case error ${fmt(r.maxError)} per value. ${r.perSlot} value${r.perSlot === 1 ? '' : 's'} per uint256 slot.`;
354+
let p = `I'm building a Solidity contract that stores __________ (describe your field: e.g., ETH staking balances, token amounts, timestamps).\n\n`;
355+
p += `Using uint-quantization-lib (https://github.com/0xferit/uint-quantization-lib), help me:\n`;
356+
p += `1. Determine the domain max for this field (contract cap, supply bound, or worst-case accumulation)\n`;
357+
p += `2. Derive the minimum bit width and native Solidity type\n`;
358+
p += `3. Decide how many bits of resolution to discard and justify the tradeoff\n`;
359+
p += `4. Compute the quantization scheme: create(discardedBitWidth, encodedBitWidth)\n`;
360+
p += `5. Show the Solidity integration code with encode/decode calls`;
361+
promptText.textContent = p;
323362
}
324363

325364
function copyCode(btn) {
@@ -333,7 +372,9 @@ <h1>uint-quantization-lib</h1>
333372

334373
// --- Events ---
335374
maxInput.addEventListener('input', update);
375+
maxInput.addEventListener('change', update);
336376
stepInput.addEventListener('input', update);
377+
stepInput.addEventListener('change', update);
337378

338379
copyBtn.addEventListener('click', () => {
339380
navigator.clipboard.writeText(promptText.textContent).then(() => {

0 commit comments

Comments
 (0)