Skip to content

Document the return value of every tooling builder - #134

Merged
thedavidmeister merged 3 commits into
mainfrom
2026-08-16-issue-94
Aug 17, 2026
Merged

Document the return value of every tooling builder#134
thedavidmeister merged 3 commits into
mainfrom
2026-08-16-issue-94

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Closes #94

The finding

The five builders on the four published tooling interfaces all return
bytes memory. The encoding of those bytes — two bytes per function pointer,
positionally indexed — is the whole contract of the call: the caller receives an
undifferentiated byte string and has to know how to cut it up. None of the five
tagged that fact as a return, so solc had nothing to put in devdoc.

Measured on origin/main (b422d97), forge build --force then reading each
artifact's metadata.output:

=== IIntegrityToolingV1
devdoc.methods: {}
userdoc.methods: ["buildIntegrityFunctionPointers()"]
=== IOpcodeToolingV1
devdoc.methods: {}
userdoc.methods: ["buildOpcodeFunctionPointers()"]
=== IParserToolingV1
devdoc.methods: {}
userdoc.methods: ["buildLiteralParserFunctionPointers()","buildOperandHandlerFunctionPointers()"]
=== ISubParserToolingV1
devdoc.methods: {}
userdoc.methods: ["buildSubParserWordParsers()"]

devdoc.methods is empty on all four, and the prose that was written landed in
userdoc as a notice, because an untagged /// line continues the preceding tag
rather than starting a return. Reading the source by eye, the functions look
documented. The artifact a consumer compiles against documents the return value
nowhere.

What changed

A @return on each of the five declarations. Ten lines, four files, and nothing
else — git diff origin/main --stat on this branch:

 src/interface/IIntegrityToolingV1.sol | 2 ++
 src/interface/IOpcodeToolingV1.sol    | 2 ++
 src/interface/IParserToolingV1.sol    | 4 ++++
 src/interface/ISubParserToolingV1.sol | 2 ++
 4 files changed, 10 insertions(+)
function @return
IIntegrityToolingV1.buildIntegrityFunctionPointers Every two bytes is a function pointer for an integrity check, positionally indexed by opcode.
IOpcodeToolingV1.buildOpcodeFunctionPointers Every two bytes is a function pointer for an opcode implementation, positionally indexed by opcode.
IParserToolingV1.buildOperandHandlerFunctionPointers Every two bytes is a function pointer for an operand handler, positionally indexed to match the parse meta.
IParserToolingV1.buildLiteralParserFunctionPointers Every two bytes is a function pointer for a literal parser, dispatched on the first byte(s) of the literal.
ISubParserToolingV1.buildSubParserWordParsers Every two bytes is a function pointer for a sub parser word parser, positionally indexed to match the parse meta.

The same build on this branch, same command, same key:

=== IIntegrityToolingV1
{"buildIntegrityFunctionPointers()":{"returns":{"_0":"Every two bytes is a function pointer for an integrity check, positionally indexed by opcode."}}}
=== IOpcodeToolingV1
{"buildOpcodeFunctionPointers()":{"returns":{"_0":"Every two bytes is a function pointer for an opcode implementation, positionally indexed by opcode."}}}
=== IParserToolingV1
{"buildLiteralParserFunctionPointers()":{"returns":{"_0":"Every two bytes is a function pointer for a literal parser, dispatched on the first byte(s) of the literal."}},"buildOperandHandlerFunctionPointers()":{"returns":{"_0":"Every two bytes is a function pointer for an operand handler, positionally indexed to match the parse meta."}}}
=== ISubParserToolingV1
{"buildSubParserWordParsers()":{"returns":{"_0":"Every two bytes is a function pointer for a sub parser word parser, positionally indexed to match the parse meta."}}}

No test guards this, and it can regress silently

This PR ships no test. Nothing in this repo's suite reads out/, so nothing
here observes the compiled artifact at all. Three regressions therefore pass
silently:

  • a @return deleted, which puts the interface straight back to the state this
    PR is fixing;
  • a @return reworded to state an encoding that is not the one the consumers
    implement;
  • a @return moved above the untagged /// paragraph that currently
    precedes it. That paragraph then stops beginning a docblock of its own and is
    appended to the return documentation instead, so returns._0 becomes the
    encoding sentence plus three sentences about state mutability. This is not
    hypothetical: it is the exact conflict resolution this branch had to get right
    when Declare every tooling builder view so an implementation can read state #124 was merged in, and a future edit to those docblocks faces the same
    choice with nothing checking it.

In every case the full suite stays green and the artifact is wrong. solc emits
no diagnostic for an absent return tag — building origin/main's interfaces,
which have none, produces only Warning (2018) mutability notices and nothing
about NatSpec — so forge build will not flag it either. After this PR nothing
in this repository will catch any of the three.

