Uh oh!
There was an error while loading. Please reload this page.
LibCodeGen: refuse a codeless instance, require an identifier name, and stop emitting a blank comment line - #54
Conversation
`bytecodeHashConstantString` reverts when the instance holds no code. A
codeless address hashes to `bytes32(0)` or `keccak256("")`, and
`address.codehash` returns those same two values, so a constant carrying
either is satisfied by every codeless address rather than by the
deployment it names.
`describedByMetaHashConstantString` requires its name to be a Solidity
identifier before interpolating it into `meta/<name>.rain.meta`, and
documents the `fs_permissions` grant it needs. `foundry.toml` takes that
grant, so the function is reachable under this repo's own configuration.
Both hand rolled `bytes32` declarations now go through
`bytes32ConstantString`, so they carry the same wrap arithmetic as every
other `bytes32` constant instead of an unconditional space.
An empty comment emits no comment line rather than an empty one, so the
generated file has no blank line for `forge fmt` to collapse.
`@param vm` describes what each function does with the Vm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>… the empty comment `requireContractName` gets its own suite: the accepted set is asserted to be exactly the Solidity identifier alphabet, and no accepted name can carry a path separator, a dot or a nul. `bytecodeHashConstantString` is asserted to refuse a non existent account, a funded but codeless account and the zero address, and to accept a single byte of code. Each of the four declaration builders pins the empty comment output as a literal, and pins that the declaration itself is unchanged by the comment being absent. Both `bytes32` declarations are measured against `LibCodeGenSlow`, which builds the one line form and measures it, so the wrap decision they now share is checked rather than assumed. The `describedByMeta` fixtures move from a `..` traversal out of `meta/` into `src/generated` to real files under `meta/`, which the library's name rule requires and the repo's `fs_permissions` now allows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fuzz property test almost never draws a name that is a valid identifier apart from one boundary character, so a range that runs one past its end survives it. Each of the three ranges is now pinned from both sides: first and last character accepted, and the character immediately outside each end refused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Warning Review limit reached
Next review available in:28 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 (10)
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 |
thedavidmeister
commented
Aug 16, 2026
Reviewed 429cee3: ready — Closes #44, #45, #46, #47, #48. 33 mutants, 33 killed, 0 survived, 0 no-run, 0 harness-error, baseline green at 110. 84 tests on main, 26 added, 0 removed — verified by diff rather than by the handoff's claim. The two decisions that reach beyond this repo were settled by looking outside it, not by reasoning. #44 refuses a codeless instance rather than documenting the precondition, and the org search found all 7 live call sites across 6 repos deploy on the line above, so nothing breaks. #45 keeps #46 routes both hand-rolled Three things the handoff did not predict, each caught rather than absorbed. The uncommitted test did not compile — its NatSpec contained a backtick- And the first re-run scored M29 to M33 as NO-RUN, which was the harness lying rather than a result: M28's That residue is a live defect beyond this PR and is disclosed in the QA block rather than buried: any failing run of this suite leaves those files behind for the next one, in CI as much as locally. CI green. CodeRabbit reports One follow-up this creates: |
Uh oh!
There was an error while loading. Please reload this page.
#54 landed `requireContractName` and `InvalidContractName` in LibCodeGen on main, so `LibContractName` is a second definition of the same rule and goes. `LibFs` already imports `LibCodeGen`, so the check arrives with an import that was already there. The coverage that `LibContractName`'s own suite held and `LibCodeGen`'s does not is folded into `test/lib/LibCodeGen.requireContractName.t.sol`: the exhaustive 256 byte sweeps in the leading and trailing positions, fuzzed agreement with an alphabet written out character by character rather than with a second copy of the library's own range arithmetic, fuzzed acceptance of constructed identifiers, and fuzzed rejection of a single bad byte anywhere in an otherwise valid name. The alphabets and the seed to identifier fold move to `LibCodeGenSlow` alongside the definition they now reference.
Closes#44
Closes#45
Closes#46
Closes#47
Closes#48
Five findings, all in
src/lib/LibCodeGen.sol. They are one PR because theytouch the same two functions: #46 changes how
bytecodeHashConstantStringassembles its declaration while #44 adds a guard to the same function, and
#45's two halves and #48's rewrite land in the same file.
The issues frame the problem and deliberately do not pick the fix. Each
decision and its reasoning is below.
#44 —
bytecodeHashConstantStringrefuses an address with no codeDecision: refuse, not document.
The constant exists so a consumer can assert
addr.codehash == BYTECODE_HASH.For an address with no code
extcodehashreturnsbytes32(0)when the accountdoes not exist and
keccak256("")when it does — andaddr.codehashreturnsthose same two values for every codeless address. So the emitted constant was
satisfied by any codeless address. The check read as verifying a deployment
and verified nothing, and the failure landed in a consumer repo asserting
against a constant that could not fail.
Documenting the precondition leaves that silent.
LibFs.buildFileForContractincludes this constant unconditionally, so there is no path where a caller opts
out of it, and generation is a build-time step where a revert costs nothing and
is loud.
The behaviour change costs no existing caller. Every live call site in the org
constructs the instance on the line above and passes it:
LibRainDeploy.deployZoltu(...)then the addressnew XWords()thenaddress(x)The guard reads
extcodesize, not "is the hash one of the two sentinelvalues", so it says what it means and does not depend on either sentinel.
bytes32(0)is not special-cased into a pass anywhere:bytes32ConstantStringstill emits zero as plainly as any other value, which its own test pins.
#45 — keep the function, sanitise the name, take the grant
Decision: keep and document, not remove. Sanitise with the Solidity
identifier rule.
Removal was on the table in the issue because nothing in this repo calls it.
Checked rather than assumed — GitHub code search over
rainlanguage:describedByMetaHashConstantStringhas seven live call sites in six repos(raindex, rainlang ×2, rain.flare, rain.pyth, rain.merkle,
rain.erc4626.words). Removing it breaks all of them. It stays.
The same check settles the permission half. All six of those repos already
grant
metainfs_permissions—{ access = "read-write", path = "meta" }in five,
{ access = "read", path = "./meta" }in raindex. Someta/is anestablished convention that only this library's own docs and config failed to
state. The NatSpec now states the grant a consumer needs (
read), andfoundry.tomlhere takesread-write—readis all the library needs, thewrite is so this repo's own suite can lay down and clear its fixtures.
For the sanitisation, the rule is the Solidity identifier: non-empty, ASCII
letters / digits /
_/$, not starting with a digit. That is what acontract name is, and it is also a character set that cannot express a
separator, a parent directory or an empty basename — so
..,/and""arerefused by the rule rather than by special cases for them. All seven live call
sites pass a plain identifier (
"RaindexV6SubParser","PythWords","FlareFtsoWords", …), so this breaks nobody.The check lives in
LibCodeGenrather than a shared file on purpose.LibFsimportsLibCodeGen, soLibCodeGencannot importLibFsback, and anew shared
LibContractName.solwould be a file two parallel branches bothcreate.
requireContractNameisinternaland callable, so if theLibFsside lands its own copy the two collapse into one shared file in a follow-up
rather than blocking either now.
One existing test file changed shape, and it should be reviewed as such.
test/lib/LibCodeGen.describedByMetaHashConstantString.t.solcould only reachthe function by traversing
../src/generated/…out ofmeta/, because therepo granted no read under
meta. The identifier rule forbids exactly that, sothe fixtures move to real files under
meta/. No assertion was weakened orremoved:
testDescribedByMetaHashConstantStringPathstill observes the paththrough the caught read failure and is unchanged, because foundry quotes the
full path in its missing-file error too — verified directly, not assumed:
meta/holds no committed file. The suite creates it and every fixture isremoved again, which leaves the directory empty and so invisible to git.
#46 — both
bytes32declarations go throughbytes32ConstantStringDecision: route them, rather than accept the two-character margin.
DESCRIBED_BY_META_HASHemitted a 118-character line against a 120 limit. Thenames are string literals in this file with nothing pinning them, and the wrap
arithmetic already existed one function away, so the fix is to have one
definition of the wrap threshold instead of three call sites of which two
skipped it.
The output is byte-identical today, which is what makes this safe: both
names fit (109 and 118 characters), so both take the space rather than the
wrap. Verified two ways —
testBuildFileForContractCommittedArtifactIsCurrentstays green, and
forge script script/Build.solregeneratessrc/generated/CodeGennable.solto no diff. Nothing was regenerated becausenothing moved.
#47 — an empty comment emits no comment line
Decision: emit no comment line, rather than reject
"".Rejecting turns a formatting helper into a reverting one for an input that
produces valid Solidity. Emitting no comment line gives the caller what they
asked for, keeps the function total, and removes the blank line that
forge fmtwould collapse. A declaration is now preceded by exactly one blankline whether or not it has a comment.
commentPrefixis shared by all four builders so there is one statement of therule rather than four.
One thing to look at:
LibCodeGenSlow, the naive differential reference the…MatchesMeasuredLinefuzz tests measure against, states the same rule on itsown side. It is the one place the two sides agree by construction rather than
by independent derivation, so its docstring says so, and the exact text of both
the empty and non-empty case is pinned separately by literal assertions in all
four suites — the reference is not the only thing holding it.
#48 —
@param vmsays what each function does with the VmTen of eleven get
The Vm instance used to format values as strings.describedByMetaHashConstantString— the one that touches disk — says so andcarries the
fs_permissionsblock from #45.MAX_LINE_LENGTH's comment saysforge fmt; there is nofoundry fmt.QA
Discriminating tests: 110 tests, up from 84, none removed, all asserting exact emitted text
or an exact revert rather than that a call did not revert.
testRequireContractNameAcceptedNamesAreIdentifiersderives the expectedverdict from the alphabet independently and fuzzes the whole string domain
against it, and
…AcceptedNamesCannotTraversestates the safety propertydirectly — no accepted name carries
/,\,.or a nul — rather thanlisting the sequences that would escape. The bytecodeHashConstantString emits a plausible BYTECODE_HASH for an address with no code, so the consumer's codehash check passes for any codeless address #44 refusal is pinned for a
non-existent account (
codehash == bytes32(0)), a funded but codelessaccount (
codehash == keccak256("")) andaddress(0)separately, with theprecondition on
codehashasserted in each so the test fails if the twovalues it is about ever stop being those values.
…AcceptsAnyNonEmptyCodepins that one byte of code is enough, so the guard is on there being no code
rather than on the code being short. Both
bytes32declarations are measuredagainst
LibCodeGenSlow, which builds the one-line form and measures it, sothe wrap decision they now share is checked rather than assumed.
Mutation: 33 mutants over
src/lib/LibCodeGen.sol, 33 killed, 0 survived,0 no-run, 0 harness errors, against a green 110-test baseline. Each of the
three identifier ranges is broken at both ends and the empty check disabled;
the empty-comment branch is forced both ways and its blank line dropped; the
codeless guard is disabled and inverted, and both the
extcodesizeand theextcodehashit sits on are pinned to constants; the meta directory isrenamed and the hash taken over the path instead of the contents; all four
wrap thresholds are moved off by one and the
bytes32wrap disabled outright;the wrap indentation, both explicit type wrappers, and the file prefix's
pragma and autogenerated warning are each broken.
One mutant survived the first pass — the uppercase range run one past
Z,which accepts
[— and that survivor is whattestRequireContractNameRangeBoundarieswas written for. The fuzzer almostnever draws a name that is a valid identifier apart from one boundary
character, so nothing else in the suite separated
[fromZ. The new testpins all three ranges from both sides and kills that mutant and four others.
The first pass also scored five mutants NO-RUN, which was the harness lying
rather than a fact about the code, and is worth a reviewer's attention on its
own. The
LibFstests write real files intosrc/generated/and thedescribedByMetatests write fixtures intometa/, and a test that failsaborts before its own cleanup — so the
pragma solidity ^0.8.26mutant leftnine generated sources behind that no installed solc could compile, and the
five mutants after it were scored on its residue rather than on themselves.
The suite command now clears both directories to their committed state before
every run and the whole matrix was re-run under it; the numbers above are from
that run. The same residue is left by any failing run of this suite, in CI as
much as locally.
Oracle: intent came from the consumers, not from this repo.
describedBy…'sdisposition was decided by GitHub code search over
rainlanguage— sevencall sites in six repos, every one passing a plain identifier, every one
already granting
meta— which is what rules out removal and what makes theidentifier rule non-breaking. The two codeless
codehashvalues were checkedagainst the EVM in-test rather than recalled. Foundry's missing-file error
text was measured before relying on it, because an existing test reads the
path out of it.
forge fmt --checkand a fullforge script script/Build.solregeneration are what establish that the bytecodeHashConstantString and describedByMetaHashConstantString hand-roll their bytes32 declaration and skip the wrap arithmetic — 2 characters of headroom #46 routing and the An empty comment makes every *ConstantString emit a blank line, so the generated file is not stable under forge fmt #47 change leave
the committed artifact untouched.
Category check: the ask is five issues in one file, and the category is every
function in that file that the change reaches, not the two the issues name.
All four
*ConstantStringbuilders take the empty-comment change and allfour have a literal empty-comment test plus a declaration-unchanged test; all
eleven
@param vmlines were rewritten, not the ten that were wrong; bothhand-rolled declarations were routed, not just the one with two characters of
headroom. Deliberately not done here: the identical unvalidated-name problem
in
LibFs.pathForContract(LibFs.pathForContract accepts any string as a contract name: empty writes a hidden dotfile, a slash targets a subdirectory, and .. escapes under a normal fs_permissions grant #38) and theREADMEhalf of the docs (README tells consumers to write script/BuildPointers.sol, which rainix CI rejects outright — plus three more stale claims #42),which are other branches — a second edit to those files is a conflict rather
than help.