Skip to content

format, pointers, bugc: specify and implement two-sorted (integer/bytes) expression evaluation - #286

Open
gnidan wants to merge 9 commits into
mainfrom
architect-expression-semantics
Open

format, pointers, bugc: specify and implement two-sorted (integer/bytes) expression evaluation#286
gnidan wants to merge 9 commits into
mainfrom
architect-expression-semantics

Conversation

@gnidan

@gnidan gnidan commented Aug 6, 2026

Copy link
Copy Markdown
Member

Pointer expressions had an unstated gap: the width of a value was undefined for numeric literals, arithmetic results, and odd-digit hex, yet $concat and $keccak256 depend on operand widths — so a compiler and a debugger could legitimately disagree on the bytes produced. This specifies the evaluation model and brings the reference implementation, the compiler, and the docs along with it.

Semantics

Expressions evaluate to a value of one of two sorts:

  • an integer — an unbounded, non-negative integer with no width;
  • bytes — a finite byte sequence with a definite width.

The forms are sorted accordingly. Integers: a JSON-number literal, $wordsize, a variable or lookup, arithmetic ($sum/$difference/$product/$quotient/$remainder), and an odd-digit hex literal (which has no whole-byte width). Bytes: an even-digit hex literal (width = bytes written), $read (width = region length), $sizedN/$wordsized (width = N / word size), $keccak256 (width 32), and $concat (width = sum of operand widths).

Coercion runs one way. Where an integer is expected — arithmetic operands, a list count, a segment slot/offset/length — a bytes value is read as the non-negative integer its bytes encode (big-endian). Where bytes are expected — the operands of $concat and $keccak256 — the operand must be width-bearing; a bare integer there is an error and must be given a width with $sizedN/$wordsized first. The resize forms are the only bridge from an integer to bytes; there is no implicit widening.

This codifies what the mapping/string pointer examples already do (they word-size a key and slot before hashing). Two schema examples that hashed bare integers are corrected: the keccak256 definition's example and the expression schema's root-level example.

Reference implementation

@ethdebug/pointers now evaluates to a tagged Value — integer or bytes — with the sort rules above, coerces where an integer is expected, and throws on a bare-integer operand to $concat or $keccak256 with a message naming the operand and suggesting $wordsized/$sizedN. Variables carry the sort of their defining expression. This is package-internal: evaluate is not exported from the package index, and the public Cursor.Region shape is unchanged, so no consumers needed edits. The previous single-width implementation gave a JSON-number literal minimal width (literal 0 was zero bytes), so a $keccak256 over a bare slot silently hashed the wrong preimage; that can no longer happen.

The evaluator's previously skipped keccak test is reinstated, and the evaluator test file now covers every sort rule, both coercion directions, and the error cases. A new test evaluates every $keccak256/$concat occurrence in the pointer schema examples so an example without operand widths fails the suite.

Compiler

bugc's slot-computation helpers emitted $keccak256 with a bare slot operand. They now word-size it, with a nested $keccak256 passed through since it is already 32-byte bytes. These helpers have no production callers today — bugc emits a mapping variable as its base slot — so emitted debug info was already valid; the helpers and their pinned tests are corrected so that any future use is too.

Docs

The expression spec page no longer says values are "assumed to be left-padded to the bytes width appropriate for the context"; it states the two sorts, the even/odd hex rule, and the width-bearing requirement. The pointer expressions guide gains an "Integers and bytes" section and word-sizes the operands in its hashing examples. The evaluation implementation guide follows the new evaluator's shape.

Note: the integer/bytes sorting discipline is meant to carry over into a future extraction of the expression language into a shared facility.

gnidan added 2 commits August 5, 2026 20:17
DRAFT for writer prose pass before the PR opens.

Codifies the two-sorted semantics of pointer expressions: values are
either unbounded non-negative integers (no width) or bytes (with a
definite width). Arithmetic, JSON-number and odd-nibble-hex literals,
$wordsize, and lookups are integers; even-nibble hex, $read, and
$sizedN/$wordsized are bytes. An integer is accepted wherever an integer
is expected (a bytes value is read as its big-endian integer value), but
$concat and $keccak256 operands must be width-bearing bytes — a bare
integer there is an error and must be resized first. Fixes the keccak256
example's bare-integer operand to be word-sized, matching what the
mapping/string pointer examples already do.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ethdebug.github.io/format/pr-preview/pr-286/

