Uh oh!
There was an error while loading. Please reload this page.
Declare every tooling builder view so an implementation can read state - #124
Conversation
`IParserToolingV1` and `ISubParserToolingV1` declared their three builders `pure`, which is a ceiling no override can loosen, so no parser or sub parser could build its pointer table from storage or from an immutable. Their two siblings already declared theirs `view`. All five builders are now `view`, the three `LibCodeGen` wrappers that were `pure` only because their interface was are `view`, and `ToolingMock` inherits all four interfaces so the compiler checks every builder's signature and mutability rather than a cast at the call site assuming them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Warning Review limit reached
Next review available in:33 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
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. Comment |
Uh oh!
There was an error while loading. Please reload this page.
Closes#92
IParserToolingV1andISubParserToolingV1declared their three builderspure. Solidity forbids an override from loosening mutability, so that was ahard ceiling: no parser or sub parser could build its pointer table from storage
or from an immutable — the normal Rain pattern where a sub parser embeds its own
or an extern's address into the bytecode it emits. Their two siblings,
IIntegrityToolingV1andIOpcodeToolingV1, already declared theirsview.All five builders across the four interfaces are now
view. The threeLibCodeGenwrappers that werepureonly because their interface was are nowview.ToolingMock— which had to opt out of the interfaces entirely anddocument the ceiling in a
@devparagraph — now inherits all four, so thecompiler checks every builder's name, arguments, return type and mutability
against the interface that declares it.
The test is the mock's inheritance
There is no runtime behaviour to assert here: the wrappers emit byte-identical
strings before and after. The defect is a compile-time one, so the test is a
compile-time one —
ToolingMockreads all five answers from storage and declaresis IOpcodeToolingV1, IParserToolingV1, ISubParserToolingV1, IIntegrityToolingV1.Under the old declarations that does not build. It is also the compile-time proof
of implementability that #74 asks for.
Failing, before the fix
ToolingMockinheriting the four interfaces, interfaces untouched,nix develop -c forge test:Three errors, on exactly the three declarations the issue names.
Passing, after the fix
Same 134 tests as on
mainat935c725; none were added, changed or removed.Mutation matrix
Every mutant flips exactly one
viewthis change touched back topureand runsthe whole suite. M0 is the unmutated baseline, run first, so a later
KILLEDis areal difference rather than a harness that never ran. Each mutant asserts the
target line's exact content before and after editing it, so a silently-missed
edit aborts instead of reading as
SURVIVED.cache/fuzz/failuresis deletedbefore every run so no counterexample is replayed from a previous mutant. The
tree was clean (
git status --shortempty) after the run.IParserToolingV1.buildOperandHandlerFunctionPointersview→pureError (6959)IParserToolingV1.buildLiteralParserFunctionPointersview→pureError (6959)ISubParserToolingV1.buildSubParserWordParsersview→pureError (6959)IOpcodeToolingV1.buildOpcodeFunctionPointersview→pureError (6959)IIntegrityToolingV1.buildIntegrityFunctionPointersview→pureError (6959)LibCodeGen.literalParserFunctionPointersConstantStringview→pureError (2527)LibCodeGen.operandHandlerFunctionPointersConstantStringview→pureError (2527)LibCodeGen.subParserWordParsersConstantStringview→pureError (2527)8/8 killed. M4 and M5 are the two interfaces this change did not flip: they show
the mock's inheritance guards all four, not only the two that moved.
Error (6959)isOverriding function changes state mutability from "pure" to "view".Error (2527)isFunction declared as pure, but this expression (potentially) reads from the environment or state and thus requires "view".interfaceIddoes not moveThe issue's note on this is garbled, so it was measured rather than reasoned
about. A throwaway probe logged
type(I…).interfaceIdand every builder selectoron the unmutated tree before and after the change:
IOpcodeToolingV10x514b5d4f0x514b5d4fIParserToolingV10x1a2c8edd0x1a2c8eddISubParserToolingV10x336284d40x336284d4IIntegrityToolingV10xb92d75530xb92d7553Every builder selector is also unchanged:
buildOperandHandlerFunctionPointers0xd6d8c9a8,buildLiteralParserFunctionPointers0xccf44775,buildSubParserWordParsers0x336284d4,buildOpcodeFunctionPointers0x514b5d4f,buildIntegrityFunctionPointers0xb92d7553.An interface id is the XOR of its function selectors, and a selector is the first
four bytes of
keccak256over the canonical signature —name(argTypes). Statemutability is not in that string, so it cannot reach the selector, and an id built
from unchanged selectors is unchanged.
0xd6d8c9a8 ^ 0xccf44775 == 0x1a2c8eddreproduces
IParserToolingV1's id by hand from the two measured selectors.What that means for already-deployed contracts: nothing. Every deployed
RainlangParser,RaindexV6SubParserand extern advertising0x1a2c8edd/0x336284d4keeps answering the same ids, and a consumer probing an id computedfrom this source keeps getting
true. This is not a new interface, so noV2is minted and #74's proposed id pins are still the pre-change literals.
The probe was not committed. #74 owns the permanent id pin, and duplicating it
here would collide with that PR.
Where the issue's proposed fix was wrong
pureimplementer'soverride". It is not — measured, see below.puredeclarations. This PR also adds themutability rationale to the two that were already
view, because the findingis that all four split their mutability "with no stated rationale" — leaving
two of five declarations unexplained just relocates the finding.
Downstream: nothing breaks, measured rather than argued
Solidity allows an override to tighten mutability —
viewmay be overridden bypure— so apureimplementation of aviewdeclaration compiles. Probed inthis repo on this branch with a throwaway contract inheriting all four interfaces
and declaring all five builders
external pure override:Not committed — it is #74's
ConformingToolingMockin all but name.rain.interpreterat HEAD (main, shallow clone) was compiled twice, before andafter patching its vendored
rain-sol-codegen-0.1.0copy exactly the way this PRpatches the source — three interface declarations
pure→viewplus the threeLibCodeGenwrappers:nix develop -c forge builddependencies/rain-sol-codegen-0.1.0Compiling 512 files with Solc 0.8.25/Compiler run successful with warningsviewCompiling 512 files with Solc 0.8.25/Compiler run successful with warnings512 files is
srcplustestplusscript, so this coversRainlangParserandRainlangReferenceExtern's fiveexternal pure overridebuilders, the sixpuretest sub-parsers, andscript/Build.sol's calls into the three widenedwrappers. Nothing had to change on the consumer side.
rain.flareandraindexwere checked by reading their call sites rather thanbuilt. Both hold the same shape and are unaffected for the same reason:
external pure—FlareFtsoSubParser.sol:63,78,86and
RaindexV6SubParser.sol:102,107,193— which is a legal tightening.purecaller reaching a builderthrough the now-
viewinterface type.raindexhas twoexternal puretestfunctions calling builders —
RaindexV6SubParser.pointers.t.sol:47,54— butthey call through the concrete
RaindexV6SubParsertype, whose own declarationsstay
pure, so the call site's mutability is unchanged.script/Build.solcall sites into the widened wrappers sit in non-viewfunctions (
raindex:49 buildRaindexSubParserPointers() internal,rain.flare:15 buildFlareFtsoWordsPointers() internal), which deploy contracts.No consumer anywhere calls a builder through the interface type from a
purecontext.
LibCodeGen's three wrappers were the only such callers, and they arewidened here.
Also changed, beyond the diff the issue drafted
The
@devparagraph onToolingMockrecorded the workaround rather than thedefect. It is replaced by a paragraph describing what the mock now is.
QA
test/concrete/ToolingMock.soldeclaringis IOpcodeToolingV1, IParserToolingV1, ISubParserToolingV1, IIntegrityToolingV1while reading all five answers from storage — a compile-time test, since the
defect is a compile-time one. Verified failing on base: with that inheritance
applied and the interfaces untouched,
nix develop -c forge testfails tocompile with three
Error (6959)onToolingMock.sol:48,53,58, quoted in fullabove. The whole 134-test suite is the thing that goes from not-building to
passing.
src/interface/IParserToolingV1.sol:21view→pure→ whole suite fails tocompile,
Error (6959)atToolingMock.sol(M1);:34likewise (M2);src/interface/ISubParserToolingV1.sol:20likewise (M3);src/interface/IOpcodeToolingV1.sol:21likewise (M4);src/interface/IIntegrityToolingV1.sol:22likewise (M5);src/lib/LibCodeGen.sol:145,:171,:195view→pure→Error (2527)inLibCodeGen.solitself (M6, M7, M8). Baseline M0 ran first and passed 134/134,so the kills are differences rather than a harness that never ran.
tighten mutability, and a
purefunction may not read state. The compiler isthe independent judge of both, and it reports them as
Error (6959)andError (2527)respectively; both error codes were named in the issue fromprobes taken before this change. The
interfaceIdclaim is judged against theABI selector definition (
keccak256("name(argTypes)"), mutability absent) andcross-checked by measuring the ids on both trees, plus
0xd6d8c9a8 ^ 0xccf44775 == 0x1a2c8eddby hand.pureinterface declarationsmade
view, (B) the threeLibCodeGenwrappers widened toview, (C) the@devworkaround paragraph deleted andToolingMockinheriting all fourinterfaces, (D) the ABI/
interfaceIdand downstream-implementer impactconfirmed against
rain.interpreter. Covered A, B, C, D. Beyond them, the twoalready-
viewdeclarations get the same mutability rationale, because thefinding is that all four split their mutability with none stated.
nix develop -c forge teston the RED tree (mock inherits, interfaces stillpure):Error: Compiler run failedwith threeError (6959), quoted above.nix develop -c forge teston the GREEN tree:Ran 16 test suites in 1.43s (12.48s CPU time): 134 tests passed, 0 failed, 0 skipped (134 total tests).full transcript kept at
/home/gildlab/artifacts/work/rain.sol.codegen-fix-92-mutations.txt.nix develop -c forge fmt --check: exit 0, no files listed.type(I…).interfaceIdprobed before and after on the unmutated tree: all fourids and all five selectors byte-identical.
nix develop -c forge buildin a freshrain.interpreterclone, before andafter patching its vendored
rain-sol-codegenthe way this PR patches source:Compiling 512 files with Solc 0.8.25/Compiler run successful with warningsboth times.external pure overridebuilders:Compiler run successful!, test passes.Review limit reached—@thedavidmeister's PR reviewlimit — so its green check is an absence of review, not a passed one. Its
unresolved-thread count over GraphQL is zero for the same reason.