format, pointers: define segment offset carry semantics and resolve multi-slot regions - #284
Merged
Merged
Conversation
Contributor
|
…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
force-pushed
the
architect-segment-carry
branch
from
September 17, 2026 01:34
9db393d to
875c6a3
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two fixes to the
segmentaddressing scheme, with the reference implementation brought along so the schema does not describe what the implementation cannot resolve.Segment offset carry. The
offsetfield inpointer/scheme/segmentrequired its value n to satisfy0 ≤ 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 byte0of the next, and the packed-struct pointer example, which places a sentinel region atoffset: $wordsize. The bound was the bug.offsetis now an unbounded non-negative value with full carry: for aslotvaluepand anoffsetvaluen, the segment begins at byten mod $wordsizeof slotp + 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 − .offsetclamps to zero onceoffsetcarries. 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 earlierstruct-pointerto resolve to. It now uses the built-in self-reference.length: $this, and the$thisreference gains a guard: a property lookup via$thismust 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 storageexample expresses the long-string body as a single region whoselengthruns across slots,{ name: "string", slot: "start-slot", offset: 0, length: "string-length" }, instead of the per-slotlistplus 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 exerciseslist, conditional, anddefine, 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/pointersnow resolves all of the above. Reads against word-addressed locations (stack, storage, transient) go through a sharedreadSegmentthat applies the carry, reads as many consecutive words aslengthspans, and concatenates them. TheMachine.State.Wordsinterface is unchanged, so@ethdebug/evmneeds 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 sameStringStoragecontract 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 initialsaltexpectation from0xto0x00000000.Docs. The regions guide now states that
offsetandlengthare relative to the slot, that an offset past the word size carries into later slots, thatlengthmay run across slots, and what an omittedlengthmeans.