Built to branch gh-pages at 2026-09-03 15:33 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

The evaluator used a single width-carrying Data representation for
every expression, so a JSON number, $wordsize, or an arithmetic result
had an incidental width that leaked into $concat and $keccak256: for
instance `{ $keccak256: [{ $wordsized: k }, 0] }` hashed 32 bytes
rather than 64.

evaluate() now returns a Value of one of two sorts, following the
expression schema:

- integer (bigint, no width): JSON numbers, $wordsize, lookups,
  arithmetic results, and odd-digit hex literals
- bytes (Data, definite width): even-digit hex literals, $read, and
  the resize forms

Arithmetic takes its operands as integers (bytes coerce big-endian)
and yields an integer. $concat and $keccak256 require bytes operands
and throw, naming the offending operand and suggesting $wordsized or
$sizedN, when given a bare integer. Resize is the only integer-to-bytes
bridge. Variables carry the sort of their defining expression; list
counts, conditionals, and region slot/offset/length coerce to
integers. Cursor.Region is unchanged: its Data-valued properties now
encode integers minimally, with bytes keeping their width.

The previously skipped keccak256 test is rewritten against these
rules, along with tests for each sort rule, both coercion directions,
and the bare-integer errors.

Claude-Session: https://claude.ai/code/session_01RJFyifZxcSXZchLFNTNPuT
The expression schema's root-level `$keccak256` example still hashed a
bare JSON number and a bare lookup, which the two-sorted rules reject.
Wrap both operands in `$wordsized`, as the Keccak256 example already
does.

pointers: add a test that evaluates every `$keccak256` / `$concat`
expression found in the pointer schemas' examples against stubbed
regions and variables, so an example whose operand lacks a width fails
the suite rather than silently contradicting the spec.

Claude-Session: https://claude.ai/code/session_01RJFyifZxcSXZchLFNTNPuT
The operands of $keccak256 must be width-bearing bytes. The mapping and
dynamic-array slot helpers in irgen/debug/pointers.ts passed the slot
operand bare (a number literal or $sum), which is invalid under the
two-sorted expression rules and hashes the wrong number of bytes: the
EVM hashes key‖slot as two 32-byte words, and a bare literal has no
width. Wrap the slot operand in $wordsized; a nested $keccak256 result
is already 32 bytes wide and is passed through unwrapped.

Note that program output is not affected: variables.ts already
wordsizes dynamic-array base slots, and mapping variables are emitted
as their base slot only (translateComputeSlotChain and the access
helpers have no production callers yet).

Claude-Session: https://claude.ai/code/session_01KzkzZRucD49j4fRHT3Ndan
The guide extracts its code listings from src/evaluate.ts by symbol name,
and the two-sorted rewrite collapsed the five per-operation arithmetic
functions into one evaluateArithmetic, so the page failed to build.

Replace the five arithmetic listings with one, introduce the integer/bytes
sorts up front with listings of the exported Value type and namespace,
and add listings of the evaluateInteger and evaluateBytesOperands helpers
where coercion and width enforcement come up. Surrounding prose now
describes integer-sort arithmetic, literal sorting by hex digit count,
and the bytes requirement on $keccak256 / $concat operands.

Claude-Session: https://claude.ai/code/session_01RJFyifZxcSXZchLFNTNPuT
The expression spec page still said odd-digit hex literals are fine and
that values are left-padded to the width the context needs, which
contradicts the two-sorted model: even-digit hex is bytes of the width
written, odd-digit hex is an integer, and there is no implicit widening.
Rewrite the literals prose, add an integers-and-bytes section ahead of
the per-form sections, and have each form's prose name its result sort.

The docs guide gains a compact integers-and-bytes section and its
keccak256 examples now word-size keys and slots before hashing.

Claude-Session: https://claude.ai/code/session_014otYPQPP9pvQabmY58Fyom
@gnidan gnidan changed the title format: specify two-sorted (integer/bytes) expression evaluation format, pointers, bugc: specify and implement two-sorted (integer/bytes) expression evaluation Sep 3, 2026
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.

1 participant