-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathValidation.lean
More file actions
1426 lines (1292 loc) · 73 KB
/
Copy pathValidation.lean
File metadata and controls
1426 lines (1292 loc) · 73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/-
Compiler.CompilationModel.Validation: Validation walkers and shape checks
-/
import Compiler.CompilationModel.Types
import Compiler.CompilationModel.AbiHelpers
import Compiler.CompilationModel.AbiTypeLayout
import Compiler.CompilationModel.DynamicData
import Compiler.CompilationModel.EcmAxiomCollection
import Compiler.CompilationModel.EventAbiHelpers
import Compiler.CompilationModel.InternalNaming
import Compiler.CompilationModel.IssueRefs
import Compiler.CompilationModel.LayoutValidation
import Compiler.CompilationModel.LogicalPurity
import Compiler.CompilationModel.MappingWrites
import Compiler.CompilationModel.ScopeValidation
import Compiler.CompilationModel.TrustSurface
import Compiler.CompilationModel.UsageAnalysis
import Compiler.CompilationModel.ValidationCalls
import Compiler.CompilationModel.ValidationEvents
import Compiler.CompilationModel.ValidationHelpers
import Compiler.CompilationModel.ValidationInterop
import Compiler.CompilationModel.SelectorInteropHelpers
import Compiler.CompilationModel.ExpressionCompile
namespace Compiler.CompilationModel
open Compiler
open Compiler.Yul
private def firstDuplicateString : List String → Option String
| [] => none
| name :: rest =>
if rest.contains name then some name else firstDuplicateString rest
private def missingDeclaredNames (actual declared : List String) : List String :=
actual.foldl
(fun acc name =>
if declared.contains name || acc.contains name then acc else acc ++ [name])
[]
private def validateUnsafeYulDeclaredScopeEffects (fragment : UnsafeYulFragment) :
Except String Unit := do
let actual := yulStmtListScopeEffects fragment.stmts
let missingBinds := missingDeclaredNames actual.bindNames fragment.scopeEffects.bindNames
if !missingBinds.isEmpty then
throw s!"Compilation error: unsafe Yul fragment '{fragment.label}' under-declares bound local(s): {String.intercalate ", " missingBinds}"
let missingAssigns := missingDeclaredNames actual.assignNames fragment.scopeEffects.assignNames
if !missingAssigns.isEmpty then
throw s!"Compilation error: unsafe Yul fragment '{fragment.label}' under-declares assigned local(s): {String.intercalate ", " missingAssigns}"
if !actual.storageWrites.isEmpty && fragment.scopeEffects.storageWrites.isEmpty then
throw s!"Compilation error: unsafe Yul fragment '{fragment.label}' under-declares storage effects for raw Yul storage write(s)."
private def adtPayloadParamNames (params : List Param) : List String :=
params.flatMap fun param =>
match param.ty with
| ParamType.adt _ maxFields =>
(List.range maxFields).map fun idx => s!"{param.name}_f{idx}"
| _ => []
private def validateAdtPayloadParamNameCollisions
(context : String) (params : List Param) (body : List Stmt) : Except String Unit := do
let generated := adtPayloadParamNames params
match firstDuplicateString generated with
| some name =>
throw s!"Compilation error: {context} has ADT parameters whose generated payload local '{name}' collides. Rename the ADT parameters so generated '<param>_f<i>' locals are unique."
| none => pure ()
let userNames := params.map (·.name) ++ collectStmtListBindNames body
match generated.find? (fun name => userNames.contains name) with
| some name =>
throw s!"Compilation error: {context} reserves generated ADT payload local '{name}'. Rename the parameter or local binding that conflicts with generated '<param>_f<i>' locals."
| none => pure ()
private def immutableNames (immutables : List ImmutableSpec) : List String :=
immutables.map (·.name)
private def validateModelImmutableExprNameNode (names : List String)
(context : String) : Expr → Except String Unit
| Expr.immutable name =>
if names.contains name then
pure ()
else
throw s!"Compilation error: {context} references unknown immutable '{name}'"
| _ => pure ()
private def validateModelImmutableStmtNode (names : List String)
(context : String) : Stmt → Except String Unit
| stmt => do
match stmt with
| Stmt.setImmutable name _ =>
if names.contains name then
pure ()
else
throw s!"Compilation error: {context} sets unknown immutable '{name}'"
| _ => pure ()
for expr in stmt.directMetadata.subexpressions do
expr.checkRec (validateModelImmutableExprNameNode names context)
private def validateSetImmutableRuntimeGuardNode (fnName : String) : Stmt → Except String Unit
| Stmt.setImmutable name _ =>
throw s!"Compilation error: function '{fnName}' uses Stmt.setImmutable for immutable '{name}' outside constructor scope"
| _ => pure ()
def validateSetImmutableRuntimeGuard (fn : FunctionSpec) : Except String Unit :=
Stmt.checkRecList (validateSetImmutableRuntimeGuardNode fn.name) fn.body
def validateImmutableNamesInFunction (immutables : List ImmutableSpec)
(fn : FunctionSpec) : Except String Unit :=
Stmt.checkRecList (validateModelImmutableStmtNode (immutableNames immutables) s!"function '{fn.name}'") fn.body
def validateImmutableNamesInConstructor (immutables : List ImmutableSpec)
(ctor : Option ConstructorSpec) : Except String Unit := do
let names := immutableNames immutables
match ctor with
| none => pure ()
| some spec =>
for imm in immutables do
imm.init.checkRec (validateModelImmutableExprNameNode names s!"immutable '{imm.name}' initializer")
Stmt.checkRecList (validateModelImmutableStmtNode names "constructor") spec.body
/-- Each declared immutable must be assigned via `Stmt.setImmutable` in the
constructor body; deploy code only emits `setimmutable` from those
statements (never from `ImmutableSpec.init`), so an unset immutable would
read uninitialized bytecode at runtime. -/
def validateImmutableInitialization (immutables : List ImmutableSpec)
(ctor : Option ConstructorSpec) : Except String Unit := do
match immutables with
| [] => pure ()
| _ =>
match ctor with
| none =>
throw "Compilation error: contract declares immutables but has no constructor to initialize them"
| some spec =>
for imm in immutables do
let isSet := Stmt.foldBoolList
(fun s => match s with
| Stmt.setImmutable name _ => name == imm.name
| _ => false) spec.body
if isSet then
pure ()
else
throw s!"Compilation error: immutable '{imm.name}' is declared but never initialized in the constructor"
def isStorageWordArrayParam : ParamType → Bool
| ty => isWordArrayParam ty
/-- Node-local check for statement parameter references; nested statement
bodies are reached via the canonical `Stmt.forDeepM`. -/
def validateStmtParamReferencesNode (fnName : String) (params : List Param) :
Stmt → Except String Unit
| Stmt.returnArray name =>
match findParamType params name with
| some ty =>
if isWordArrayParam ty then
pure ()
else
throw s!"Compilation error: function '{fnName}' returnArray '{name}' requires an array parameter with single-word static elements, got {repr ty}"
| none =>
pure ()
| Stmt.returnBytes name =>
match findParamType params name with
| some ParamType.bytes | some ParamType.string => pure ()
| some ty =>
throw s!"Compilation error: function '{fnName}' returnBytes '{name}' requires bytes/string parameter, got {repr ty}"
| none =>
throw s!"Compilation error: function '{fnName}' returnBytes references unknown parameter '{name}'"
| Stmt.returnStorageWords name =>
match findParamType params name with
| some ty =>
if isStorageWordArrayParam ty then
pure ()
else
throw s!"Compilation error: function '{fnName}' returnStorageWords '{name}' requires an array parameter with single-word static elements, got {repr ty}"
| none =>
throw s!"Compilation error: function '{fnName}' returnStorageWords references unknown parameter '{name}'"
| Stmt.returnCodeData _pointer =>
-- `returnCodeData` may return a pointer computed into a local; the scope-aware
-- validator checks the expression once local bindings are available.
pure ()
| _ => pure ()
def validateStmtParamReferences (fnName : String) (params : List Param)
(stmt : Stmt) : Except String Unit :=
stmt.checkRec (validateStmtParamReferencesNode fnName params)
def validateStmtParamReferencesInList (fnName : String) (params : List Param)
(stmts : List Stmt) : Except String Unit :=
Stmt.checkRecList (validateStmtParamReferencesNode fnName params) stmts
def validateStmtParamReferencesInBranches (fnName : String) (params : List Param)
(branches : List (String × List String × List Stmt)) : Except String Unit :=
Stmt.checkRecBranches (validateStmtParamReferencesNode fnName params) branches
def validateStmtParamReferences_viaFold (fnName : String) (params : List Param)
(stmt : Stmt) : Except String Unit :=
Stmt.checkRec (validateStmtParamReferencesNode fnName params) stmt
def validateStmtParamReferencesInList_viaFold (fnName : String) (params : List Param)
(stmts : List Stmt) : Except String Unit :=
Stmt.checkRecList (validateStmtParamReferencesNode fnName params) stmts
def validateStmtParamReferencesInBranches_viaFold (fnName : String) (params : List Param)
(branches : List (String × List String × List Stmt)) : Except String Unit :=
Stmt.checkRecBranches (validateStmtParamReferencesNode fnName params) branches
theorem validateStmtParamReferences_eq_viaFold
(fnName : String) (params : List Param) (stmt : Stmt) :
validateStmtParamReferences fnName params stmt =
validateStmtParamReferences_viaFold fnName params stmt := by
rfl
theorem validateStmtParamReferencesInList_eq_viaFold
(fnName : String) (params : List Param) (stmts : List Stmt) :
validateStmtParamReferencesInList fnName params stmts =
validateStmtParamReferencesInList_viaFold fnName params stmts := by
rfl
theorem validateStmtParamReferencesInBranches_eq_viaFold
(fnName : String) (params : List Param)
(branches : List (String × List String × List Stmt)) :
validateStmtParamReferencesInBranches fnName params branches =
validateStmtParamReferencesInBranches_viaFold fnName params branches := by
rfl
/-- Node-local return-shape check; nested statement bodies are reached via
the canonical `Stmt.forDeepM`. -/
def validateReturnShapesNode (fnName : String) (params : List Param)
(expectedReturns : List ParamType) (isInternal : Bool) : Stmt → Except String Unit
| Stmt.return _ =>
if isInternal then
match expectedReturns with
| [_] => pure ()
| [] =>
throw s!"Compilation error: function '{fnName}' uses Stmt.return but declares no return values"
| _ =>
throw s!"Compilation error: function '{fnName}' uses Stmt.return but declares multiple return values; use Stmt.returnValues"
else if expectedReturns.length > 1 then
throw s!"Compilation error: function '{fnName}' uses Stmt.return but declares multiple return values; use Stmt.returnValues"
else
pure ()
| Stmt.returnValues values =>
if expectedReturns.isEmpty then
throw s!"Compilation error: function '{fnName}' uses Stmt.returnValues but declares no return values"
else if values.length != expectedReturns.length then
throw s!"Compilation error: function '{fnName}' returnValues count mismatch: expected {expectedReturns.length}, got {values.length}"
else
pure ()
| Stmt.returnArray name =>
match findParamType params name with
| some ty =>
if !isWordArrayParam ty then
throw s!"Compilation error: function '{fnName}' uses Stmt.returnArray with parameter '{name}' of type {repr ty}; only arrays with single-word static elements are currently supported"
else if expectedReturns == [ty] then
pure ()
else
throw s!"Compilation error: function '{fnName}' uses Stmt.returnArray to return parameter '{name}' of type {repr ty}, but declared returns are {repr expectedReturns}"
| none =>
match expectedReturns with
| [ty] =>
if isWordArrayParam ty then
pure ()
else
throw s!"Compilation error: function '{fnName}' uses Stmt.returnArray with memory array '{name}', but declared return type {repr ty} is not a supported word array"
| _ =>
throw s!"Compilation error: function '{fnName}' returnArray references unknown parameter '{name}'"
| Stmt.returnBytes name =>
if isInternal then
throw s!"Compilation error: internal function '{fnName}' cannot use Stmt.returnBytes; only static returns via Stmt.return/Stmt.returnValues are supported ({issue625Ref})."
else if expectedReturns == [ParamType.bytes] || expectedReturns == [ParamType.string] then
match findParamType params name with
| some ty =>
if expectedReturns == [ty] then
pure ()
else
throw s!"Compilation error: function '{fnName}' uses Stmt.returnBytes to return parameter '{name}' of type {repr ty}, but declared returns are {repr expectedReturns}"
| none =>
throw s!"Compilation error: function '{fnName}' returnBytes references unknown parameter '{name}'"
else
throw s!"Compilation error: function '{fnName}' uses Stmt.returnBytes but declared returns are {repr expectedReturns}"
| Stmt.returnStorageWords _ =>
if isInternal then
throw s!"Compilation error: internal function '{fnName}' cannot use Stmt.returnStorageWords; only static returns via Stmt.return/Stmt.returnValues are supported ({issue625Ref})."
else if expectedReturns == [ParamType.array ParamType.uint256] then
pure ()
else
throw s!"Compilation error: function '{fnName}' uses Stmt.returnStorageWords but declared returns are {repr expectedReturns}"
| Stmt.returnCodeData _ =>
if isInternal then
throw s!"Compilation error: internal function '{fnName}' cannot use Stmt.returnCodeData; only static returns via Stmt.return/Stmt.returnValues are supported ({issue625Ref})."
else if expectedReturns.isEmpty then
throw s!"Compilation error: function '{fnName}' uses Stmt.returnCodeData but declares no return values"
else
-- #1982: CodeData (SSTORE2-style) returns remain static-only for now.
-- Full lifting of the static gate (dynamic payloads, broader return shapes)
-- is tracked under the 1982 dynamic/composite ABI work.
pure ()
| _ => pure ()
def validateReturnShapesInStmt (fnName : String) (params : List Param)
(expectedReturns : List ParamType) (isInternal : Bool) (stmt : Stmt) :
Except String Unit :=
stmt.checkRec (validateReturnShapesNode fnName params expectedReturns isInternal)
def validateReturnShapesInStmtList (fnName : String)
(params : List Param) (expectedReturns : List ParamType) (isInternal : Bool)
(stmts : List Stmt) : Except String Unit :=
Stmt.checkRecList (validateReturnShapesNode fnName params expectedReturns isInternal) stmts
def validateReturnShapesInBranches (fnName : String)
(params : List Param) (expectedReturns : List ParamType) (isInternal : Bool)
(branches : List (String × List String × List Stmt)) : Except String Unit :=
Stmt.checkRecBranches
(validateReturnShapesNode fnName params expectedReturns isInternal) branches
def validateReturnShapesInStmt_viaFold (fnName : String) (params : List Param)
(expectedReturns : List ParamType) (isInternal : Bool) (stmt : Stmt) :
Except String Unit :=
Stmt.checkRec (validateReturnShapesNode fnName params expectedReturns isInternal) stmt
def validateReturnShapesInStmtList_viaFold (fnName : String)
(params : List Param) (expectedReturns : List ParamType) (isInternal : Bool)
(stmts : List Stmt) : Except String Unit :=
Stmt.checkRecList (validateReturnShapesNode fnName params expectedReturns isInternal) stmts
def validateReturnShapesInBranches_viaFold (fnName : String)
(params : List Param) (expectedReturns : List ParamType) (isInternal : Bool)
(branches : List (String × List String × List Stmt)) : Except String Unit :=
Stmt.checkRecBranches
(validateReturnShapesNode fnName params expectedReturns isInternal) branches
theorem validateReturnShapesInStmt_eq_viaFold
(fnName : String) (params : List Param) (expectedReturns : List ParamType)
(isInternal : Bool) (stmt : Stmt) :
validateReturnShapesInStmt fnName params expectedReturns isInternal stmt =
validateReturnShapesInStmt_viaFold fnName params expectedReturns isInternal stmt := by
rfl
theorem validateReturnShapesInStmtList_eq_viaFold
(fnName : String) (params : List Param) (expectedReturns : List ParamType)
(isInternal : Bool) (stmts : List Stmt) :
validateReturnShapesInStmtList fnName params expectedReturns isInternal stmts =
validateReturnShapesInStmtList_viaFold fnName params expectedReturns isInternal stmts := by
rfl
theorem validateReturnShapesInBranches_eq_viaFold
(fnName : String) (params : List Param) (expectedReturns : List ParamType)
(isInternal : Bool) (branches : List (String × List String × List Stmt)) :
validateReturnShapesInBranches fnName params expectedReturns isInternal branches =
validateReturnShapesInBranches_viaFold
fnName params expectedReturns isInternal branches := by
rfl
private def stmtListAlwaysReturnsOrReverts (stmts : List Stmt) : Bool :=
ControlFlowSummary.alwaysReturnsOrReverts (Stmt.controlFlowList stmts)
/-- Node-local classifier: does this expression itself read state or the
environment? `externalCall` to the builtin exponentiation helper is pure.
Lifted with the canonical `Expr.anyDeep`. -/
def exprReadsStateOrEnvNode : Expr → Bool
| Expr.storage _ | Expr.storageAddr _
| Expr.mapping _ _ | Expr.mappingWord _ _ _ | Expr.mappingPackedWord _ _ _ _
| Expr.mapping2 _ _ _ | Expr.mapping2Word _ _ _ _
| Expr.mappingUint _ _ | Expr.mappingChain _ _
| Expr.structMember _ _ _ | Expr.structMember2 _ _ _ _
| Expr.caller | Expr.contractAddress | Expr.txOrigin | Expr.chainid
| Expr.extcodesize _ | Expr.msgValue | Expr.selfBalance
| Expr.blockTimestamp | Expr.blockNumber | Expr.blobbasefee
| Expr.calldatasize | Expr.calldataload _ | Expr.tload _
| Expr.call _ _ _ _ _ _ _ | Expr.staticcall _ _ _ _ _ _
| Expr.delegatecall _ _ _ _ _ _
| Expr.returndataSize | Expr.returndataOptionalBoolAt _
| Expr.internalCall _ _
| Expr.storageArrayLength _ | Expr.storageArrayElement _ _
| Expr.adtTag _ _ | Expr.adtField _ _ _ _ _ => true
| Expr.externalCall name _ => name != builtinExpName
| _ => false
def exprReadsStateOrEnv (e : Expr) : Bool :=
e.anyDeep exprReadsStateOrEnvNode
/-- Node-local classifier: does this expression itself write state? Lifted
with the canonical `Expr.anyDeep` (so `staticcall` operands etc. are still
scanned, exactly like the old walk). -/
def exprWritesStateNode : Expr → Bool
| Expr.call _ _ _ _ _ _ _ | Expr.delegatecall _ _ _ _ _ _ => true
| Expr.externalCall name _ => name != builtinExpName
| Expr.internalCall _ _ => true
| _ => false
def exprWritesState (e : Expr) : Bool :=
e.anyDeep exprWritesStateNode
def exprListWritesState (es : List Expr) : Bool :=
es.any exprWritesState
/-- Node-local statement classifier for state writes; nested statement bodies
are reached via the canonical `Stmt.anyDeep`. -/
def stmtWritesStateNode : Stmt → Bool
| Stmt.letVar _ value | Stmt.assignVar _ value =>
exprWritesState value
| Stmt.setStorage _ _ | Stmt.setStorageAddr _ _ | Stmt.setStorageWord _ _ _
| Stmt.storageArrayPush _ _ | Stmt.storageArrayPop _ | Stmt.setStorageArrayElement _ _ _
| Stmt.setMapping _ _ _ | Stmt.setMappingWord _ _ _ _ | Stmt.setMappingPackedWord _ _ _ _ _ | Stmt.setMappingUint _ _ _
| Stmt.setMappingChain _ _ _
| Stmt.setMapping2 _ _ _ _ | Stmt.setMapping2Word _ _ _ _ _
| Stmt.setStructMember _ _ _ _ | Stmt.setStructMember2 _ _ _ _ _ => true
| Stmt.require cond _ =>
exprWritesState cond
| Stmt.requireError cond _ args =>
exprWritesState cond || exprListWritesState args
| Stmt.revertError _ args =>
exprListWritesState args
| Stmt.return value =>
exprWritesState value
| Stmt.returnValues values =>
exprListWritesState values
| Stmt.returnCodeData pointer =>
exprWritesState pointer
| Stmt.unsafeYul fragment =>
!fragment.scopeEffects.storageWrites.isEmpty || fragment.mechanics.contains .tstore
| Stmt.mstore offset value =>
exprWritesState offset || exprWritesState value
| Stmt.tstore _ _ =>
true
| Stmt.calldatacopy destOffset sourceOffset size
| Stmt.returndataCopy destOffset sourceOffset size =>
exprWritesState destOffset || exprWritesState sourceOffset || exprWritesState size
| Stmt.ite cond _ _ =>
exprWritesState cond
| Stmt.forEach _ count _ =>
exprWritesState count
| Stmt.emit _ _ | Stmt.rawLog _ _ _
| Stmt.internalCall _ _ | Stmt.internalCallAssign _ _ _
| Stmt.externalCallBind _ _ _ | Stmt.tryExternalCallBind _ _ _ _ => true
| Stmt.ecm mod args =>
mod.writesState || exprListWritesState args
| Stmt.matchAdt _ scrutinee _ =>
exprWritesState scrutinee
| _ => false
def stmtWritesState (s : Stmt) : Bool :=
s.anyDeep stmtWritesStateNode
def stmtListWritesState (stmts : List Stmt) : Bool :=
Stmt.anyDeepList stmtWritesStateNode stmts
def matchBranchesWriteState (branches : List (String × List String × List Stmt)) : Bool :=
branches.any fun (_, _, body) => stmtListWritesState body
/-- Collect the set of storage field names written by a statement.
The per-statement contribution is exactly the canonical
`directMetadata.scopeEffects.storageWrites` (which also covers raw
`unsafeYul` fragments); nested statement bodies are reached via
`Stmt.childLists`. Used by `modifies(...)` validation (#1729, Axis 3 Step 1b). -/
def stmtWrittenFields (s : Stmt) : List String :=
s.directMetadata.scopeEffects.storageWrites ++
(Stmt.childLists s).attach.flatMap (fun ⟨l, hl⟩ =>
l.attach.flatMap (fun ⟨c, hc⟩ =>
have := Stmt.childLists_sizeOf_lt s l hl c hc
stmtWrittenFields c))
termination_by sizeOf s
decreasing_by exact Nat.lt_trans this.1 this.2
def stmtListWrittenFields (stmts : List Stmt) : List String :=
stmts.flatMap stmtWrittenFields
def matchBranchesWrittenFields (branches : List (String × List String × List Stmt)) : List String :=
branches.flatMap fun (_, _, body) => stmtListWrittenFields body
/-- Detect expression-position internal helper calls whose callee write set is
not visible to single-function `modifies(...)` validation. External-call
forms (`externalCall`/`call`/`staticcall`/`delegatecall`) deliberately do
NOT recurse into their operands, exactly like the old walk; all other
constructors recurse via the canonical `Expr.children`. -/
def exprHasUntrackableWrites : Expr → Bool
| Expr.internalCall _ _ => true
| Expr.externalCall _ _ | Expr.call _ _ _ _ _ _ _
| Expr.staticcall _ _ _ _ _ _ | Expr.delegatecall _ _ _ _ _ _ => false
| e =>
(Expr.children e).attach.any (fun ⟨c, hc⟩ =>
have := Expr.children_sizeOf_lt e c hc
exprHasUntrackableWrites c)
termination_by e => sizeOf e
def exprListHasUntrackableWrites (es : List Expr) : Bool :=
es.any exprHasUntrackableWrites
/-- Node-local statement classifier for untrackable writes; nested statement
bodies are reached via the canonical `Stmt.anyDeep`. External calls
(`externalCallBind`, `tryExternalCallBind`, `ecm`) target other contracts
and cannot directly modify the current contract's storage fields, so they
are safe for `modifies()` (as in the old walk). -/
def stmtHasUntrackableWritesNode : Stmt → Bool
| Stmt.internalCall _ _ | Stmt.internalCallAssign _ _ _ => true
| Stmt.letVar _ value | Stmt.assignVar _ value =>
exprHasUntrackableWrites value
| Stmt.setStorage _ value | Stmt.setStorageAddr _ value
| Stmt.require value _ =>
exprHasUntrackableWrites value
| Stmt.setStorageWord _ wordOffset value =>
-- Nonzero wordOffset writes to a sibling field that `stmtWrittenFields`
-- cannot track (it returns only the base field), so flag as untrackable
-- to keep `modifies(...)` validation sound.
wordOffset != 0 || exprHasUntrackableWrites value
| Stmt.requireError cond _ args =>
exprHasUntrackableWrites cond || args.any exprHasUntrackableWrites
| Stmt.revertError _ args | Stmt.returnValues args | Stmt.emit _ args =>
args.any exprHasUntrackableWrites
| Stmt.return value | Stmt.storageArrayPush _ value =>
exprHasUntrackableWrites value
| Stmt.setStorageArrayElement _ index value =>
exprHasUntrackableWrites index || exprHasUntrackableWrites value
| Stmt.setMapping _ key value | Stmt.setMappingUint _ key value =>
exprHasUntrackableWrites key || exprHasUntrackableWrites value
| Stmt.setMappingWord _ key _ value | Stmt.setMappingPackedWord _ key _ _ value =>
exprHasUntrackableWrites key || exprHasUntrackableWrites value
| Stmt.setMappingChain _ keys value =>
keys.any exprHasUntrackableWrites || exprHasUntrackableWrites value
| Stmt.setMapping2 _ key1 key2 value | Stmt.setMapping2Word _ key1 key2 _ value
| Stmt.setStructMember2 _ key1 key2 _ value =>
exprHasUntrackableWrites key1 || exprHasUntrackableWrites key2 || exprHasUntrackableWrites value
| Stmt.setStructMember _ key _ value =>
exprHasUntrackableWrites key || exprHasUntrackableWrites value
| Stmt.rawLog topics dataOffset dataSize =>
topics.any exprHasUntrackableWrites || exprHasUntrackableWrites dataOffset || exprHasUntrackableWrites dataSize
| Stmt.mstore offset value | Stmt.tstore offset value =>
exprHasUntrackableWrites offset || exprHasUntrackableWrites value
| Stmt.calldatacopy destOffset sourceOffset size | Stmt.returndataCopy destOffset sourceOffset size =>
exprHasUntrackableWrites destOffset || exprHasUntrackableWrites sourceOffset || exprHasUntrackableWrites size
| Stmt.ite cond _ _ =>
exprHasUntrackableWrites cond
| Stmt.forEach _ count _ =>
exprHasUntrackableWrites count
| Stmt.unsafeYul fragment =>
-- Raw Yul storage writes target computed slots that cannot be tied back to
-- declared storage fields, so any storage-writing fragment is untrackable.
!fragment.scopeEffects.storageWrites.isEmpty || fragment.mechanics.contains .storageWrite
| Stmt.matchAdt _ scrutinee _ =>
exprHasUntrackableWrites scrutinee
| _ => false
/-- Check whether a statement may write to storage fields that `stmtWrittenFields`
cannot track. Used by `modifies(...)` validation to conservatively reject
annotations when write-set tracking is incomplete. -/
def stmtHasUntrackableWrites (s : Stmt) : Bool :=
s.anyDeep stmtHasUntrackableWritesNode
def stmtListHasUntrackableWrites (stmts : List Stmt) : Bool :=
Stmt.anyDeepList stmtHasUntrackableWritesNode stmts
def matchBranchesHasUntrackableWrites (branches : List (String × List String × List Stmt)) : Bool :=
branches.any fun (_, _, body) => stmtListHasUntrackableWrites body
/-- Node-local classifier: is this expression itself an external call (call,
staticcall, delegatecall, or non-builtin externalCall)? Lifted with the
canonical `Expr.anyDeep`. Used by `no_external_calls` validation
(#1729, Axis 3 Step 1c). -/
def exprContainsExternalCallNode : Expr → Bool
| Expr.call _ _ _ _ _ _ _ | Expr.staticcall _ _ _ _ _ _
| Expr.delegatecall _ _ _ _ _ _ => true
| Expr.externalCall name _ => name != builtinExpName
| _ => false
def exprContainsExternalCall (e : Expr) : Bool :=
e.anyDeep exprContainsExternalCallNode
def exprListContainsExternalCall (es : List Expr) : Bool :=
es.any exprContainsExternalCall
/-- Conservative variant of `exprContainsExternalCallNode` for annotations such
as `no_external_calls`, where an internal helper expression may itself
perform an external interaction. CEI uses `exprContainsExternalCall`
instead so that local helper reads do not become false interaction
barriers. -/
def exprMayContainExternalCallNode : Expr → Bool
| Expr.internalCall _ _ => true
| e => exprContainsExternalCallNode e
def exprMayContainExternalCall (e : Expr) : Bool :=
e.anyDeep exprMayContainExternalCallNode
def exprListMayContainExternalCall (es : List Expr) : Bool :=
es.any exprMayContainExternalCall
/-- Node-local statement classifier for external calls; nested statement
bodies are reached via the canonical `Stmt.anyDeep`.
Used by `no_external_calls` validation (#1729, Axis 3 Step 1c). -/
def stmtContainsExternalCallNode : Stmt → Bool
| Stmt.externalCallBind _ _ _ | Stmt.tryExternalCallBind _ _ _ _ => true
| Stmt.ecm _ _ => true
| Stmt.letVar _ value | Stmt.assignVar _ value =>
exprContainsExternalCall value
| Stmt.setStorage _ value | Stmt.setStorageAddr _ value | Stmt.setStorageWord _ _ value
| Stmt.require value _ =>
exprContainsExternalCall value
| Stmt.requireError cond _ args =>
exprContainsExternalCall cond || args.any exprContainsExternalCall
| Stmt.revertError _ args =>
args.any exprContainsExternalCall
| Stmt.return value =>
exprContainsExternalCall value
| Stmt.returnValues values =>
values.any exprContainsExternalCall
| Stmt.storageArrayPush _ value =>
exprContainsExternalCall value
| Stmt.setStorageArrayElement _ index value =>
exprContainsExternalCall index || exprContainsExternalCall value
| Stmt.setMapping _ key value | Stmt.setMappingUint _ key value =>
exprContainsExternalCall key || exprContainsExternalCall value
| Stmt.setMappingWord _ key _ value =>
exprContainsExternalCall key || exprContainsExternalCall value
| Stmt.setMappingPackedWord _ key _ _ value =>
exprContainsExternalCall key || exprContainsExternalCall value
| Stmt.setMappingChain _ keys value =>
keys.any exprContainsExternalCall || exprContainsExternalCall value
| Stmt.setMapping2 _ key1 key2 value =>
exprContainsExternalCall key1 || exprContainsExternalCall key2 || exprContainsExternalCall value
| Stmt.setMapping2Word _ key1 key2 _ value =>
exprContainsExternalCall key1 || exprContainsExternalCall key2 || exprContainsExternalCall value
| Stmt.setStructMember _ key _ value =>
exprContainsExternalCall key || exprContainsExternalCall value
| Stmt.setStructMember2 _ key1 key2 _ value =>
exprContainsExternalCall key1 || exprContainsExternalCall key2 || exprContainsExternalCall value
| Stmt.emit _ args =>
args.any exprContainsExternalCall
| Stmt.rawLog topics dataOffset dataSize =>
topics.any exprContainsExternalCall || exprContainsExternalCall dataOffset || exprContainsExternalCall dataSize
| Stmt.tstore offset value | Stmt.mstore offset value =>
exprContainsExternalCall offset || exprContainsExternalCall value
| Stmt.calldatacopy destOffset sourceOffset size =>
exprContainsExternalCall destOffset || exprContainsExternalCall sourceOffset || exprContainsExternalCall size
| Stmt.returndataCopy destOffset sourceOffset size =>
exprContainsExternalCall destOffset || exprContainsExternalCall sourceOffset || exprContainsExternalCall size
| Stmt.ite cond _ _ =>
exprContainsExternalCall cond
| Stmt.forEach _ count _ =>
exprContainsExternalCall count
| Stmt.matchAdt _ scrutinee _ =>
exprContainsExternalCall scrutinee
| Stmt.internalCall _ args | Stmt.internalCallAssign _ _ args =>
args.any exprContainsExternalCall
| Stmt.unsafeYul fragment =>
fragment.mechanics.contains .call ||
fragment.mechanics.contains .staticcall ||
fragment.mechanics.contains .delegatecall ||
yulStmtListContainsExternalCall fragment.stmts
| _ => false
def stmtContainsExternalCall (s : Stmt) : Bool :=
s.anyDeep stmtContainsExternalCallNode
def stmtListContainsExternalCall (stmts : List Stmt) : Bool :=
Stmt.anyDeepList stmtContainsExternalCallNode stmts
def matchBranchesContainExternalCall (branches : List (String × List String × List Stmt)) : Bool :=
branches.any fun (_, _, body) => stmtListContainsExternalCall body
/-- Conservative variant of `stmtContainsExternalCallNode` for
`no_external_calls` validation. Returns `true` for internal calls because
callee bodies may contain external calls that are not visible at
single-function validation scope. -/
def stmtMayContainExternalCallNode : Stmt → Bool
| Stmt.internalCall _ _ | Stmt.internalCallAssign _ _ _ => true
| Stmt.ite cond _ _ =>
exprMayContainExternalCall cond
| Stmt.forEach _ count _ =>
exprMayContainExternalCall count
| Stmt.matchAdt _ scrutinee _ =>
exprMayContainExternalCall scrutinee
| Stmt.letVar _ value | Stmt.assignVar _ value =>
exprMayContainExternalCall value
| Stmt.setStorage _ value | Stmt.setStorageAddr _ value | Stmt.setStorageWord _ _ value
| Stmt.require value _ =>
exprMayContainExternalCall value
| Stmt.requireError cond _ args =>
exprMayContainExternalCall cond || args.any exprMayContainExternalCall
| Stmt.revertError _ args =>
args.any exprMayContainExternalCall
| Stmt.return value =>
exprMayContainExternalCall value
| Stmt.returnValues values =>
values.any exprMayContainExternalCall
| Stmt.storageArrayPush _ value =>
exprMayContainExternalCall value
| Stmt.setStorageArrayElement _ index value =>
exprMayContainExternalCall index || exprMayContainExternalCall value
| Stmt.setMapping _ key value | Stmt.setMappingUint _ key value =>
exprMayContainExternalCall key || exprMayContainExternalCall value
| Stmt.setMappingWord _ key _ value =>
exprMayContainExternalCall key || exprMayContainExternalCall value
| Stmt.setMappingPackedWord _ key _ _ value =>
exprMayContainExternalCall key || exprMayContainExternalCall value
| Stmt.setMappingChain _ keys value =>
keys.any exprMayContainExternalCall || exprMayContainExternalCall value
| Stmt.setMapping2 _ key1 key2 value =>
exprMayContainExternalCall key1 || exprMayContainExternalCall key2 || exprMayContainExternalCall value
| Stmt.setMapping2Word _ key1 key2 _ value =>
exprMayContainExternalCall key1 || exprMayContainExternalCall key2 || exprMayContainExternalCall value
| Stmt.setStructMember _ key _ value =>
exprMayContainExternalCall key || exprMayContainExternalCall value
| Stmt.setStructMember2 _ key1 key2 _ value =>
exprMayContainExternalCall key1 || exprMayContainExternalCall key2 || exprMayContainExternalCall value
| Stmt.emit _ args =>
args.any exprMayContainExternalCall
| Stmt.rawLog topics dataOffset dataSize =>
topics.any exprMayContainExternalCall || exprMayContainExternalCall dataOffset || exprMayContainExternalCall dataSize
| Stmt.tstore offset value | Stmt.mstore offset value =>
exprMayContainExternalCall offset || exprMayContainExternalCall value
| Stmt.calldatacopy destOffset sourceOffset size =>
exprMayContainExternalCall destOffset || exprMayContainExternalCall sourceOffset || exprMayContainExternalCall size
| Stmt.returndataCopy destOffset sourceOffset size =>
exprMayContainExternalCall destOffset || exprMayContainExternalCall sourceOffset || exprMayContainExternalCall size
| s => stmtContainsExternalCallNode s
def stmtMayContainExternalCall (s : Stmt) : Bool :=
s.anyDeep stmtMayContainExternalCallNode
def stmtListMayContainExternalCall (stmts : List Stmt) : Bool :=
Stmt.anyDeepList stmtMayContainExternalCallNode stmts
def matchBranchesMayContainExternalCall (branches : List (String × List String × List Stmt)) : Bool :=
branches.any fun (_, _, body) => stmtListMayContainExternalCall body
/-- Node-local statement classifier for state/environment reads; nested
statement bodies are reached via the canonical `Stmt.anyDeep`. -/
def stmtReadsStateOrEnvNode : Stmt → Bool
| Stmt.letVar _ value | Stmt.assignVar _ value | Stmt.setStorage _ value | Stmt.setStorageAddr _ value
| Stmt.setImmutable _ value | Stmt.setStorageWord _ _ value |
Stmt.return value | Stmt.require value _ =>
exprReadsStateOrEnv value
| Stmt.storageArrayPush _ _ | Stmt.setStorageArrayElement _ _ _ | Stmt.storageArrayPop _ =>
true
| Stmt.requireError cond _ args =>
exprReadsStateOrEnv cond || args.any exprReadsStateOrEnv
| Stmt.revertError _ args | Stmt.emit _ args | Stmt.returnValues args =>
args.any exprReadsStateOrEnv
| Stmt.returnArray _ | Stmt.returnBytes _ =>
false
| Stmt.returnStorageWords _ =>
true
| Stmt.returnCodeData _ =>
true
| Stmt.mstore offset value | Stmt.tstore offset value =>
exprReadsStateOrEnv offset || exprReadsStateOrEnv value
| Stmt.calldatacopy _ _ _ | Stmt.returndataCopy _ _ _ => true
| Stmt.revertReturndata =>
true
| Stmt.stop =>
false
| Stmt.setMapping _ _ _ | Stmt.setMappingWord _ _ _ _ | Stmt.setMappingPackedWord _ _ _ _ _ | Stmt.setMappingUint _ _ _
| Stmt.setMappingChain _ _ _
| Stmt.setMapping2 _ _ _ _ | Stmt.setMapping2Word _ _ _ _ _
| Stmt.setStructMember _ _ _ _ | Stmt.setStructMember2 _ _ _ _ _ => true
| Stmt.ite cond _ _ =>
exprReadsStateOrEnv cond
| Stmt.forEach _ count _ =>
exprReadsStateOrEnv count
| Stmt.unsafeBlock _ _ =>
false
| Stmt.rawLog topics dataOffset dataSize =>
topics.any exprReadsStateOrEnv || exprReadsStateOrEnv dataOffset || exprReadsStateOrEnv dataSize
| Stmt.internalCall _ _ | Stmt.internalCallAssign _ _ _
| Stmt.externalCallBind _ _ _ | Stmt.tryExternalCallBind _ _ _ _ => true
| Stmt.ecm mod args => mod.readsState || mod.writesState || args.any exprReadsStateOrEnv
| Stmt.matchAdt _ scrutinee _ =>
exprReadsStateOrEnv scrutinee
| Stmt.unsafeYul fragment =>
!fragment.mechanics.isEmpty || !fragment.scopeEffects.storageWrites.isEmpty
def stmtReadsStateOrEnv (s : Stmt) : Bool :=
s.anyDeep stmtReadsStateOrEnvNode
def stmtListReadsStateOrEnv (stmts : List Stmt) : Bool :=
Stmt.anyDeepList stmtReadsStateOrEnvNode stmts
def matchBranchesReadStateOrEnv (branches : List (String × List String × List Stmt)) : Bool :=
branches.any fun (_, _, body) => stmtListReadsStateOrEnv body
structure FunctionEffect where
writesState : Bool := false
readsStateOrEnv : Bool := false
deriving BEq, Repr
private def unknownFunctionEffect : FunctionEffect :=
{ writesState := true, readsStateOrEnv := true }
private def lookupFunctionEffect
(effects : List (String × FunctionEffect)) (name : String) : FunctionEffect :=
match effects.find? (fun entry => entry.fst == name) with
| some (_, effect) => effect
| none => unknownFunctionEffect
/-- Node-local classifier for state writes with inferred per-function
effects; lifted with the canonical `Expr.anyDeep`. -/
def exprWritesStateWithFunctionEffectsNode
(effects : List (String × FunctionEffect)) : Expr → Bool
| Expr.call _ _ _ _ _ _ _ | Expr.delegatecall _ _ _ _ _ _ => true
| Expr.externalCall name _ => name != builtinExpName
| Expr.internalCall name _ => (lookupFunctionEffect effects name).writesState
| _ => false
def exprWritesStateWithFunctionEffects
(effects : List (String × FunctionEffect)) (e : Expr) : Bool :=
e.anyDeep (exprWritesStateWithFunctionEffectsNode effects)
def exprListWritesStateWithFunctionEffects
(effects : List (String × FunctionEffect)) (es : List Expr) : Bool :=
es.any (exprWritesStateWithFunctionEffects effects)
/-- Node-local statement classifier for state writes with inferred
per-function effects; nested bodies via the canonical `Stmt.anyDeep`. -/
def stmtWritesStateWithFunctionEffectsNode
(effects : List (String × FunctionEffect)) : Stmt → Bool
| Stmt.letVar _ value | Stmt.assignVar _ value =>
exprWritesStateWithFunctionEffects effects value
| Stmt.setStorage _ _ | Stmt.setStorageAddr _ _ | Stmt.setStorageWord _ _ _
| Stmt.storageArrayPush _ _ | Stmt.storageArrayPop _ | Stmt.setStorageArrayElement _ _ _
| Stmt.setMapping _ _ _ | Stmt.setMappingWord _ _ _ _ | Stmt.setMappingPackedWord _ _ _ _ _ | Stmt.setMappingUint _ _ _
| Stmt.setMappingChain _ _ _
| Stmt.setMapping2 _ _ _ _ | Stmt.setMapping2Word _ _ _ _ _
| Stmt.setStructMember _ _ _ _ | Stmt.setStructMember2 _ _ _ _ _ => true
| Stmt.require cond _ =>
exprWritesStateWithFunctionEffects effects cond
| Stmt.requireError cond _ args =>
exprWritesStateWithFunctionEffects effects cond ||
exprListWritesStateWithFunctionEffects effects args
| Stmt.revertError _ args =>
exprListWritesStateWithFunctionEffects effects args
| Stmt.return value =>
exprWritesStateWithFunctionEffects effects value
| Stmt.returnValues values =>
exprListWritesStateWithFunctionEffects effects values
| Stmt.returnCodeData pointer =>
exprWritesStateWithFunctionEffects effects pointer
| Stmt.mstore offset value =>
exprWritesStateWithFunctionEffects effects offset ||
exprWritesStateWithFunctionEffects effects value
| Stmt.tstore _ _ =>
true
| Stmt.calldatacopy destOffset sourceOffset size
| Stmt.returndataCopy destOffset sourceOffset size =>
exprWritesStateWithFunctionEffects effects destOffset ||
exprWritesStateWithFunctionEffects effects sourceOffset ||
exprWritesStateWithFunctionEffects effects size
| Stmt.ite cond _ _ =>
exprWritesStateWithFunctionEffects effects cond
| Stmt.forEach _ count _ =>
exprWritesStateWithFunctionEffects effects count
| Stmt.emit _ _ | Stmt.rawLog _ _ _
| Stmt.externalCallBind _ _ _ | Stmt.tryExternalCallBind _ _ _ _ => true
| Stmt.internalCall name args | Stmt.internalCallAssign _ name args =>
(lookupFunctionEffect effects name).writesState ||
exprListWritesStateWithFunctionEffects effects args
| Stmt.ecm mod args =>
mod.writesState || exprListWritesStateWithFunctionEffects effects args
| Stmt.matchAdt _ scrutinee _ =>
exprWritesStateWithFunctionEffects effects scrutinee
| Stmt.unsafeYul fragment =>
!fragment.scopeEffects.storageWrites.isEmpty || fragment.mechanics.contains .tstore
| _ => false
def stmtWritesStateWithFunctionEffects
(effects : List (String × FunctionEffect)) (s : Stmt) : Bool :=
s.anyDeep (stmtWritesStateWithFunctionEffectsNode effects)
def stmtListWritesStateWithFunctionEffects
(effects : List (String × FunctionEffect)) (stmts : List Stmt) : Bool :=
Stmt.anyDeepList (stmtWritesStateWithFunctionEffectsNode effects) stmts
def matchBranchesWriteStateWithFunctionEffects
(effects : List (String × FunctionEffect))
(branches : List (String × List String × List Stmt)) : Bool :=
branches.any fun (_, _, body) => stmtListWritesStateWithFunctionEffects effects body
/-- Node-local classifier for state/environment reads with inferred
per-function effects; lifted with the canonical `Expr.anyDeep`. -/
def exprReadsStateOrEnvWithFunctionEffectsNode
(effects : List (String × FunctionEffect)) : Expr → Bool
| Expr.internalCall name _ => (lookupFunctionEffect effects name).readsStateOrEnv
| e => exprReadsStateOrEnvNode e
def exprReadsStateOrEnvWithFunctionEffects
(effects : List (String × FunctionEffect)) (e : Expr) : Bool :=
e.anyDeep (exprReadsStateOrEnvWithFunctionEffectsNode effects)
def exprListReadsStateOrEnvWithFunctionEffects
(effects : List (String × FunctionEffect)) (es : List Expr) : Bool :=
es.any (exprReadsStateOrEnvWithFunctionEffects effects)
/-- Node-local statement classifier for state/environment reads with inferred
per-function effects; nested bodies via the canonical `Stmt.anyDeep`. -/
def stmtReadsStateOrEnvWithFunctionEffectsNode
(effects : List (String × FunctionEffect)) : Stmt → Bool
| Stmt.internalCall name args | Stmt.internalCallAssign _ name args =>
(lookupFunctionEffect effects name).readsStateOrEnv ||
exprListReadsStateOrEnvWithFunctionEffects effects args
| Stmt.letVar _ value | Stmt.assignVar _ value | Stmt.setStorage _ value | Stmt.setStorageAddr _ value
| Stmt.setImmutable _ value | Stmt.setStorageWord _ _ value |
Stmt.return value | Stmt.require value _ =>
exprReadsStateOrEnvWithFunctionEffects effects value
| Stmt.storageArrayPush _ _ | Stmt.setStorageArrayElement _ _ _ | Stmt.storageArrayPop _ =>
true
| Stmt.requireError cond _ args =>
exprReadsStateOrEnvWithFunctionEffects effects cond ||
exprListReadsStateOrEnvWithFunctionEffects effects args
| Stmt.revertError _ args | Stmt.emit _ args | Stmt.returnValues args =>
exprListReadsStateOrEnvWithFunctionEffects effects args
| Stmt.returnArray _ | Stmt.returnBytes _ =>
false
| Stmt.returnStorageWords _ =>
true
| Stmt.returnCodeData _ =>
true
| Stmt.mstore offset value | Stmt.tstore offset value =>
exprReadsStateOrEnvWithFunctionEffects effects offset ||
exprReadsStateOrEnvWithFunctionEffects effects value
| Stmt.calldatacopy _ _ _ | Stmt.returndataCopy _ _ _ => true
| Stmt.revertReturndata =>
true
| Stmt.stop =>
false
| Stmt.setMapping _ _ _ | Stmt.setMappingWord _ _ _ _ | Stmt.setMappingPackedWord _ _ _ _ _ | Stmt.setMappingUint _ _ _
| Stmt.setMappingChain _ _ _
| Stmt.setMapping2 _ _ _ _ | Stmt.setMapping2Word _ _ _ _ _
| Stmt.setStructMember _ _ _ _ | Stmt.setStructMember2 _ _ _ _ _ => true
| Stmt.ite cond _ _ =>
exprReadsStateOrEnvWithFunctionEffects effects cond
| Stmt.forEach _ count _ =>
exprReadsStateOrEnvWithFunctionEffects effects count
| Stmt.unsafeBlock _ _ =>
false
| Stmt.rawLog topics dataOffset dataSize =>
topics.any (exprReadsStateOrEnvWithFunctionEffects effects) ||
exprReadsStateOrEnvWithFunctionEffects effects dataOffset ||
exprReadsStateOrEnvWithFunctionEffects effects dataSize
| Stmt.externalCallBind _ _ _ | Stmt.tryExternalCallBind _ _ _ _ => true
| Stmt.ecm mod args =>
mod.readsState || mod.writesState ||
exprListReadsStateOrEnvWithFunctionEffects effects args
| Stmt.matchAdt _ scrutinee _ =>
exprReadsStateOrEnvWithFunctionEffects effects scrutinee
| Stmt.unsafeYul fragment =>
!fragment.mechanics.isEmpty || !fragment.scopeEffects.storageWrites.isEmpty
def stmtReadsStateOrEnvWithFunctionEffects
(effects : List (String × FunctionEffect)) (s : Stmt) : Bool :=
s.anyDeep (stmtReadsStateOrEnvWithFunctionEffectsNode effects)
def stmtListReadsStateOrEnvWithFunctionEffects
(effects : List (String × FunctionEffect)) (stmts : List Stmt) : Bool :=
Stmt.anyDeepList (stmtReadsStateOrEnvWithFunctionEffectsNode effects) stmts
def matchBranchesReadStateOrEnvWithFunctionEffects
(effects : List (String × FunctionEffect))
(branches : List (String × List String × List Stmt)) : Bool :=
branches.any fun (_, _, body) => stmtListReadsStateOrEnvWithFunctionEffects effects body
private def functionEffectWithFunctionEffects
(effects : List (String × FunctionEffect)) (fn : FunctionSpec) : FunctionEffect :=
{ writesState := stmtListWritesStateWithFunctionEffects effects fn.body
readsStateOrEnv := stmtListReadsStateOrEnvWithFunctionEffects effects fn.body }
private def inferFunctionEffectsStep
(functions : List FunctionSpec) (effects : List (String × FunctionEffect)) :
List (String × FunctionEffect) :=
functions.map fun fn => (fn.name, functionEffectWithFunctionEffects effects fn)
private def iterateFunctionEffects
(functions : List FunctionSpec) : Nat → List (String × FunctionEffect) →
List (String × FunctionEffect)
| 0, effects => effects
| fuel + 1, effects =>
iterateFunctionEffects functions fuel (inferFunctionEffectsStep functions effects)
def inferFunctionEffects (functions : List FunctionSpec) : List (String × FunctionEffect) :=
-- Start optimistic and iterate once per function edge so internal-call read/write
-- effects propagate through helper chains and cycles before mutability validation.
let initial := functions.map fun fn => (fn.name, { writesState := false, readsStateOrEnv := false })
iterateFunctionEffects functions (functions.length + 1) initial
def validateFunctionSpecMutability
(effects : List (String × FunctionEffect)) (spec : FunctionSpec) : Except String Unit := do
let effect := functionEffectWithFunctionEffects effects spec
if spec.isView && effect.writesState then
throw s!"Compilation error: function '{spec.name}' is marked view but writes state ({issue734Ref})"
if spec.isPure && effect.writesState then
throw s!"Compilation error: function '{spec.name}' is marked pure but writes state ({issue734Ref})"
if spec.isPure && effect.readsStateOrEnv then
throw s!"Compilation error: function '{spec.name}' is marked pure but reads state/environment ({issue734Ref})"
/-- Node-local classifier: is this statement itself a persistent-storage
write? Covers all `setStorage*`, `setMapping*`, `storageArray*`,
`setStructMember*`, and `tstore` constructors plus storage-writing raw
Yul fragments. Events, local variables, and memory writes are NOT
considered persistent state writes for CEI purposes.
(#1728, Axis 2 Step 2a) -/
def stmtIsPersistentWriteNode : Stmt → Bool
| Stmt.setStorage _ _ | Stmt.setStorageAddr _ _ | Stmt.setStorageWord _ _ _
| Stmt.storageArrayPush _ _ | Stmt.storageArrayPop _ | Stmt.setStorageArrayElement _ _ _
| Stmt.setMapping _ _ _ | Stmt.setMappingWord _ _ _ _ | Stmt.setMappingPackedWord _ _ _ _ _ | Stmt.setMappingUint _ _ _
| Stmt.setMappingChain _ _ _