Skip to content

format, pointers: define segment offset carry semantics and resolve multi-slot regions - #284

Merged
gnidan merged 6 commits into
mainfrom
architect-segment-carry
Sep 17, 2026
Merged

gnidan merged 6 commits into
mainfrom
architect-segment-carry

Conversation

@gnidan

@gnidan gnidan commented Aug 6, 2026

Copy link
Copy Markdown
Member

Two fixes to the segment addressing scheme, with the reference implementation brought along so the schema does not describe what the implementation cannot resolve.

Segment offset carry. The offset field in pointer/scheme/segment required its value n to satisfy 0 ≤ n < $wordsize ("must begin inside the slot"). That contradicts the schema's own multi-slot note, which already says byte { "offset": "$wordsize" } of a slot is byte 0 of the next, and the packed-struct pointer example, which places a sentinel region at offset: $wordsize. The bound was the bug. offset is now an unbounded non-negative value with full carry: for a slot value p and an offset value n, the segment begins at byte n mod $wordsize of slot p + floor(n / $wordsize). Emitters may chain byte sums across slot boundaries; resolvers recover slot and byte by division and remainder against $wordsize. The multi-slot note and the packed-struct example are unchanged.

Length default under carry. The old default $wordsize − .offset clamps to zero once offset carries. The default is now $wordsize − (.offset mod $wordsize): the segment ends at the end of the slot in which it begins.

Region self-reference. The packed-array example computed a region's own offset from .length: "struct-pointer", a reference to the very region being declared. The reference-resolution rules only resolve previously declared names, so on the first list iteration there is no earlier struct-pointer to resolve to. It now uses the built-in self-reference .length: $this, and the $this reference gains a guard: a property lookup via $this must not be circular. The pointers implementation already detects this and throws rather than looping; a test now pins that.

Byte-chaining example. A companion to the string storage example expresses the long-string body as a single region whose length runs across slots, { name: "string", slot: "start-slot", offset: 0, length: "string-length" }, instead of the per-slot list plus last-slot-trim machinery. A minimal carry example is also added to the segment scheme's own examples. The per-slot list form stays primary: it exercises list, conditional, and define, and yields a distinct region per slot, which a consumer may want for display. The companion models only solc's long form.

Reference implementation. @ethdebug/pointers now resolves all of the above. Reads against word-addressed locations (stack, storage, transient) go through a shared readSegment that applies the carry, reads as many consecutive words as length spans, and concatenates them. The Machine.State.Words interface is unchanged, so @ethdebug/evm needs no changes. Unit tests cover carry at and past the word boundary, spans across two and three slots, the default length under a carried offset, and stack/transient carry. A new integration case observes the companion example against the same StringStorage contract as the existing test and decodes the multi-slot string end to end. The ganache test adapter now reads slots absent from the struct log as zero instead of garbage; that corrects the struct-storage case's initial salt expectation from 0x to 0x00000000.

Docs. The regions guide now states that offset and length are relative to the slot, that an offset past the word size carries into later slots, that length may run across slots, and what an omitted length means.

@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 01:44 UTC

@gnidan gnidan changed the title format: define segment offset carry semantics and fix region self-reference format, pointers: define segment offset carry semantics and resolve multi-slot regions Sep 3, 2026
…erence

Two pointer-side fixes from the design rulings.

Segment offset (ruling 5): the offset field prose required a value n with
0 <= n < $wordsize, contradicting the schema's own multi-slot note and
the packed-struct pointer example, both of which use offsets at or beyond
a word boundary. The offset is now defined as an unbounded non-negative
value with full carry: for slot p and offset n, the segment begins at
byte (n mod $wordsize) of slot (p + floor(n / $wordsize)). The multi-slot
note and the example are unchanged; only the erroneous bound is removed.

Region self-reference (ruling 6): the packed-array example computed a
region's own offset from '.length: "struct-pointer"' — a reference to the
region being declared, which the name-resolution rules (previously-declared
names only) cannot satisfy on the first iteration. It now uses the built-in
'.length: $this'. A guard is added to the $this reference: a property
lookup via $this must not be circular.
Adds a companion to the string-storage pointer example that expresses the
long-string body as one region whose length runs across slots, instead of
a per-slot list — the byte chaining the carry semantics enable. The
existing per-slot list form is kept as the primary example: it stays
resolvable by the reference implementation and yields a distinct region
per slot, which a consumer may want. Also adds a minimal carry example to
the segment scheme's own examples (offset at a word boundary addressing
the next slot).

Note: a single storage region spanning slots is not yet resolved by the
pointers reference implementation (flagged to debugger as a tracked
follow-up); the primary per-slot form remains the resolvable one.
The companion example shared the string example's
'string-storage-contract-variable-slot' identifier, which the pointers
integration tests use with findExamplePointer (first substring match).
First-match already selects the resolvable per-slot form, but renaming the
companion's variable to 'string-storage-slot' removes the shared lookup
substring so the companion can never be selected even if examples are
reordered.
Segment regions (stack, storage, transient) now follow the addressing
scheme's carry semantics: an offset at or beyond $wordsize addresses a
later slot, a length may run across slots (concatenating sequentially
addressed slots), and an omitted length ends at the end of the slot in
which the segment begins. The Machine.State interface is unchanged;
read() assembles the bytes from per-slot reads.

Also:

- express the segment `length` default with $remainder so that it is
  consistent with offset carry (it previously clamped to 0 for offsets
  at or beyond $wordsize)
- add an integration test selecting the single-region `string storage`
  companion example against the same StringStorage contract
- treat storage slots absent from ganache struct logs as zero (they
  previously decoded as garbage); this corrects the struct storage
  test's expected initial `salt` to 0x00000000

Claude-Session: https://claude.ai/code/session_01RJFyifZxcSXZchLFNTNPuT
…guide

The slot-based locations paragraph in the pointers regions guide still
described offset/length as sub-slot positioning only. Restate it against
the segment scheme's semantics: offsets carry past $wordsize into later
slots, lengths may span consecutive slots, and an omitted length ends at
the end of the starting slot. Points at the multi-slot string storage
example as the canonical use.

Claude-Session: https://claude.ai/code/session_014otYPQPP9pvQabmY58Fyom
@gnidan
gnidan force-pushed the architect-segment-carry branch from 9db393d to 875c6a3 Compare September 17, 2026 01:34
@gnidan
gnidan merged commit a291931 into main Sep 17, 2026
7 checks passed
@gnidan
gnidan deleted the architect-segment-carry branch September 17, 2026 01:40
gnidan added a commit that referenced this pull request Sep 17, 2026
The empty-context history was attributed to the wrong PR: #145 dropped
`minProperties: 1` and the `required` entry, but the context schema's
`anyOf` still rejected an explicit `{}` until #150 replaced the union
with `if`/`then` clauses. #131 likewise never declared the compilation
`id` required in prose; it only dropped the "optional" wording.

Every `Schemas:` list now names each schema file the change touched,
verified against the diffs. The #284 entry is split, since the segment
offset carry and the `$this` circularity rule are separate changes with
separate consequences. The encoding label rule is marked as a normative
tightening no validator catches, and its canonical label is described as
preferred rather than required, matching the schema. The id uniqueness
requirement is presented as the new part of #172, and `frame`'s values
as examples rather than an enumeration. The preamble explains why this
file is keyed by the `@ethdebug/format` version, and the history ends at
`0.1.0-0`.
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