An earlier revision of this PR carried test/lib/LibDevdoc.sol and four
test/src/interface/<Interface>.devdoc.t.sol suites, 192 lines, that read the
compiled artifact and asserted the return text against five file-level string
constants. They are removed here, along with the
{ access = "read", path = "out" } fs_permissions grant that existed only to
let them read out/. foundry.toml is now byte-identical to origin/main.

They were hand-rolled static analysis written in Solidity, and they pinned the
prose byte-for-byte, so every future wording change would have had to be made
twice, in two files, or the suite goes red for a documentation edit that is
correct.

The half of that which generalises — every published interface function with a
return value carries a @return that reaches devdoc in its artifact
— is a
repo-wide lint rule with no home in this repo. It is rehomed to
rainlanguage/rainix#317 ("Six repos' worth of static analysis is being
hand-rolled in Solidity because rainix-static has no home for repo-wide lint
checks"), which is where the presence check belongs and where it can run against
every repo rather than this one. The prose pinning is not rehomed anywhere:
asserting that documentation says a particular sentence, by writing that sentence
down a second time, is a duplicate rather than a check.

Where the issue's proposed fix needed correcting

  • The issue supplied wording for buildOperandHandlerFunctionPointers and
    buildLiteralParserFunctionPointers only, and said the other three take
    "equivalents". LibCodeGen states the two byte width for exactly those two
    (src/lib/LibCodeGen.sol:151, :177-179) and says nothing about the width of
    the other three, so their wording is derived from the consumers instead:
    LibEval.sol:110 dispatches opcodes at mul(mod(opcode, fsCount), 2),
    LibIntegrityCheck.sol:166 at mul(opcodeIndex, 2), and
    BaseRainlangSubParser.sol:206 reads a word parser at mul(add(index, 1), 2),
    all in rainlanguage/rain.interpreter at main. Two bytes, positionally
    indexed, holds for all five.
  • The issue's literal parser wording carried "dispatched on the first byte(s) of
    the literal, rather than a full word lookup". The second clause is about how
    the parser is implemented, not about how the returned bytes are encoded, so it
    is dropped; LibCodeGen still emits it into the generated constant's comment,
    which is where it describes something.

Interface ids do not move

#119 pins these four ids, so this was checked rather than assumed. Each
interface's abi was extracted from its artifact on origin/main and again on
this branch, sorted and diffed:

IIntegrityToolingV1: ABI IDENTICAL to main
IOpcodeToolingV1: ABI IDENTICAL to main
IParserToolingV1: ABI IDENTICAL to main
ISubParserToolingV1: ABI IDENTICAL to main

An interface id is the exclusive or of the ABI selectors, and a selector is a
function name and its parameter types, so an identical ABI is an identical id.
NatSpec cannot reach any of it.

Merged origin/main, and how the conflict was resolved

#124 (issue #92) landed on all four of these files while this branch was open, so
origin/main is merged in at 726a86e (merge, not rebase). Every conflict was
in the same five docblocks: #124 added an untagged mutability paragraph and moved
three declarations from pure to view.

Both sides are kept. The ordering is load bearing rather than cosmetic — an
untagged /// line continues whatever tag precedes it, so #124's paragraph sits
before the @return tag. Written the other way round its three sentences
would be swallowed into the return documentation. The artifact output quoted
above is that ordering holding: each returns._0 is exactly the encoding
sentence and nothing else.

Suite

nix develop -c forge test on this branch:

Ran 18 test suites in 1.96s (17.64s CPU time): 142 tests passed, 0 failed, 0 skipped (142 total tests)

Before the strip the same command reported 22 test suites … 147 tests passed.
The drop is 5 tests across 4 suites, which is exactly the five deleted devdoc
tests and their four suites; no other test changed.

nix develop -c forge fmt --check exits 0.

CI on the head of this branch: rainix / test, rainix / static,
rainix / legal and build-pointers / copy-artifacts all pass.

Left to the sibling issues

The same five docblocks are the subject of two other open findings, and neither
is touched here: #93 (the description folded into @title, so the contract level
@notice is emitted empty) and #95 (the
.github/workflows/build-pointers.yaml reference that .soldeerignore strips
from the published package, still present on all five). #92 is in this branch
only because it landed as #124 and was merged in, unchanged.

CodeRabbit

