Language design axes: full implementation (25 steps across 6 axes)#1731
Language design axes: full implementation (25 steps across 6 axes)#1731
Conversation
Planning document for the 5 axes of language design improvements that go beyond Solidity parity (#1726): - Axis 1: Type system enrichment (newtypes, ADTs, Result types) #1727 - Axis 2: Compile-time safety enforcement (CEI, access control, unsafe) #1728 - Axis 3: Effect system & auto-theorem generation #1729 - Axis 4: Storage safety (EIP-7201 namespaces) #1730 Includes suggested implementation order (Axis 3 first — lowest friction, highest proof payoff, builds theorem generation infra reused by Axis 2), effort estimates, file-level impact analysis, and integration mapping to #1724 parity waves. No code changes — planning only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ctions Phase 1 of the Language Design Axes plan (#1729, Axis 3). For every `view` function in a `verity_contract`, the macro now auto-generates a `@[simp]` theorem `<fn>_is_view` stating: FunctionSpec.isView (<fn>_model) = true This is provable by `rfl` because the macro already sets the flag. The `stmtWritesState` validation in `Validation.lean` guarantees the body contains no state-writing statements, so the flag accurately reflects the function's behavior. Changes: - Specs/Common.lean: add `viewPreservesState` predicate (full state unchanged frame condition, covering all storage + context + events + knownAddresses) with `@[simp]` reflexivity theorem - Macro/Bridge.lean: add `mkViewTheoremCommand` that generates the `_is_view` simp lemma for a view function - Macro/Elaborate.lean: call `mkViewTheoremCommand` in the per-function loop for view functions - MacroTranslateInvariantTest.lean: compile-time assertion that the `MutabilitySmoke.currentOwner_is_view` theorem exists Verified: `lake build` succeeds, `make check` passes, all existing contracts (ERC20, Vault, SimpleToken, Ledger, etc.) unaffected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b4a13f829a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Emits a `@[simp]` lemma stating the model's `isView` flag is `true`, making this | ||
| fact available to downstream proof automation. Only called when `fnDecl.isView`. -/ | ||
| def mkViewTheoremCommand (fnDecl : FunctionDecl) : CommandElabM Cmd := do | ||
| let viewName ← mkSuffixedIdent fnDecl.ident "_is_view" |
There was a problem hiding this comment.
Reserve generated
_is_view helper names
Generating $fn_is_view introduces a new declaration name, but the existing collision validator still only reserves _modelBody, _model, _bridge, and _semantic_preservation in validateGeneratedDefNamesPublic (Verity/Macro/Translate.lean). If a contract already declares a symbol like currentOwner_is_view (function/field/constant), elaboration now fails with a duplicate-name error instead of being rejected up front with the normal validation path. Please add _is_view to generated-name conflict checks (at least for view functions) so these contracts don’t regress.
Useful? React with 👍 / 👎.
Implements Phase 1, Step 1b of the language design axes plan (#1729). New `modifies(field1, field2)` annotation for verity_contract functions declares which storage fields a function writes to. The compiler: 1. Validates modifies field names exist in the storage section 2. Validates the function body only writes to declared fields (via new `stmtWrittenFields` / `stmtListWrittenFields` walkers) 3. Auto-generates `_modifies` simp theorem recording the declared set 4. Auto-generates `_frame` definition: conjunction of unchanged predicates for every field NOT in the modifies set plus sameContext 5. Auto-generates `_frame_rfl` simp lemma proving the frame holds reflexively Syntax: `function myFn (...) modifies(field1, field2) : RetTy := body` Files changed: - Macro/Syntax.lean: new `verityModifies` syntax category - Macro/Translate.lean: FunctionDecl.modifies field, parser, validation - CompilationModel/Types.lean: FunctionSpec.modifies field - CompilationModel/Validation.lean: stmtWrittenFields walker + check - Macro/Bridge.lean: mkModifiesTheoremCommand + mkFrameDefCommand - Macro/Elaborate.lean: emit modifies/frame in per-function loop - Contracts/Smoke.lean: ModifiesSmoke test contract - Tests: invariant test + round-trip fuzz coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 405f491f9d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| stmtListWrittenFields thenBranch ++ stmtListWrittenFields elseBranch | ||
| | Stmt.forEach _ _ body => | ||
| stmtListWrittenFields body | ||
| | _ => [] |
There was a problem hiding this comment.
Account for call statements in modifies validation
stmtWrittenFields falls through to [] for Stmt.internalCall, Stmt.internalCallAssign, Stmt.externalCallBind, and Stmt.ecm, so a function annotated modifies(counter) can still mutate other storage via a callee and pass validation. That makes the generated _frame theorem unsound (it can claim untouched fields are unchanged when they are not). Please either do interprocedural write-set checking or conservatively reject modifies(...) on statements with unknown write effects.
Useful? React with 👍 / 👎.
| let frameName ← mkSuffixedIdent fnDecl.ident "_frame" | ||
| let frameRflName ← mkSuffixedIdent fnDecl.ident "_frame_rfl" |
There was a problem hiding this comment.
Reserve new frame-helper names before generation
This commit now generates <fn>_modifies, <fn>_frame, and <fn>_frame_rfl, but the generated-name validator still only reserves the older helper suffixes. A user-defined symbol with one of these new names (for example a function named transfer_frame) will pass upfront validation and then fail later with a duplicate declaration when these helpers are emitted. Add the new suffixes to generated-name conflict checks so errors are caught deterministically during validation.
Useful? React with 👍 / 👎.
…s theorem Add the `no_external_calls` mutability modifier that rejects external call statements at compile time and auto-generates a `_no_calls` simp theorem. - Syntax: `function no_external_calls foo (...) : T := body` - New `stmtContainsExternalCall` / `exprContainsExternalCall` walkers in Validation.lean detect call/staticcall/delegatecall/externalCall/ecm - `noExternalCalls : Bool` added to FunctionDecl and FunctionSpec - `mkNoCallsTheoremCommand` in Bridge.lean emits `@[simp] theorem _no_calls` - NoExternalCallsSmoke test contract with compile-time theorem assertions - All `make check` CI invariants pass Phase 1 progress: Steps 1a, 1b, 1c done; Step 1d (annotation composition) remaining. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: deca050b4f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| exprContainsExternalCall offset | ||
| | Expr.keccak256 offset size => | ||
| exprContainsExternalCall offset || exprContainsExternalCall size | ||
| | _ => false |
There was a problem hiding this comment.
Traverse all Expr variants in no_external_calls scan
exprContainsExternalCall falls back to _ => false for any unhandled expression node, and several constructors with child expressions (e.g. Expr.ge, Expr.le, Expr.logicalAnd, Expr.logicalOr, Expr.signextend, Expr.extcodesize, Expr.arrayElement, Expr.internalCall) are currently unhandled. This lets annotated functions bypass no_external_calls by nesting call/staticcall/delegatecall/externalCall under one of those wrappers (for example inside ge(...)), so the validator can accept code that still performs external calls.
Useful? React with 👍 / 👎.
| | Stmt.internalCall _ args | Stmt.internalCallAssign _ _ args => | ||
| args.any exprContainsExternalCall |
There was a problem hiding this comment.
Treat internal calls as unknown external-call effects
For Stmt.internalCall/Stmt.internalCallAssign, the check only inspects argument expressions and never inspects the callee body, so a function marked no_external_calls can call an internal helper that performs externalCallBind, ecm, or low-level call expressions and still pass validation. That makes the annotation unsound unless the validator either performs interprocedural analysis or conservatively rejects internal calls under no_external_calls.
Useful? React with 👍 / 👎.
| (#1729, Axis 3 Step 1c). Emits a `@[simp]` lemma stating the model's | ||
| `noExternalCalls` flag is `true`. Only called when `fnDecl.noExternalCalls`. -/ | ||
| def mkNoCallsTheoremCommand (fnDecl : FunctionDecl) : CommandElabM Cmd := do | ||
| let noCallsName ← mkSuffixedIdent fnDecl.ident "_no_calls" |
There was a problem hiding this comment.
Reserve generated
_no_calls helper names
This commit starts auto-generating <fn>_no_calls declarations, but generated-name validation still only reserves the older helper suffixes, so user symbols like transfer_no_calls are accepted initially and then fail later with duplicate declarations when theorem generation runs. Please add _no_calls to the reserved/generated helper-name conflict checks to keep failures deterministic at validation time.
Useful? React with 👍 / 👎.
…on theorem When a function has 2+ effect annotations (view, no_external_calls, modifies), auto-generate an _effects theorem that bundles all individual effect facts into a single And proposition, simplifying downstream proof automation. - Add effectAnnotationCount and mkEffectsTheoremCommand in Bridge.lean - Wire _effects emission in Elaborate.lean for functions with ≥2 annotations - Add EffectCompositionSmoke test contract exercising all combinations - Update MacroTranslateInvariantTest with specs, sigs, selectors, theorem checks - Update MacroTranslateRoundTripFuzz with EffectCompositionSmoke.spec Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
verity/Verity/Macro/Translate.lean
Lines 3859 to 3863 in d639daa
_effects helper names
The elaborator now emits <fn>_effects declarations (mkEffectsTheoremCommand), but generated-name validation still only reserves _modelBody, _model, _bridge, and _semantic_preservation. This allows user symbols like transfer_effects to pass early validation and then fail later with duplicate declaration errors when theorem generation runs. Add _effects to the helper-name collision checks so the failure is caught deterministically during validation.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| syntax "payable" : verityMutability | ||
| syntax "view" : verityMutability | ||
| syntax "no_external_calls" : verityMutability | ||
| syntax "modifies(" sepBy(ident, ",") ")" : verityModifies |
There was a problem hiding this comment.
Reject empty
modifies() clauses
Using sepBy(ident, ",") allows modifies() with zero fields, and downstream validation/theorem generation is gated on !modifies.isEmpty, so this annotation becomes a silent no-op: write-set checks are skipped and no frame artifacts are emitted. That can mislead users into believing writes are constrained when they are not. Either require at least one field (sepBy1) or treat an empty clause as “writes nothing” and enforce it.
Useful? React with 👍 / 👎.
| | .mappingAddressToUint256 | .mappingChain _ | .mappingStruct _ _ => | ||
| -- ∀ k, s'.storageMap slot k = s.storageMap slot k | ||
| `(∀ k, s'.storageMap $slotLit k = s.storageMap $slotLit k) |
There was a problem hiding this comment.
Preserve mapping key types in generated frame predicates
This branch emits frame conjuncts for all mappingStruct fields via storageMap (address-keyed), ignoring whether the struct mapping key type is Uint256. For contracts that use MappingStruct(Uint256, ...), the generated _frame predicate constrains the wrong state component, so frame obligations can be proven without actually preserving the uint-keyed mapping field. The adjacent mappingStruct2 wildcard has the same key-type erasure risk. Split these cases by key types and use the corresponding storage relation.
Useful? React with 👍 / 👎.
…n_writes opt-out Add Checks-Effects-Interactions ordering enforcement as a default-on static analysis pass. Functions that write storage after an external call are rejected at compile time unless annotated with allow_post_interaction_writes. Compliant functions receive an auto-generated @[simp] _cei_compliant theorem. - Add allow_post_interaction_writes syntax modifier and FunctionDecl/FunctionSpec field - Implement stmtIsPersistentWrite, stmtIsDirectExternalCall, and mutual stmtListCEIViolation/stmtInternalCEIViolation walkers in Validation.lean - Wire CEI validation into validateFunctionSpec (runs at compileChecked time) - Generate _cei_compliant theorem in Bridge.lean/Elaborate.lean - Add CEISmoke positive test and CEIViolationRejected negative test (#guard_msgs) - Annotate 5 existing functions with allow_post_interaction_writes - Update invariant test, fuzz coverage exclusion, and property test artifacts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9fff4b4fba
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| own nested structure. -/ | ||
| def stmtInternalCEIViolation : Stmt → Option String | ||
| | Stmt.ite _ thenBranch elseBranch => | ||
| match stmtListCEIViolation thenBranch false with |
There was a problem hiding this comment.
Propagate prior external-call state into nested CEI checks
stmtInternalCEIViolation always checks if branches with seenCall = false, so an earlier interaction is forgotten once control reaches a compound statement. This lets patterns like externalCall ...; if cond then setStorage ... pass CEI validation even though the write is post-interaction on that path; the same reset happens for loop-body checks. The nested CEI walk needs to inherit the caller’s seenCall state.
Useful? React with 👍 / 👎.
| if seenCall && stmtIsPersistentWrite s then | ||
| some "state write after external call" | ||
| else | ||
| let newSeenCall := seenCall || stmtContainsExternalCall s | ||
| stmtListCEIViolation rest newSeenCall |
There was a problem hiding this comment.
Reject CEI violations when call and write occur in one stmt
The CEI check only errors on writes when seenCall was already true before the current statement. For statements that both contain an external call and perform a persistent write (for example setStorage/setMapping with a call expression on the RHS), newSeenCall is set after the statement and no violation is raised, allowing an interaction-before-effect sequence to slip through.
Useful? React with 👍 / 👎.
| elabCommand (← mkEffectsTheoremCommand fn) | ||
|
|
||
| -- Emit per-function _cei_compliant theorem for functions that pass CEI | ||
| -- validation without opting out (#1728, Axis 2 Step 2a). | ||
| for fn in functions do | ||
| if !fn.allowPostInteractionWrites then | ||
| elabCommand (← mkCEICompliantTheoremCommand fn) |
There was a problem hiding this comment.
Reserve new
_effects and _cei_compliant helper names
This commit now emits <fn>_effects and <fn>_cei_compliant declarations, but generated-name validation still reserves only the older helper suffixes in validateGeneratedDefNamesPublic (Verity/Macro/Translate.lean). A user symbol with either new suffix can pass early validation and then fail later with duplicate declarations during elaboration. Please add both suffixes to the generated-name conflict checks.
Useful? React with 👍 / 👎.
…i_safe Complete the four-rung CEI escalation ladder: Rung 1: Safe by default (compiler enforces CEI ordering) [Step 2a] Rung 2: cei_safe (machine-checked proof obligation) [NEW] Rung 3: nonreentrant(field) (known-safe reentrancy guard) [NEW] Rung 4: allow_post_interaction_writes (explicit trust-surface opt-out) [Step 2a] - Add nonreentrant(field) and cei_safe syntax modifiers - Add nonReentrantLock/ceiSafe fields to FunctionDecl and FunctionSpec - Refactor parseMutabilityModifiers to use ParsedMutability record (was 4-tuple) - Update CEI enforcement to bypass for any escalation rung - Validate nonreentrant lock field references and mutual exclusivity - Auto-generate _nonreentrant and _cei_safe @[simp] theorems - Add CEILadderSmoke test contract exercising all ladder rungs - Update invariant, fuzz, and property test infrastructure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b1fd6e3a44
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| -- Rung 2: cei_safe (machine-checked proof obligation) | ||
| -- Rung 3: nonreentrant(field) (known-safe reentrancy guard) | ||
| -- Rung 4: allow_post_interaction_writes (explicit trust-surface opt-out) | ||
| let ceiExempt := spec.allowPostInteractionWrites || spec.nonReentrantLock.isSome || spec.ceiSafe |
There was a problem hiding this comment.
Enforce proof before honoring
cei_safe exemption
Including spec.ceiSafe in ceiExempt disables CEI validation outright, but this change set does not generate or verify any proof obligation for that annotation, so cei_safe currently behaves like an unchecked opt-out. A function can mark itself cei_safe and still perform interaction-then-write sequences (as in CEILadderSmoke.callThenStoreProved) without any machine-checked evidence, which defeats the stated safety ladder.
Useful? React with 👍 / 👎.
| | some lockField => | ||
| let lockName := toString lockField.getId | ||
| let allFieldNames := fields.map (·.name) | ||
| if !allFieldNames.contains lockName then | ||
| throwErrorAt lockField s!"function '{fn.name}': nonreentrant references unknown storage field '{lockName}'; known fields: {allFieldNames.toList}" |
There was a problem hiding this comment.
Verify nonreentrant semantics before skipping CEI
The nonreentrant(...) path only checks that the named field exists, but CEI enforcement is later bypassed whenever nonReentrantLock.isSome; there is no validation that the function actually acquires/releases a lock or even touches that field. This allows unsafe post-interaction writes to pass simply by adding the annotation, turning a supposed "known-safe guard" into an unchecked bypass.
Useful? React with 👍 / 👎.
…ole theorem Add `requires(field)` annotation for functions that auto-injects a `require(caller == roleHolder)` check at the start of the function body. The named field must be an Address-typed storage field. Both source-level (DSL do-block) and IR-level (Stmt prelude) guards are generated. Changes: - Syntax.lean: new `verityRequiresRole` syntax category, `requires(ident)` - Translate.lean: `requiresRole` field on FunctionDecl, `resolveRoleField` validation, `mkRoleGuardedBody` (source-level), `roleGuardPreludeStmtTerms` (IR-level), wired into `translateBodyToStmtTerms` and FunctionSpec construction - Types.lean: `requiresRole : Option String` on FunctionSpec - Bridge.lean: `mkRequiresRoleTheoremCommand` generates `_requires_role` theorem - Elaborate.lean: emits `_requires_role` theorems for annotated functions - Smoke.lean: RolesSmoke test contract with requires(admin) - Integration tests: MacroTranslateInvariantTest, MacroTranslateRoundTripFuzz, PropertyRolesSmoke.t.sol Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d3a5e48d94
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ← `(Compiler.CompilationModel.Stmt.require | ||
| (Compiler.CompilationModel.Expr.eq | ||
| (Compiler.CompilationModel.Expr.caller) | ||
| (Compiler.CompilationModel.Expr.storage $(strTerm field.name))) |
There was a problem hiding this comment.
Read role guard from address storage accessor
The IR prelude generated for requires(...) compares Expr.caller against Expr.storage field, even though resolveRoleField enforces that the role field is Address-typed and the executable body uses getStorageAddr for the same check. In the model semantics this reads the wrong state component (storage vs storageAddr), so proofs/validation over FunctionSpec.body can disagree with the actual guarded function behavior for role-protected entrypoints.
Useful? React with 👍 / 👎.
| match fn.nonReentrantLock with | ||
| | some lockIdent => | ||
| elabCommand (← mkNonReentrantTheoremCommand fn (toString lockIdent.getId)) |
There was a problem hiding this comment.
Reserve new CEI/role helper theorem names
This commit now emits <fn>_nonreentrant, <fn>_cei_safe, and <fn>_requires_role theorems, but generated-name validation still only reserves the legacy helper suffixes, so user declarations with these names can pass early validation and then fail later with duplicate declarations during elaboration. Please add these new suffixes to the generated-name conflict checks to keep failures deterministic and upfront.
Useful? React with 👍 / 👎.
Add a `types` section to the verity_contract grammar that lets users declare semantic newtypes (e.g. `TokenId : Uint256`). At the language level the types are distinct identifiers; at the EVM/Yul level they are erased to their base types (zero overhead). Changes: - Syntax.lean: declare verityNewtype syntax category, add optional `types` section before `storage` in verityContractCmd - Translate.lean: NewtypeDecl structure, parseNewtype with scalar-only validation, thread newtypes array through all parse* and valueTypeFromSyntax functions, resolve unknown idents against newtypes before erroring - Elaborate.lean: destructure the new newtypes return value - Smoke.lean: NewtypeSmoke test contract (TokenId, Amount, Owner) with #check_contract - MacroTranslateInvariantTest: specs, signatures, selectors, theorems - MacroTranslateRoundTripFuzz: specs - generate_macro_property_tests.py: parse `types` section, resolve newtypes to base types in function params/return types - PropertyNewtypeSmoke.t.sol: auto-generated property tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every contract now emits a `storageNamespace : Nat` definition equal to
`keccak256("{ContractName}.storage.v0")` computed at elaboration time
using the vendored kernel-computable Keccak engine. This hash can serve
as a base offset so different contracts occupy non-overlapping regions of
the 2^256 storage address space (Step 4b will wire the offset).
Changes:
- Translate.lean: import Compiler.Keccak.Sponge, add byteArrayToNatBE
helper, computeStorageNamespace, mkStorageNamespaceCommand
- Elaborate.lean: emit storageNamespace after storage/immutable defs
- Smoke.lean: fix #check_contract ordering for RolesSmoke/NewtypeSmoke
(forward-reference bug from Step 3a), add storageNamespace type
verification examples
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `storage_namespace` keyword to the `verity_contract` grammar.
When present, all user-declared `slot N` values are offset by
`keccak256("{ContractName}.storage.v0")`, giving each contract a
unique region in the 2^256 storage address space (EIP-7201 pattern).
Existing contracts without `storage_namespace` are unaffected —
the feature is strictly opt-in, merging the original plan's steps
4b (slot offsetting) and 4c (override attributes) into a single
backwards-compatible mechanism.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thread `storageNamespace : Option Nat` through the CompilationModel so
downstream tooling can consume it:
- Add `storageNamespace` field to `CompilationModel` (default `none`)
- Pass namespace from `parseContractSyntax` → spec generation
- Emit `"storageNamespace"` key in layout report JSON
- Add `emitContractStorageLayoutJson` / `writeContractStorageLayoutFile`
in ABI.lean — writes `{Contract}.storage.json` alongside ABI output
- Smoke tests verify `spec.storageNamespace.isSome` for namespaced
contracts and `.isNone` for regular contracts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| \n### CI Failure Hints\n\nFailed jobs: `macro-fuzz`\n\nCopy-paste local triage:\n```bash\nmake check\nlake build\nFOUNDRY_PROFILE=difftest forge test -vv\n``` |
Add `unsafe "reason" do` syntax as a transparent wrapper around statement blocks, laying the groundwork for restricted-operation gating (Step 6b) and trust reporting (Step 6c). - New Stmt.unsafeBlock variant with reason string and body - doElem syntax: `unsafe "..." do <doSeq>` - Translate.lean: validation + translation cases - All 16 exhaustive Stmt pattern-match sites updated (transparent recurse-into-body semantics) - UnsafeBlockSmoke test contract with #check_contract - Integration tests updated (invariant, round-trip fuzz, property gen) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add unguarded mechanics collector that skips `unsafe "reason" do` block bodies, so low-level operations (mstore, call, staticcall, etc.) inside unsafe blocks no longer trigger the local_obligations requirement. Functions/constructors using low-level mechanics *outside* an unsafe block must still declare local_obligations or the validation pass rejects them with a clear error referencing Issue #1424. - TrustSurface: collectUnguardedLowLevelStmtMechanics (returns [] for unsafeBlock instead of recursing), plus public API wrappers - Validation: validateFunctionSpec/validateConstructorSpec now use the unguarded collector - Smoke tests: UnsafeGatingAccepted (mstore inside unsafe passes) and UnsafeGatingRejected (#guard_msgs negative test for mstore outside) - Integration: MacroTranslateInvariantTest, MacroTranslateRoundTripFuzz, property tests, fuzz coverage exclusion all updated Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…afe flag Add unsafe block tracking to the trust report infrastructure: - TrustSurface: collectUnsafeBlockReasons extracts reason strings from Stmt.unsafeBlock nodes; UsageSiteSummary gains unsafeBlocks field; JSON trust report and verbose CLI output both include unsafe blocks; emitUnsafeBlockUsageSiteLines provides per-site diagnostic lines - CompileDriverCommon: ensureNoUnsafeBlocks rejects contracts containing unsafe blocks when --deny-unsafe is set; verbose output includes "Unsafe block report" section - MainDriver: --deny-unsafe CLI flag parsed and threaded through - CompileDriverTest: deny-unsafe rejection test, pass-through test for contracts without unsafe blocks, trust report JSON content assertion Phase 6 (Unsafe Blocks) is now complete: 6a (syntax), 6b (gating), 6c (trust report + deny flag). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `inductive` section to `verity_contract` grammar for declaring algebraic data types (tagged unions) with typed variant fields. ADTs are parsed, validated (no duplicate names, no built-in shadowing), and threaded through the parse pipeline. IR generation deferred to Step 5b. Grammar: `inductive OptionalUint := | Some(value : Uint256) | None` Syntax categories: verityAdtVariant, verityAdtDecl Structures: AdtVariantDecl, AdtDecl (in Translate.lean) Parsers: parseAdtVariant, parseAdtDecl Files: Syntax.lean, Translate.lean, Elaborate.lean, Smoke.lean, MacroTranslateInvariantTest.lean, MacroTranslateRoundTripFuzz.lean Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add IR-level algebraic data type support across the compilation model: - Types.lean: AdtVariant, AdtTypeDef structs; ParamType.adt variant; Expr.adtConstruct/adtTag/adtField; Stmt.matchAdt; adtTypes in spec - Exhaustive pattern match coverage for ParamType.adt across AbiHelpers, AbiTypeLayout, AbiEncoding, EventAbiHelpers, ParamLoading - Exhaustive Expr ADT cases in LogicalPurity, UsageAnalysis, TrustSurface, ExpressionCompile, ValidationHelpers, Validation - Stmt.matchAdt handling with explicit recursive helpers (replacing branches.any/flatMap/all/for patterns that fail Lean termination) in Validation, ValidationCalls, ValidationEvents, ValidationInterop, ValidationHelpers, ScopeValidation, UsageAnalysis, LogicalPurity - Param now derives BEq (needed for ADT field comparison) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement tagged-union storage layout for ADTs and wire compilation through to Yul output. Key changes: - New AdtStorageLayout.lean: layout helpers (tag slot, field slots, total footprint), Yul fragment generators for sload/sstore - Expr.adtField now carries pre-resolved fieldIndex + storageField (avoids threading adtTypes through 90+ compileExpr call sites) - compileExpr: adtTag → sload(base) & 0xFF, adtField → sload(base+i+1) - compileStmt: matchAdt → YulStmt.switch on tag with field bindings - compileMatchAdtBranches: explicit recursive helper (avoids for-loop termination issues in mutual blocks) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- exprReadsStateOrEnv: recurse into adtConstruct args instead of returning true unconditionally, matching exprWritesState behavior and avoiding false rejection of pure functions constructing ADTs - CompileDriver: forward denyUnsafe through both compileSpecsWithOptions and compileModulesWithOptions wrappers so --deny-unsafe works via the public Compiler API - CompileDriverTest: add allowPostInteractionWrites to linkedLibrarySpec's storeHash (external call followed by storage write is a CEI violation that new validation correctly catches) - SourceSemantics proof: handle ADT expr cases in evalExprWithHelpers_eq_evalExpr_when_no_helpers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de34a5b3c4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| -- instead of reverting on failure. (#1727, Axis 1 Step 5f) | ||
| | Stmt.tryExternalCallBind successVar resultVars externalName args => do | ||
| let argExprs ← compileExprList fields dynamicSource args | ||
| let tryFnName := s!"{externalName}_try" |
There was a problem hiding this comment.
Validate or generate
_try wrapper before lowering calls
Stmt.tryExternalCallBind is lowered to a call to {externalName}_try (Compile.lean), but the target validation path only checks that externalName exists in spec.externals and never requires a corresponding _try symbol. In practice, a contract that declares external echo(...) and uses tryExternalCall "echo" emits Yul calling echo_try; linking libraries that satisfy ordinary externalCall now fails with unresolved references unless authors add an undocumented extra helper. Either synthesize the _try wrapper from the declared external or validate that _try is explicitly declared/provided.
Useful? React with 👍 / 👎.
…n pipeline Propagate the new `adtTypes` parameter through all proof files in the IRGeneration chain. The `compileFunctionSpec` and `compileInternalFunction` functions gained an `adtTypes` positional parameter, requiring updates to every call site in the proof files and threading `hnoAdtTypes` hypotheses through the theorem signatures. - SupportedSpec: add `noAdtTypes` field and shortcut theorems - GenericInduction: thread `hnoAdtTypes` hypothesis through induction lemmas - FunctionBody: add `hnoAdtTypes` to body correctness theorems - Function: add `hnoAdtTypes` to function-level theorems, bridge type mismatches with rewrite/subst patterns - Dispatch: insert `[]` for adtTypes at all `compileFunctionSpec` call sites - Contract: insert `[]` for adtTypes, update `compileValidatedCore` rewrites - ContractFeatureTest: add missing `noAdtTypes` field - Regenerate PrintAxioms.lean Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 962d000b3c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| | Expr.adtConstruct adtName variantName _args => | ||
| throw s!"Compilation error: ADT construct '{adtName}.{variantName}' cannot be used in expression position. ADT construction expands to multiple sstores and must be compiled at the statement level." |
There was a problem hiding this comment.
Lower
Expr.adtConstruct instead of throwing
This new IR constructor is advertised as part of ADT support, but codegen currently aborts as soon as it encounters it. Any function body that constructs an ADT value (for example via a storage assignment or return expression that contains Expr.adtConstruct) will fail during Yul compilation, so ADT construction is effectively unusable even though the type and validation plumbing was added.
Useful? React with 👍 / 👎.
| params := ← params.mapM (valueTypeFromSyntax newtypes #[]) | ||
| returnTys := ← returnTys.mapM (valueTypeFromSyntax newtypes #[]) |
There was a problem hiding this comment.
Thread parsed ADTs into external signature parsing
External declarations are still parsed with valueTypeFromSyntax ... #[], so ADT names from the contract's inductive section are ignored at this boundary. In practice, external params/returns that reference a declared ADT are rejected as unsupported types, which blocks ADT-typed external-call signatures and undermines the new ADT ABI interop path.
Useful? React with 👍 / 👎.
Add mstore and tstore as first-class constructors in StmtCompileCore, StmtListCompileCore, and StmtListTerminalCore, enabling these memory and transient-storage operations to appear inside proven compile-core sequences rather than only as singleton SupportedStmtList entries. This is the highest-leverage proof surface widening for issue #1723. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a677c37841
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| | Expr.internalCall _ args | Expr.adtConstruct _ _ args => | ||
| exprListContainsExternalCall args |
There was a problem hiding this comment.
Treat
Expr.internalCall as effectful in external-call scans
exprContainsExternalCall currently treats Expr.internalCall as external-call-free unless its arguments contain a low-level call, so expression-form internal calls to helpers that perform externalCallBind/tryExternalCallBind/ecm are missed. Because both no_external_calls validation and CEI tracking flow through this scanner (via stmtContainsExternalCall), a function can bypass those checks by invoking an effectful internal helper in expression position (for example in letVar, assignVar, or RHS expressions) and still compile as compliant.
Useful? React with 👍 / 👎.
Internal calls (internalCall, internalCallAssign) have callee bodies that are not visible at single-function validation scope. Previously: 1. `modifies(...)` only checked `stmtWrittenFields` which returns [] for call statements, silently allowing internal calls to write to any field. 2. `no_external_calls` only checked `stmtContainsExternalCall` which only looked at direct external calls, missing external calls inside callees. Added two conservative validation functions: - `stmtHasUntrackableWrites`: flags internal calls for modifies() validation - `stmtMayContainExternalCall`: flags internal calls for no_external_calls External calls (externalCallBind, tryExternalCallBind, ecm) are correctly excluded from the modifies() check — they target other contracts and cannot directly modify the current contract's storage fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
verity/Verity/Macro/Translate.lean
Line 3195 in 7ace9cc
setStorageAddr only matches .scalar .address, so fields declared as address-based newtypes are rejected with "not Address" even though getStorageAddr already treats .newtype _ .address as valid. This makes address newtypes non-roundtrippable for storage updates and breaks the advertised zero-cost newtype behavior.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if bodyHasCall && bodyHasWrite then | ||
| some "loop body contains both external call and state write (subsequent iterations would violate CEI)" | ||
| else | ||
| match stmtListCEIViolation body seenCall with |
There was a problem hiding this comment.
Propagate loop-count calls into CEI body analysis
The CEI walker analyzes forEach bodies with stmtListCEIViolation body seenCall, but it does not include external calls that happen in the loop count expression. That lets patterns like forEach i (externalCall ...) do setStorage ... pass CEI even though the body writes after an interaction in the same statement. The loop-body check should receive a call-state that includes the count expression’s external-call status.
Useful? React with 👍 / 👎.
| let f ← lookupStorageField fields (toString field.getId) | ||
| match f.ty with | ||
| | .scalar .uint256 => | ||
| | .scalar .uint256 | .scalar (.adt _ _) => |
There was a problem hiding this comment.
Allow uint-based newtypes in setStorage lowering
setStorage now accepts plain Uint256 and ADT fields, but not .newtype _ .uint256. As a result, storage fields declared with a uint-based newtype can be read (getStorage supports them) but cannot be written, so valid newtype contracts fail during macro translation when assigning to those fields.
Useful? React with 👍 / 👎.
…arbitrary The statement-level compilation compatibility lemmas in GenericInduction.lean previously hardcoded `[] []` for events and errors parameters. Since none of the supported statement forms (letVar, assignVar, require, return, stop, mstore, tstore, ite, setStorage, setMapping, etc.) consume events or errors during compilation, these proofs hold for arbitrary event/error lists. This generalization eliminates the need for Contract.lean to rewrite `model.events → []` and `model.errors → []` in the compilation layer, which is the foundational step toward removing `noEvents`/`noErrors` from `SupportedSpecSurface` entirely once Function.lean is similarly generalized. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1dc7cc87dc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| def stmtHasUntrackableWrites : Stmt → Bool | ||
| | Stmt.internalCall _ _ | Stmt.internalCallAssign _ _ _ => true | ||
| | Stmt.ite _ thenBranch elseBranch => |
There was a problem hiding this comment.
Flag expression internal calls as untrackable in modifies checks
The modifies(...) guard only treats statement-form internal calls as untrackable (Stmt.internalCall/Stmt.internalCallAssign), so internal helpers invoked inside expressions are missed. A function like setStorage declaredField (internalCall helper ...) can pass modifies validation even if helper writes additional storage fields, which makes generated _modifies/_frame guarantees unsound whenever expression-form internal calls are used.
Useful? React with 👍 / 👎.
| ident := name | ||
| name := toString name.getId | ||
| params := ← params.mapM valueTypeFromSyntax | ||
| params := ← params.mapM (valueTypeFromSyntax newtypes #[]) |
There was a problem hiding this comment.
Pass ADT declarations when parsing custom-error parameters
parseError parses parameter types with valueTypeFromSyntax newtypes #[], which drops all inductive ADT declarations at this boundary. As a result, error definitions that reference a declared ADT are rejected as unsupported types during parsing, so ADT-typed custom errors cannot be authored even though the rest of this change set adds ADT ABI/type plumbing.
Useful? React with 👍 / 👎.
- Support newtype-typed storage fields in setStorage/setStorageAddr (previously only reads worked; writes rejected newtypes as unknown type) - Fix frame conjunct generation to use sameStorageAddrSlot for address newtypes (was incorrectly using sameStorageSlot) - Add tstore to stmtIsPersistentWrite so transient storage writes trigger CEI violation detection - Propagate external calls from ite conditions and forEach count expressions into CEI analysis (previously only body was checked) - Add allow_post_interaction_writes to LowLevelTryCatchSmoke functions (pre-existing CEI violation from write-after-call in catch blocks) - Add NewtypeStorageSmoke test contract exercising newtype-typed storage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f3514a8b57
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let scrutineeExpr ← compileExpr fields dynamicSource scrutinee | ||
| -- Extract storage field name from scrutinee for field bindings | ||
| let storageFieldName ← match scrutinee with | ||
| | Expr.adtTag _ fieldName => pure fieldName |
There was a problem hiding this comment.
Validate
matchAdt scrutinee type before codegen
Stmt.matchAdt drops the ADT name carried by Expr.adtTag (| Expr.adtTag _ fieldName => ...) and proceeds using only the storage field name. This allows mismatched IR such as matching ADT A while tagging a field of ADT B (or a non-ADT field), so case tags/field bindings are derived from A but reads come from a different slot layout. In that scenario codegen emits a semantically incorrect switch and sload(base+...) sequence instead of rejecting the statement; add a consistency check that the scrutinee’s ADT/field type matches adtName before compiling branches.
Useful? React with 👍 / 👎.
Add mstore and tstore induction alternatives to all StmtListCompileCore and StmtListTerminalCore inductions that were missing them after the compile-core fragment widening (a677c37). This fixes the CI build failure on the `build` job. Affected theorems: - exec_compileStmtList_core (StmtListCompileCore, length+1 fuel) - exec_compileStmtList_core_extraFuel (StmtListCompileCore, length+extraFuel+1 fuel) - execStmtList_terminal_core_not_continue (StmtListTerminalCore, no IR) - exec_compileStmtList_terminal_core_sizeOf_extraFuel (StmtListTerminalCore, sizeOf fuel) Each mstore case: compile offset+value, show source continues with updated memory, show IR continues with matching state, recurse on tail. Each tstore case: same pattern but updating transientStorage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d1058a16d8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| match field.ty with | ||
| | .scalar .address => pure field | ||
| | _ => throwErrorAt roleIdent s!"function '{toString fnIdent.getId}': requires({roleName}) must reference an Address-typed storage field, but '{roleName}' has a different type" |
There was a problem hiding this comment.
Accept address newtypes in requires(role) checks
resolveRoleField only accepts .scalar .address, so a role field declared as an address-backed newtype (for example types Owner : Address with owner : Owner) is rejected even though newtypes are otherwise erased and handled as their base types. This makes requires(...) unusable for valid address newtype storage fields and breaks the intended zero-cost newtype workflow for access control.
Useful? React with 👍 / 👎.
| let slot := f.slot.getD idx | ||
| let entry := "{" ++ joinJsonFields [ | ||
| s!"\"name\": {jsonString f.name}", | ||
| s!"\"slot\": {toString slot}", |
There was a problem hiding this comment.
Serialize storage layout slots without precision loss
The storage layout writer emits slot as a bare JSON number. For namespaced contracts, slots are offset by a keccak-derived 256-bit namespace and routinely exceed IEEE-754 safe integer range, so common JSON consumers (especially JS-based tooling) will round these values and derive incorrect storage locations. Emit slots as strings (like storageNamespace) to preserve exact slot identities.
Useful? React with 👍 / 👎.
… newtype roles, slot precision) - matchAdt CEI: propagate scrutinee external calls into branch analysis - ADT custom errors: allow ParamType.adt in supportedCustomErrorParamType - ADT error/external parsing: thread parsedAdts into parseError and parseExternal so ADT-typed parameters are recognized in error and external declarations - requires(role): accept address-backed newtypes (.newtype _ .address) - Storage layout JSON: emit slot as string to prevent JS precision loss on 256-bit namespaced slot values Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 107d2c5aa7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| | ParamType.uint256 | ParamType.int256 | ParamType.uint8 | ParamType.address | ParamType.bool | ParamType.bytes32 | ||
| | ParamType.adt _ _ => | ||
| let encoded ← encodeStaticCustomErrorArg errorDef.name ty argExpr |
There was a problem hiding this comment.
Route ADT custom-error args through composite encoding
This branch treats ParamType.adt as a scalar and forwards it to encodeStaticCustomErrorArg, but that helper only accepts uint/int/address/bool/bytes32 and throws on all other types. Since ADT custom-error params are now accepted by validation, requireError/revertError with an ADT argument will pass checks and then fail during codegen with an "unsupported non-scalar parameter type" error. ADT arguments need to be encoded via the composite path instead of the scalar helper.
Useful? React with 👍 / 👎.
| | .mapping2AddressToAddressToUint256 | .mappingStruct2 _ _ _ => | ||
| `(∀ k1 k2, s'.storageMap2 $slotLit k1 k2 = s.storageMap2 $slotLit k1 k2) |
There was a problem hiding this comment.
Constrain double-mapping frame predicates on hashed storage
The generated frame conjunct for both .mapping2AddressToAddressToUint256 and .mappingStruct2 only asserts equality of storageMap2, but compilation-model semantics for setMapping2/setStructMember2 update hashed slots in world.storage (via mapping-slot writes), not storageMap2. This means a function can modify a non-modifies double-mapping field while the generated _frame predicate still holds, making the effect guarantee unsound for those field types.
Useful? React with 👍 / 👎.
…heorems The CI build was failing because four theorems in GenericInduction.lean (stmtListCompileCore_of_scopeNamesIncluded, stmtListTerminalCore_of_scopeNamesIncluded, stmtListGenericCore_of_stmtListCompileCore_of_scopeNamesIncluded, stmtListGenericCore_of_stmtListTerminalCore_of_scopeNamesIncluded) were missing match alternatives for the mstore and tstore constructors that were added to StmtListCompileCore and StmtListTerminalCore in a previous commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd6181f463
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| match f.ty with | ||
| | .scalar .uint256 => pure .uint256 | ||
| | .scalar (.newtype ntName (.uint256)) => pure (.newtype ntName .uint256) | ||
| | .scalar (.newtype _ (.address)) => throwErrorAt rhs s!"field '{f.name}' is Address-based newtype; use getStorageAddr" | ||
| | .scalar .address => throwErrorAt rhs s!"field '{f.name}' is Address; use getStorageAddr" |
There was a problem hiding this comment.
Accept ADT storage fields in getStorage bind typing
inferBindSourceType rejects getStorage <field> unless the field is Uint256/newtype-Uint256, but ADT-typed storage fields are also lowered through Expr.storage in translateBindSource. As a result, valid code like let s ← getStorage myAdtField fails during translation/type inference with the mapping fallback error, even though ADT storage read support is otherwise present. Add a .scalar (.adt _ _) case here so ADT storage binds can type-check consistently.
Useful? React with 👍 / 👎.
…rs/adtTypes The theorem was hardcoded to work only with empty events, errors, and adtTypes lists, causing type mismatch errors in GenericInduction.lean callers that pass general values. Generalizing the implicit parameters fixes the CI build failure while remaining backwards-compatible with all existing callers (which unify [] with the new implicit params). Also cleans up several linter warnings: unnecessary simpa → simp/exact, and simpa using → simp at for contradiction-based proofs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfc3b59b4e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| match s with | ||
| | Stmt.internalCall _ _ | Stmt.internalCallAssign _ _ _ => true | ||
| | _ => stmtContainsExternalCall s |
There was a problem hiding this comment.
Recurse conservative internal-call detection for no_external_calls
no_external_calls validation can be bypassed when an internal call is nested inside a compound statement (if, forEach, unsafeBlock, matchAdt). validateFunctionSpec checks spec.body.any stmtMayContainExternalCall, but stmtMayContainExternalCall only treats top-level Stmt.internalCall/Stmt.internalCallAssign as conservative and otherwise falls back to stmtContainsExternalCall, which does not conservatively flag nested internal calls. As a result, a function annotated no_external_calls can still pass validation while calling an internal helper that performs external calls, making the generated no-calls guarantee unsound in those control-flow shapes.
Useful? React with 👍 / 👎.
… suite CEIWriteInBranchAfterCall, CEICallBothBranchesWrite, and UnsafeCEIViolation are negative test contracts that deliberately violate CEI ordering (tested via #guard_msgs in Smoke.lean). They should not be in the round-trip fuzz macroSpecs list, which calls compileChecked and fails on validation errors. Fixes the macro-fuzz CI shard failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e0a8df267
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| | .scalar (.tuple _) => throwError "storage fields cannot be Tuple; use mapping encodings" | ||
| | .scalar .unit => throwError "storage fields cannot be Unit" | ||
| | .scalar (.newtype _ baseType) => modelFieldTypeTerm (.scalar baseType) -- Erased to base type | ||
| | .scalar (.adt _ _) => `(Compiler.CompilationModel.FieldType.uint256) -- ADT stored as tag (uint256 slot) |
There was a problem hiding this comment.
Model ADT storage fields with full slot footprint
ADT storage is lowered as a tagged union spanning multiple consecutive slots (tag at base, payload at base+1...), but this mapping records every ADT storage field as FieldType.uint256 (single-slot). Because downstream layout checks and slot assignment reason over that model, overlaps in ADT payload slots are not detected (including auto-assigned immutables via nextUserSlot), so contracts can compile while matchAdt reads/writes slots that are also used by other fields. The storage model should reserve the full ADT slot range (e.g., dedicated ADT field shape or populated alias slots) so conflict detection and emitted layout remain sound.
Useful? React with 👍 / 👎.
…ge writes The CEI (Checks-Effects-Interactions) validator previously used `stmtIsPersistentWrite` which returned false for internal calls, so a pattern like `externalCallBind(...); internalCall(helper, [...])` where the helper writes storage would silently pass CEI validation. Add `stmtMayPersistentlyWrite` — a conservative variant that treats `internalCall` and `internalCallAssign` as potential persistent writes (since their callee bodies are not visible at single-function scope). Use it in the CEI write-after-call check. Note: internal calls do NOT set `seenCall` for CEI purposes — each callee function has its own CEI validation, so treating internal calls as both interactions and writes would produce false positives on contracts like DirectHelperCallSmoke. Also fix a guard_pending state leak in generate_macro_property_tests.py where `#guard_msgs in` / `#check_contract Foo` would incorrectly skip the next `verity_contract` declaration (affected RolesCEISmoke, ModifiesRolesSmoke, UnsafeCEICompliant property test generation). Addresses P1 review threads #23 and #5 on PR #1731. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9b9a3d5. Configure here.
| -- In a loop, if the body has both an external call and a state write, | ||
| -- a second iteration would violate CEI even if the first doesn't | ||
| let bodyHasCall := body.any stmtContainsExternalCall | ||
| let bodyHasWrite := body.any stmtIsPersistentWrite |
There was a problem hiding this comment.
Loop CEI pre-check misses internal-call writes across iterations
Medium Severity
The forEach branch in stmtInternalCEIViolation uses body.any stmtIsPersistentWrite for its cross-iteration pre-check, but stmtIsPersistentWrite returns false for internal calls. The sequential analysis in stmtListCEIViolation correctly uses stmtMayPersistentlyWrite (which conservatively treats internal calls as potential writes). This inconsistency means a loop body like [internalCall(helper_that_writes), externalCallBind(...)] passes validation — the pre-check misses the internal-call write, and the single-iteration sequential analysis sees the write before the call. But on the second iteration, the internal call's storage write follows the previous iteration's external call, which is a CEI violation.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 9b9a3d5. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9b9a3d5d33
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| -- Rung 2: cei_safe (machine-checked proof obligation) | ||
| -- Rung 3: nonreentrant(field) (known-safe reentrancy guard) | ||
| -- Rung 4: allow_post_interaction_writes (explicit trust-surface opt-out) | ||
| let ceiExempt := spec.allowPostInteractionWrites || spec.nonReentrantLock.isSome || spec.ceiSafe |
There was a problem hiding this comment.
Enforce nonreentrant guard before skipping CEI checks
This exempts any function with nonReentrantLock from CEI, but the new pipeline does not synthesize a lock acquire/release around the function body (the lock name is only carried as metadata/theorem input), so nonreentrant(lock) currently disables CEI without adding runtime protection. In practice, a function can perform externalCall followed by storage writes and be accepted as “guarded” while still being reentrancy-prone.
Useful? React with 👍 / 👎.
| `parseContractSyntax` to respect custom `storage_namespace "key"`. | ||
| (#1730, Axis 4 Step 4a) -/ | ||
| def mkStorageNamespaceCommand (contractName : String) (resolvedNamespace : Option Nat := none) : CommandElabM Cmd := do | ||
| let ns := resolvedNamespace.getD (computeStorageNamespace contractName) |
There was a problem hiding this comment.
Emit inactive namespace value when namespacing is off
For contracts without a storage_namespace clause, parseContractSyntax leaves slots unshifted and stores storageNamespace = none, but this fallback emits keccak(contractName.storage.v0) anyway. That makes Contract.storageNamespace disagree with the actual slot base (0) for non-namespaced contracts, so downstream slot-derivation tooling/proofs that trust this constant can compute incorrect storage locations.
Useful? React with 👍 / 👎.


Summary
Implements the complete language design improvement plan for Verity's
verity_contractEDSL — 25 steps across 6 axes, taking the language beyond Solidity parity with verified safety properties.63 files changed, +3349 / -235 lines
Axis 1: Effect System (Steps 1a–1d)
@[view]theorem generation (1a): Auto-generates_is_viewtheorems proving functions make no state writesmodifies(...)annotation (1b): Declares which storage fields a function may write; generates_frametheorem proving all other fields are preservedno_external_callsannotation (1c): Forbids external calls; generates_no_callstheorem_effectsconjunction theorem combining all individual guaranteesAxis 2: Safety Enforcement (Steps 2a–2c)
allow_post_interaction_writesnonreentrant(lock)generates reentrancy guard with_nonreentranttheorem;cei_safemarks functions as CEI-compliant by developer assertionrequires(role)access control (2c): Guards function entry onmsg.sender == role_field; generates_requires_roletheoremAxis 3: Newtypes (Steps 3a–3c)
typessection (3a):verityNewtypesyntax for zero-cost semantic wrappers (e.g.,TokenId : Uint256)ParamType.newtypeOfandValueType.newtypepropagate through all 11+ compilation model files; newtypes erase to their base type at every ABI/Yul boundaryAxis 4: Storage Safety (Steps 4a–4d)
computeStorageNamespacehashes the contract name via kernel Keccak at elaboration timestorage_namespacekeyword (4b): Opt-in; when present, allslot Nbecomeslot (namespaceBase + N)storage_namespace "custom.key"overrides the default contract-name hash{contract}.storage.jsonincludesstorageNamespace; layout reports include the namespace baseAxis 5: ADTs + External Calls (Steps 5a–5f)
inductivesection (5a): Grammar for algebraic data types with named variants and typed fieldsParamType.adt,Expr.adtConstruct/adtTag/adtField,Stmt.matchAdtacross ~17 compilation model files(uint8 tag, uint256 fields...);matchcompiles to Yulswitchon tagtryExternalCall(5f): Non-reverting external calls withCall.Resultreturn type;!sugar for force-unwrapadtTypesfield wired from parsed syntax through to specAxis 6: Unsafe Blocks (Steps 6a–6c)
unsafe "reason" dosyntax (6a): Wraps low-level operations requiring explicit justificationunsafewrapper--deny-unsafe(6c): Unsafe blocks appear in the trust report;--deny-unsafeflag rejects any unsafe usageBug Fixes (Post-Review)
Addresses all CI failures and review comments from Codex/Bugbot:
MacroExternal.storeEchoCEI violation fixed withallow_post_interaction_writesseenCallpropagated into branches; same-stmt call+write detection;stmtIsPersistentWritemade recursive for compound statementsexprContainsExternalCall: Added 11 missing Expr variantsExpr.storage→Expr.storageAddrfor address fieldsmappingStructsplit by key type (.address→storageMap,.uint256→storageMapUint)modifies():sepBy→sepBy1to reject empty clausemodifiesfieldTest plan
#check_contractpass in Smoke.lean (including new contracts: EffectAnnotationSmoke, ModifiesSmoke, NoExternalCallsSmoke, EffectCompositionSmoke, CEISmoke, CEILadderSmoke, RolesSmoke, NewtypeSmoke, NamespacedStorageSmoke, CustomNamespacedSmoke, AdtSmoke, UnsafeBlockSmoke, UnsafeGatingAccepted, ExternalCallMultiReturn, TryExternalCallSmoke, CEIViolationRejected)lake buildpasses (full build)make checkpasses (425 tests)🤖 Generated with Claude Code
Note
High Risk
High risk because this changes core IR types, ABI encoding/decoding, statement compilation, and validator logic (including CEI and trust-surface gates), which can affect generated Yul and safety checks across all contracts.
Overview
Implements new IR surface area for ADTs (tagged unions) and zero-cost newtypes, wiring them through ABI rendering, ABI encode/decode, param loading, event/custom-error support, and statement/expression compilation (including
matchAdtlowering to Yulswitchand storage-backed tag/field reads).Adds new effect/safety validations on function specs:
modifies(...)andno_external_callsenforcement, compile-time CEI (Checks-Effects-Interactions) ordering rejection with opt-outs (e.g.allow_post_interaction_writes), plus anunsafe "reason" dostatement wrapper that scopes low-level mechanics and is surfaced in trust reporting.Extends tooling outputs and CLI: emits
{contract}.storage.jsonwith optional EIP-7201storageNamespace, augments layout/trust reports with namespace/unsafe-block info, and introduces--deny-unsafeto fail compilation when anyunsafeblocks are present.Reviewed by Cursor Bugbot for commit 9b9a3d5. Bugbot is set up for automated code reviews on this repo. Configure here.