Skip to content

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

Merged
gnidan merged 4 commits into
mainfrom
architect-expression-semantics
Sep 17, 2026
Merged

gnidan merged 4 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.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-17 02:05 UTC

@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
The width of an expression's 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 disagree on the bytes produced. Expressions now evaluate to one
of two sorts: an unbounded non-negative integer with no width, or
bytes with a definite width. JSON numbers, $wordsize, variables,
lookups, arithmetic, and odd-digit hex are integers; even-digit hex,
$read, the resize forms, $keccak256, and $concat are bytes. Where an
integer is expected a bytes value is read big-endian; where bytes are
expected ($concat and $keccak256 operands) a bare integer is an
error, and the resize forms are the only bridge. The examples that
hashed bare integers are corrected to word-size their operands.
The evaluator used one width-carrying representation for everything
and gave a JSON-number literal its minimal width, so literal 0 was
zero bytes and a $keccak256 over a bare slot hashed the wrong
preimage. evaluate() now returns a tagged Value, integer or bytes,
with the sort rules the schema states; arithmetic coerces operands to
integers; $concat and $keccak256 throw on a bare-integer operand with
a message naming it and suggesting $wordsized or $sizedN. Variables
carry the sort of their defining expression. The change is internal:
evaluate is not exported from the package index and Cursor.Region is
unchanged. The previously skipped keccak test is reinstated, every
sort rule and coercion direction is tested, and a new test evaluates
every $keccak256 and $concat occurrence in the pointer schema
examples so an example without widths fails the suite. The
implementation guide that lists the evaluator's functions by name is
updated to the new shape, since its code listings would otherwise
break the site build.
The slot-computation helpers emitted $keccak256 with a bare slot
operand, which the two-sorted semantics reject. They now word-size
it, passing a nested $keccak256 through since it is already 32-byte
bytes. These helpers have no production callers yet, so emitted debug
info was already valid; the helpers and their pinned tests are
corrected so that any future use is too.
The spec page said values are "assumed to be left-padded to the bytes
width appropriate for the context", which the schema now contradicts.
It states the two sorts, the even/odd hex rule, and the width-bearing
requirement, and each form's prose names its result sort. The
expressions guide gains an "Integers and bytes" section and
word-sizes the operands in its hashing examples.
@gnidan
gnidan force-pushed the architect-expression-semantics branch from 9a9c742 to a915671 Compare September 17, 2026 01:43
@gnidan
gnidan merged commit 73cf65a into main Sep 17, 2026
7 checks passed
@gnidan
gnidan deleted the architect-expression-semantics branch September 17, 2026 02:01
gnidan added a commit that referenced this pull request Sep 17, 2026
CHANGELOG.md at the repository root records changes to the schemas
under schemas/. The schemas ship inside @ethdebug/format, so the file
is keyed by that package's version.

Each entry gives a summary and the pull request, then three sub-items:
the schemas the change touches, what it means for producers, and what
it means for consumers. The producer and consumer lines each start
with one of three fixed prefixes:

- "no change needed." when nothing valid becomes invalid or changes
  meaning for that party
- "optional:" when the change adds a capability that obliges nobody
- "required:" when earlier valid output stops validating, the
  specification adds a must, or the meaning of valid data changes;
  the line names the keyword or prose that imposes the obligation

