Skip to content

Commit d639daa

Browse files
committed
feat(axis3): step 1d — annotation composition with _effects conjunction 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>
1 parent deca050 commit d639daa

6 files changed

Lines changed: 119 additions & 0 deletions

File tree

Contracts/MacroTranslateInvariantTest.lean

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ private def macroSpecs : List CompilationModel :=
324324
, Contracts.Smoke.LocalObligationRequiredForUnsafeConstructorBoundary.spec
325325
, Contracts.Smoke.ModifiesSmoke.spec
326326
, Contracts.Smoke.NoExternalCallsSmoke.spec
327+
, Contracts.Smoke.EffectCompositionSmoke.spec
327328
]
328329

329330
private def functionSignature (fn : FunctionSpec) : String :=
@@ -405,6 +406,7 @@ private def expectedExternalSignatures : List (String × List String) :=
405406
, ("LocalObligationRequiredForUnsafeConstructorBoundary", ["noop()"])
406407
, ("ModifiesSmoke", ["increment()", "transferOwnership(address)", "deposit(uint256)", "getCounter()"])
407408
, ("NoExternalCallsSmoke", ["increment()", "getCounter()", "setOwner(address)"])
409+
, ("EffectCompositionSmoke", ["getCounter()", "increment()", "setOwner(address)", "deposit(uint256)"])
408410
]
409411

410412
private def expectedExternalSelectors : List (String × List String) :=
@@ -469,6 +471,7 @@ private def expectedExternalSelectors : List (String × List String) :=
469471
, ("LocalObligationRequiredForUnsafeConstructorBoundary", ["0x5dfc2e4a"])
470472
, ("ModifiesSmoke", ["0xd09de08a", "0xf2fde38b", "0xb6b55f25", "0x8ada066e"])
471473
, ("NoExternalCallsSmoke", ["0xd09de08a", "0x8ada066e", "0x13af4035"])
474+
, ("EffectCompositionSmoke", ["0x8ada066e", "0xd09de08a", "0x13af4035", "0xb6b55f25"])
472475
]
473476