Reviewed. Its first attempt on this PR was rate limited ("you've reached your PR
review limit"), which is an absence of review rather than a passed one, so a
review was requested again once the quota reset. That one ran: "No actionable
comments were generated in the recent review", and its commit status on the head
of this branch reads success / "Review completed". Unresolved threads were
checked over GraphQL rather than taken from the green check —
reviewThreads returns totalCount: 0, so there is nothing open.

QA

  • Discriminating tests: none. This change has no behaviour and ships no test,
    by decision. The regression it can suffer, and the fact that nothing here would
    catch it, are stated in full above. The presence check is rehomed to
    Six repos' worth of static analysis is being hand-rolled in Solidity because rainix-static has no home for repo-wide lint checks rainix#317.
  • Oracle: the compiled artifact, not the source, because the finding is precisely
    that source prose was not reaching the artifact. devdoc.methods is {} on
    all four interfaces on origin/main, and carries the five expected returns._0
    strings on this branch. Both measurements are quoted above, from the same
    forge build --force on the same tree.
  • Wording oracle: the consumers in rainlanguage/rain.interpreter
    (LibEval.sol:110, LibIntegrityCheck.sol:166,
    BaseRainlangSubParser.sol:206) and the comments LibCodeGen already emits —
    not the interfaces being documented.
  • Full suite: 142 passed, 0 failed, 0 skipped across 18 suites.
    forge fmt --check exits 0. Run on the tip, which has origin/main merged in.
  • Category check: No @return on any of the five tooling interface functions, so the pointer encoding is documented nowhere the compiler carries #94 asks for a @return on each of the five declarations
    carrying the encoding. Covered: all five. Nothing outside those five docblocks
    is changed — foundry.toml is byte-identical to origin/main and no test file
    is added.

The five builders on the four published tooling interfaces each return
`bytes memory` whose encoding is two byte function pointers, and none of
them tagged that fact as a return, so solc emitted an empty `methods`
object into their devdoc and a consumer received an artifact that
documents the return value nowhere.

Each builder now carries a `@return`, and each is asserted against the
devdoc of its own compiled artifact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Aug 16, 2026
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b90b63dd-9f84-4ab9-9c3b-071282de4de1

📥 Commits

Reviewing files that changed from the base of the PR and between b422d97 and 726a86e.

📒 Files selected for processing (10)
  • foundry.toml
  • src/interface/IIntegrityToolingV1.sol
  • src/interface/IOpcodeToolingV1.sol
  • src/interface/IParserToolingV1.sol
  • src/interface/ISubParserToolingV1.sol
  • test/lib/LibDevdoc.sol
  • test/src/interface/IIntegrityToolingV1.devdoc.t.sol
  • test/src/interface/IOpcodeToolingV1.devdoc.t.sol
  • test/src/interface/IParserToolingV1.devdoc.t.sol
  • test/src/interface/ISubParserToolingV1.devdoc.t.sol

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


Walkthrough

The interfaces now document encoded function-pointer return values. A test library reads compiled devdoc artifacts, and new tests verify the documented return text for all five tooling functions. Foundry can read the out directory required by these assertions.

Changes

Devdoc documentation and verification

Layer / File(s) Summary
Interface return documentation
src/interface/IIntegrityToolingV1.sol, src/interface/IOpcodeToolingV1.sol, src/interface/IParserToolingV1.sol, src/interface/ISubParserToolingV1.sol
NatSpec @return descriptions define the two-byte function-pointer encoding and its indexing or dispatch rules.
Compiled devdoc verification
foundry.toml, test/lib/LibDevdoc.sol, test/src/interface/*.devdoc.t.sol
Foundry grants read access to out. LibDevdoc reads compiled return documentation, and tests compare it with expected text for each tooling function.

Estimated code review effort: 2 (Simple) | ~15 minutes

Merge Risk: ⚪ Minimal · up to 726a8

This PR documents the tooling builders’ byte-encoded return values and verifies the generated artifacts without changing runtime behavior or interface IDs. No actionable merge-blocking risk remains after normal checks.

Possibly related issues

Possibly related PRs

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding return-value documentation to all tooling builder functions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-08-16-issue-94

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

# Conflicts:
#	src/interface/IIntegrityToolingV1.sol
#	src/interface/IOpcodeToolingV1.sol
#	src/interface/IParserToolingV1.sol
#	src/interface/ISubParserToolingV1.sol
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

The fixer brief that drove this change required every fix to ship a test.
That was wrong for a change with no behaviour, and the tests it produced
here were hand-rolled static analysis in Solidity: `test/lib/LibDevdoc.sol`
and four `.devdoc.t.sol` suites pinned five file-level constants
byte-for-byte against the compiled artifacts.

Only the five `@return` tags remain. `foundry.toml` returns to `main`'s
three-entry `fs_permissions` block exactly.
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thedavidmeister
thedavidmeister merged commit 959d527 into main Aug 17, 2026
5 checks passed
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.

No @return on any of the five tooling interface functions, so the pointer encoding is documented nowhere the compiler carries

1 participant