The file has an entry for every pull request that changed schemas/
between the 0.1.0-0 publication and the 0.1.0-1 tag, each checked
against the schema diff of its pull request, and an Unreleased section
for the changes merged since (#284, #286).
gnidan added a commit that referenced this pull request Sep 17, 2026
CHANGELOG.md at the repository root records changes to the schemas
under schemas/. The schemas ship inside @ethdebug/format, so the file
is keyed by that package's version.

Each entry gives a summary and the pull request, then three sub-items:
the schemas the change touches, what it means for producers, and what
it means for consumers. The producer and consumer lines each start
with one of three fixed prefixes:

- "no change needed." when nothing valid becomes invalid or changes
  meaning for that party
- "optional:" when the change adds a capability that obliges nobody
- "required:" when earlier valid output stops validating, the
  specification adds a must, or the meaning of valid data changes;
  the line names the keyword or prose that imposes the obligation

An impact line states the net effect for a party that moves from the
previous published version to the version of its section. A change
inside a schema that is new in that version obliges nobody, and an
obligation that a later change in the same version reverses does not
appear. Sections are only Added and Changed; they do not signal
obligations, the prefixes do.

The file has an entry for every pull request that changed schemas/
between the 0.1.0-0 publication and the 0.1.0-1 tag, each checked
against the schema trees of those two versions, and an Unreleased
section for the changes merged since (#284, #286).
gnidan added a commit that referenced this pull request Sep 17, 2026
* docs: add a changelog for the specification

CHANGELOG.md at the repository root records changes to the schemas
under schemas/. The schemas ship inside @ethdebug/format, so the file
is keyed by that package's version.

Each entry gives a summary and the pull request, then three sub-items:
the schemas the change touches, what it means for producers, and what
it means for consumers. The producer and consumer lines each start
with one of three fixed prefixes:

- "no change needed." when nothing valid becomes invalid or changes
  meaning for that party
- "optional:" when the change adds a capability that obliges nobody
- "required:" when earlier valid output stops validating, the
  specification adds a must, or the meaning of valid data changes;
  the line names the keyword or prose that imposes the obligation

An impact line states the net effect for a party that moves from the
previous published version to the version of its section. A change
inside a schema that is new in that version obliges nobody, and an
obligation that a later change in the same version reverses does not
appear. Sections are only Added and Changed; they do not signal
obligations, the prefixes do.

The file has an entry for every pull request that changed schemas/
between the 0.1.0-0 publication and the 0.1.0-1 tag, each checked
against the schema trees of those two versions, and an Unreleased
section for the changes merged since (#284, #286).

* web: show the specification changelog on the docs site

The page at /spec/changelog imports the root CHANGELOG.md, so the
site and the repository always show the same text. The file must stay
valid MDX: no HTML comments, and no bare "<" or "{" outside code.

* docs: add a changelog to each published package

Each of the seven public packages gets a CHANGELOG.md keyed by its own
version, because the packages are versioned independently. Entries
describe changes that a user of the package can see.

@ethdebug/format and @ethdebug/pointers were on npm at 0.1.0-0, so
their files carry the history from that publication to 0.1.0-1,
reconstructed from a comparison of the published tarballs. The other
five packages were first published at 0.1.0-1.

* packages: ship CHANGELOG.md in the published tarballs

npm always includes package.json, README* and LICENSE* in a tarball,
but not CHANGELOG.md. Each public package now lists the file in
"files", and the tarball allow-list in bin/packlist.ts accepts
CHANGELOG* so that bin/check-tarballs.ts does not reject it.

* ci: require a changelog entry for schema and package changes

bin/check-changelog.ts compares a pull request with its base branch.
It fails when the diff changes schemas/ without the root CHANGELOG.md,
or changes the src/, bin/ or package.json of a public package without
that package's CHANGELOG.md. Colocated test files do not count.

The label "changelog: skip" exempts a pull request. The job has its
own workflow that also runs on "labeled" and "unlabeled" events:
Actions cannot filter those events by label name, so in ci.yml each
label change would run the whole suite again.

* bin: check the format of the specification changelog

Each "Producers:" and "Consumers:" sub-item in the root CHANGELOG.md
must start with "no change needed.", "optional:", or "required:", and
each section heading must be "Added" or "Changed". The check reads the
file on every run, whether or not the diff changes schemas/, and
reports each problem with its line number. The bullets in the intro
that describe the sub-items are not sub-items, and the check ignores
them.

* docs: add the changelog cut to the release steps

Before the version bump, the operator renames "## Unreleased" to the
new version and date in the root file and in the file of each package
that Lerna will bump, and leaves an empty Unreleased heading above it.
"yarn lerna changed" shows which packages those are. A package that
is bumped only because a dependency changed gets one "Changed" entry
that names the dependency, so that each published version has a
section of its own.

In the root file the operator also reconciles the Unreleased entries
against the previous published version, because the impact lines
state the net effect between published versions.
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