474477
private def expectedFor
@@ -519,6 +522,14 @@ private def checkMutabilitySmoke : IO Unit := do
519522
-- Verify auto-generated _no_calls theorem exists (#1729, Axis 3 Step 1c).
520523
let _ := @Contracts.Smoke.NoExternalCallsSmoke.increment_no_calls
521524
let _ := @Contracts.Smoke.NoExternalCallsSmoke.getCounter_no_calls
525+
-- Verify auto-generated _effects conjunction theorem exists (#1729, Axis 3 Step 1d).
526+
-- getCounter has view + no_external_calls → _effects bundles both
527+
let _ := @Contracts.Smoke.EffectCompositionSmoke.getCounter_effects
528+
-- increment has no_external_calls + modifies → _effects bundles both
529+
let _ := @Contracts.Smoke.EffectCompositionSmoke.increment_effects
530+
-- Also verify the existing NoExternalCallsSmoke.getCounter gets _effects
531+
-- (it has view + no_external_calls)
532+
let _ := @Contracts.Smoke.NoExternalCallsSmoke.getCounter_effects
522533

523534
private def checkSignedBuiltinSmoke : IO Unit := do
524535
let functions := Contracts.Smoke.SignedBuiltinSmoke.spec.functions

Contracts/MacroTranslateRoundTripFuzz.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ private def macroSpecs : List CompilationModel :=
7979
, Contracts.Smoke.LowLevelTryCatchSmoke.spec
8080
, Contracts.Smoke.ModifiesSmoke.spec
8181
, Contracts.Smoke.NoExternalCallsSmoke.spec
82+
, Contracts.Smoke.EffectCompositionSmoke.spec
8283
]
8384

8485
private structure FuzzRng where

Contracts/Smoke.lean

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,4 +1680,31 @@ verity_contract NoExternalCallsSmoke where
16801680
function setOwner (newOwner : Address) : Unit := do
16811681
setStorageAddr owner newOwner
16821682

1683+
-- #1729, Axis 3 Step 1d: smoke test for annotation composition (_effects theorem)
1684+
verity_contract EffectCompositionSmoke where
1685+
storage
1686+
counter : Uint256 := slot 0
1687+
owner : Address := slot 1
1688+
balances : Address → Uint256 := slot 2
1689+
1690+
-- view + no_external_calls: _effects bundles _is_view ∧ _no_calls
1691+
function view no_external_calls getCounter () : Uint256 := do
1692+
let current ← getStorage counter
1693+
return current
1694+
1695+
-- no_external_calls + modifies: _effects bundles _no_calls ∧ _modifies
1696+
function no_external_calls increment () modifies(counter) : Unit := do
1697+
let current ← getStorage counter
1698+
setStorage counter (add current 1)
1699+
1700+
-- Single annotation only (no _effects expected)
1701+
function no_external_calls setOwner (newOwner : Address) : Unit := do
1702+
setStorageAddr owner newOwner
1703+
1704+
-- No annotations at all
1705+
function deposit (amount : Uint256) : Unit := do
1706+
let sender ← msgSender
1707+
let balance ← getMapping balances sender
1708+
setMapping balances sender (add balance amount)
1709+
16831710
end Contracts.Smoke

Verity/Macro/Bridge.lean

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,52 @@ def mkFrameDefCommand
133133

134134
pure #[frameCmd, frameRflCmd]
135135

136+
/-- Count how many effect annotations are active on a function declaration. -/
137+
def effectAnnotationCount (fnDecl : FunctionDecl) : Nat :=
138+
(if fnDecl.isView then 1 else 0) +
139+
(if fnDecl.noExternalCalls then 1 else 0) +
140+
(if !fnDecl.modifies.isEmpty then 1 else 0)
141+
142+
/-- Auto-generated `_effects` conjunction theorem for functions with multiple
143+
effect annotations (#1729, Axis 3 Step 1d). Bundles all individual effect
144+
theorems (`_is_view`, `_no_calls`, `_modifies`) into a single `And` fact so
145+
downstream proofs can obtain all guarantees in one `exact`. -/
146+
def mkEffectsTheoremCommand (fnDecl : FunctionDecl) : CommandElabM Cmd := do
147+
let effectsName ← mkSuffixedIdent fnDecl.ident "_effects"
148+
let modelName ← mkSuffixedIdent fnDecl.ident "_model"
149+
-- Collect conjunct terms and proof terms for each active annotation
150+
let mut conjuncts : Array Term := #[]
151+
let mut proofs : Array Term := #[]
152+
if fnDecl.isView then
153+
let viewName ← mkSuffixedIdent fnDecl.ident "_is_view"
154+
conjuncts := conjuncts.push (← `(
155+
(Compiler.CompilationModel.FunctionSpec.isView
156+
($modelName : Compiler.CompilationModel.FunctionSpec)) = true))
157+
proofs := proofs.push (← `($viewName))
158+
if fnDecl.noExternalCalls then
159+
let noCallsName ← mkSuffixedIdent fnDecl.ident "_no_calls"
160+
conjuncts := conjuncts.push (← `(
161+
(Compiler.CompilationModel.FunctionSpec.noExternalCalls
162+
($modelName : Compiler.CompilationModel.FunctionSpec)) = true))
163+
proofs := proofs.push (← `($noCallsName))
164+
if !fnDecl.modifies.isEmpty then
165+
let modifiesName ← mkSuffixedIdent fnDecl.ident "_modifies"
166+
let fieldTerms : Array Term := fnDecl.modifies.map fun ident => strTermPublic (toString ident.getId)
167+
conjuncts := conjuncts.push (← `(
168+
(Compiler.CompilationModel.FunctionSpec.modifies
169+
($modelName : Compiler.CompilationModel.FunctionSpec)) = [ $[$fieldTerms],* ]))
170+
proofs := proofs.push (← `($modifiesName))
171+
-- Build right-nested And: P₁ ∧ P₂ ∧ ... ∧ Pn
172+
-- For n=2: And.intro p₁ p₂
173+
-- For n=3: And.intro p₁ (And.intro p₂ p₃)
174+
let mut propTerm := conjuncts[conjuncts.size - 1]!
175+
for i in List.range (conjuncts.size - 1) |>.reverse do
176+
propTerm ← `($(conjuncts[i]!) ∧ $propTerm)
177+
let mut proofTerm := proofs[proofs.size - 1]!
178+
for i in List.range (proofs.size - 1) |>.reverse do
179+
proofTerm ← `(And.intro $(proofs[i]!) $proofTerm)
180+
`(command|
181+
@[simp] theorem $effectsName :
182+
$propTerm := $proofTerm)
183+
136184
end Verity.Macro

Verity/Macro/Elaborate.lean

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ def elabVerityContract : CommandElab := fun stx => do
7171
for cmd in frameCmds do
7272
elabCommand cmd
7373

74+
-- Emit per-function _effects conjunction theorem when multiple effect
75+
-- annotations are active on the same function (#1729, Axis 3 Step 1d).
76+
for fn in functions do
77+
if effectAnnotationCount fn ≥ 2 then
78+
elabCommand (← mkEffectsTheoremCommand fn)
79+
7480
elabCommand (← `(end $contractName))
7581
catch err =>
7682
elabCommand (← `(end $contractName))
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.33;
3+
4+
import "./yul/YulTestBase.sol";
5+
6+
/**
7+
* @title PropertyEffectCompositionSmokeTest
8+
* @notice Auto-generated baseline property stubs from `verity_contract` declarations.
9+
* @dev Source: Contracts/Smoke.lean
10+
*/
11+
contract PropertyEffectCompositionSmokeTest is YulTestBase {
12+
address target;
13+
address alice = address(0x1111);
14+
15+
function setUp() public {
16+
target = deployYul("EffectCompositionSmoke");
17+
require(target != address(0), "Deploy failed");
18+
}
19+
20+
// Property 1: deposit has no unexpected revert
21+
function testAuto_Deposit_NoUnexpectedRevert() public {
22+
vm.prank(alice);
23+
(bool ok,) = target.call(abi.encodeWithSignature("deposit(uint256)", uint256(1)));
24+
require(ok, "deposit reverted unexpectedly");
25+
}
26+
}

0 commit comments

Comments
 (0)