-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathStrataMain.lean
More file actions
1395 lines (1299 loc) · 61 KB
/
StrataMain.lean
File metadata and controls
1395 lines (1299 loc) · 61 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
/-
Copyright Strata Contributors
SPDX-License-Identifier: Apache-2.0 OR MIT
-/
-- Executable with utilities for working with Strata files.
import Strata.Backends.CBMC.CollectSymbols
import Strata.Backends.CBMC.GOTO.CoreToGOTOPipeline
import Strata.DDM.Integration.Java.Gen
import Strata.Languages.Core.Verifier
import Strata.Languages.Core.SarifOutput
import Strata.Languages.C_Simp.Verify
import Strata.Languages.B3.Verifier.Program
import Strata.Languages.Laurel.LaurelCompilationPipeline
import Strata.Languages.Python.Python
import Strata.Languages.Python.Specs.IdentifyOverloads
import Strata.Languages.Python.Specs.ToLaurel
import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator
import Strata.Languages.Laurel.Laurel
import Strata.Languages.Core.EntryPoint
import Strata.Transform.ProcedureInlining
import Strata.Util.IO
import Strata.SimpleAPI
import Strata.Util.Profile
import Strata.Util.Json
open Strata
open Core (VerifyOptions VerboseMode VerificationMode CheckLevel EntryPoint)
/-! ## Exit codes
All `strata` subcommands use a common exit code scheme:
| Code | Category | Meaning |
|------|--------------------|-----------------------------------------------------------|
| 0 | Success | Analysis passed, inconclusive, or `--no-solve` completed. |
| 1 | User error | Bad input: invalid arguments, malformed source, etc. |
| 2 | Failures found | Analysis completed and found failures. |
| 3 | Internal error | Tool bug, unexpected solver result, or translation crash. |
| 4 | Known limitation | Intentionally unsupported language construct. |
Codes 1–2 are **user-actionable** (fix the input or the code under analysis).
Codes 3–4 are **tool-side** (report as a bug or wait for support). -/
namespace ExitCode
def userError : UInt8 := 1
def failuresFound : UInt8 := 2
def internalError : UInt8 := 3
def knownLimitation : UInt8 := 4
end ExitCode
def exitFailure {α} (message : String) (hint : String := "strata --help") : IO α := do
IO.eprintln s!"Exception: {message}\n\nRun {hint} for additional help."
IO.Process.exit ExitCode.userError
/-- Exit with code 1 for user errors (bad input, malformed source, etc.). -/
def exitUserError {α} (message : String) : IO α := do
IO.eprintln s!"❌ {message}"
IO.Process.exit ExitCode.userError
/-- Exit with code 2 for analysis failures found. -/
def exitFailuresFound {α} (message : String) : IO α := do
IO.eprintln s!"Failures found: {message}"
IO.Process.exit ExitCode.failuresFound
/-- Exit with code 3 for internal errors (tool limitations or crashes). -/
def exitInternalError {α} (message : String) : IO α := do
IO.eprintln s!"Exception: {message}"
IO.Process.exit ExitCode.internalError
/-- Exit with code 4 for known limitations (unsupported constructs). -/
def exitKnownLimitation {α} (message : String) : IO α := do
IO.eprintln s!"Known limitation: {message}"
IO.Process.exit ExitCode.knownLimitation
/-- Like `exitFailure` but tailors the help hint to a specific subcommand. -/
def exitCmdFailure {α} (cmdName : String) (message : String) : IO α :=
exitFailure message (hint := s!"strata {cmdName} --help")
/-- How a flag consumes arguments. -/
inductive FlagArg where
| none -- boolean flag, e.g. --verbose
| arg (name : String) -- takes one value, e.g. --output <file>
| repeat (name : String) -- takes one value, may appear multiple times, e.g. --include <path>
/-- A flag that a command accepts. -/
structure Flag where
name : String -- flag name without "--", used as lookup key
help : String
takesArg : FlagArg := .none
/-- Parsed flags from the command line. Stored as an ordered array so that
command-line position is preserved (needed by `transform` to bind
`--procedures`/`--functions` to the preceding `--pass`).
For `.arg` flags that appear more than once, `getString` returns the
**last** occurrence (last-writer-wins). -/
structure ParsedFlags where
entries : Array (String × Option String) := #[]
namespace ParsedFlags
def getBool (pf : ParsedFlags) (name : String) : Bool :=
pf.entries.any (·.1 == name)
def getString (pf : ParsedFlags) (name : String) : Option String :=
-- Scan from the end so last occurrence wins.
match pf.entries.findRev? (·.1 == name) with
| some (_, some v) => some v
| _ => Option.none
def getRepeated (pf : ParsedFlags) (name : String) : Array String :=
pf.entries.foldl (init := #[]) fun acc (n, v) =>
if n == name then match v with | some s => acc.push s | none => acc else acc
def insert (pf : ParsedFlags) (name : String) (value : Option String) : ParsedFlags :=
{ pf with entries := pf.entries.push (name, value) }
def buildDialectFileMap (pflags : ParsedFlags) : IO Strata.DialectFileMap := do
let preloaded := Strata.Elab.LoadedDialects.builtin
|>.addDialect! Strata.Python.Python
|>.addDialect! Strata.Python.Specs.DDM.PythonSpecs
|>.addDialect! Strata.Core
|>.addDialect! Strata.Laurel.Laurel
|>.addDialect! Strata.smtReservedKeywordsDialect
|>.addDialect! Strata.SMTCore
|>.addDialect! Strata.SMT
|>.addDialect! Strata.SMTResponse
let mut sp ← Strata.DialectFileMap.new preloaded
for path in pflags.getRepeated "include" do
match ← sp.add path |>.toBaseIO with
| .error msg => exitFailure msg
| .ok sp' => sp := sp'
return sp
end ParsedFlags
def parseCheckMode (pflags : ParsedFlags)
(default : VerificationMode := .deductive) : IO VerificationMode :=
match pflags.getString "check-mode" with
| .none => pure default
| .some s => match VerificationMode.ofString? s with
| .some m => pure m
| .none => exitFailure s!"Invalid check mode: '{s}'. Must be {VerificationMode.options}."
def parseCheckLevel (pflags : ParsedFlags)
(default : CheckLevel := .minimal) : IO CheckLevel :=
match pflags.getString "check-level" with
| .none => pure default
| .some s => match CheckLevel.ofString? s with
| .some l => pure l
| .none => exitFailure s!"Invalid check level: '{s}'. Must be {CheckLevel.options}."
/-- Common CLI flags for VerifyOptions fields.
Commands can append these to their own flags list.
Note: `parseOnly`, `typeCheckOnly`, and `checkOnly` are omitted here
because they are specific to the `verify` command. -/
def verifyOptionsFlags : List Flag := [
{ name := "check-mode",
help := s!"Check mode: {VerificationMode.options}. Default: 'deductive'.",
takesArg := .arg "mode" },
{ name := "check-level",
help := s!"Check level: {CheckLevel.options}. Default: 'minimal'.",
takesArg := .arg "level" },
{ name := "verbose", help := "Enable verbose output." },
{ name := "quiet", help := "Suppress warnings on stderr." },
{ name := "profile", help := "Print elapsed time for each pipeline step." },
{ name := "sarif", help := "Write results as SARIF to <file>.sarif." },
{ name := "solver",
help := s!"SMT solver executable (default: {Core.defaultSolver}).",
takesArg := .arg "name" },
{ name := "solver-timeout",
help := "Solver timeout in seconds (default: 10).",
takesArg := .arg "seconds" },
{ name := "vc-directory",
help := "Store VCs in SMT-Lib format in <dir>.",
takesArg := .arg "dir" },
{ name := "no-solve",
help := "Generate SMT-Lib files but do not invoke the solver." },
{ name := "stop-on-first-error",
help := "Exit after the first verification error." },
{ name := "unique-bound-names",
help := "Use globally unique names for quantifier-bound variables." },
{ name := "use-array-theory",
help := "Use SMT-LIB Array theory instead of axiomatized maps." },
{ name := "remove-irrelevant-axioms",
help := "Prune irrelevant axioms: 'off', 'aggressive', or 'precise'.",
takesArg := .arg "mode" },
{ name := "overflow-checks",
help := "Comma-separated overflow checks to enable (signed,unsigned,float64,all,none).",
takesArg := .arg "checks" }
]
/-- Build a VerifyOptions from parsed CLI flags, starting from a base config.
Fields not present in the flags keep their base values.
Note: boolean flags can only enable a setting; a `true` in the base
cannot be turned off from the CLI (there is no `--no-X` syntax). -/
def parseVerifyOptions (pflags : ParsedFlags)
(base : VerifyOptions := VerifyOptions.default) : IO VerifyOptions := do
let checkMode ← parseCheckMode pflags base.checkMode
let checkLevel ← parseCheckLevel pflags base.checkLevel
let solverTimeout ← match pflags.getString "solver-timeout" with
| .none => pure base.solverTimeout
| .some s => match s.toNat? with
| .some n => pure n
| .none => exitFailure s!"Invalid solver timeout: '{s}'"
let noSolve := pflags.getBool "no-solve"
let removeIrrelevantAxioms ← match pflags.getString "remove-irrelevant-axioms" with
| .none => pure base.removeIrrelevantAxioms
| .some "off" => pure .Off
| .some "aggressive" => pure .Aggressive
| .some "precise" => pure .Precise
| .some s => exitFailure s!"Invalid remove-irrelevant-axioms mode: '{s}'. Must be 'off', 'aggressive', or 'precise'."
let overflowChecks := match pflags.getString "overflow-checks" with
| .none => base.overflowChecks
| .some s => s.splitOn "," |>.foldl (fun acc c =>
match c.trimAscii.toString with
| "signed" => { acc with signedBV := true }
| "unsigned" => { acc with unsignedBV := true }
| "float64" => { acc with float64 := true }
| "none" => { signedBV := false, unsignedBV := false, float64 := false }
| "all" => { signedBV := true, unsignedBV := true, float64 := true }
| _ => acc) { signedBV := false, unsignedBV := false, float64 := false }
let vcDirectory := (pflags.getString "vc-directory" |>.map (⟨·⟩ : String → System.FilePath)).orElse (fun _ => base.vcDirectory)
let skipSolver := noSolve || base.skipSolver
if skipSolver && vcDirectory.isNone then
exitFailure "--no-solve requires --vc-directory to specify where SMT files are stored."
pure { base with
verbose := if pflags.getBool "verbose" then .normal
else if pflags.getBool "quiet" then .quiet
else base.verbose,
solver := pflags.getString "solver" |>.getD base.solver,
solverTimeout,
checkMode, checkLevel,
stopOnFirstError := pflags.getBool "stop-on-first-error" || base.stopOnFirstError,
uniqueBoundNames := pflags.getBool "unique-bound-names" || base.uniqueBoundNames,
useArrayTheory := pflags.getBool "use-array-theory" || base.useArrayTheory,
removeIrrelevantAxioms,
outputSarif := pflags.getBool "sarif" || base.outputSarif,
profile := pflags.getBool "profile" || base.profile,
skipSolver,
alwaysGenerateSMT := noSolve || base.alwaysGenerateSMT,
overflowChecks,
vcDirectory
}
/-- Read and parse a Strata program file, loading the Core, C_Simp, and B3CST
dialects. Returns the parsed program and the input context (for source
location resolution), or an array of error messages on failure. -/
private def readStrataProgram (file : String)
: IO (Except (Array Lean.Message) (Strata.Program × Lean.Parser.InputContext)) := do
let text ← Strata.Util.readInputSource file
let inputCtx := Lean.Parser.mkInputContext text (Strata.Util.displayName file)
let dctx := Elab.LoadedDialects.builtin
let dctx := dctx.addDialect! Core
let dctx := dctx.addDialect! C_Simp
let dctx := dctx.addDialect! B3CST
let leanEnv ← Lean.mkEmptyEnvironment 0
match Strata.Elab.elabProgram dctx leanEnv inputCtx with
| .ok pgm => pure (.ok (pgm, inputCtx))
| .error msgs => pure (.error msgs)
structure Command where
name : String
args : List String
flags : List Flag := []
help : String
callback : Vector String args.length → ParsedFlags → IO Unit
def includeFlag : Flag :=
{ name := "include", help := "Add a dialect search path.", takesArg := .repeat "path" }
def checkCommand : Command where
name := "check"
args := [ "file" ]
flags := [includeFlag]
help := "Parse and validate a Strata file (text or Ion). Reports errors and exits."
callback := fun v pflags => do
let fm ← pflags.buildDialectFileMap
let _ ← Strata.readStrataFile fm v[0]
def toIonCommand : Command where
name := "toIon"
args := [ "input", "output" ]
flags := [includeFlag]
help := "Convert a Strata text file to Ion binary format."
callback := fun v pflags => do
let searchPath ← pflags.buildDialectFileMap
let pd ← Strata.readStrataFile searchPath v[0]
match pd with
| .dialect d =>
IO.FS.writeBinFile v[1] d.toIon
| .program pgm =>
IO.FS.writeBinFile v[1] pgm.toIon
def printCommand : Command where
name := "print"
args := [ "file" ]
flags := [includeFlag]
help := "Pretty-print a Strata file (text or Ion) to stdout."
callback := fun v pflags => do
let searchPath ← pflags.buildDialectFileMap
-- Special case for already loaded dialects.
let ld ← searchPath.getLoaded
if mem : v[0] ∈ ld.dialects then
IO.print <| ld.dialects.format v[0] mem
return
let pd ← Strata.readStrataFile searchPath v[0]
match pd with
| .dialect d =>
let ld ← searchPath.getLoaded
let .isTrue mem := inferInstanceAs (Decidable (d.name ∈ ld.dialects))
| exitInternalError "Internal error reading file."
IO.print <| ld.dialects.format d.name mem
| .program pgm =>
IO.print <| toString pgm
def diffCommand : Command where
name := "diff"
args := [ "file1", "file2" ]
flags := [includeFlag]
help := "Compare two program files for syntactic equality. Reports the first difference found."
callback := fun v pflags => do
let fm ← pflags.buildDialectFileMap
let p1 ← Strata.readStrataFile fm v[0]
let p2 ← Strata.readStrataFile fm v[1]
match p1, p2 with
| .program p1, .program p2 =>
if p1.dialect != p2.dialect then
exitFailure s!"Dialects differ: {p1.dialect} and {p2.dialect}"
let Decidable.isTrue eq := inferInstanceAs (Decidable (p1.commands.size = p2.commands.size))
| exitFailure s!"Number of commands differ {p1.commands.size} and {p2.commands.size}"
for (c1, c2) in Array.zip p1.commands p2.commands do
if c1 != c2 then
exitFailure s!"Commands differ: {repr c1} and {repr c2}"
| _, _ =>
exitFailure "Cannot compare dialect def with another dialect/program."
def pySpecsCommand : Command where
name := "pySpecs"
args := [ "source_dir", "output_dir" ]
flags := [
{ name := "quiet", help := "Suppress default logging." },
{ name := "log", help := "Enable logging for an event type.",
takesArg := .repeat "event" },
{ name := "skip",
help := "Skip a top-level definition (module.name). Overloads are kept.",
takesArg := .repeat "name" },
{ name := "module",
help := "Translate only the named module (dot-separated). May be repeated.",
takesArg := .repeat "module" }
]
help := "Translate Python specification files in a directory into Strata DDM Ion format. If --module is given, translates only those modules; otherwise translates all .py files. Creates subdirectories as needed. (Experimental)"
callback := fun v pflags => do
let quiet := pflags.getBool "quiet"
let mut events : Std.HashSet String := {}
if !quiet then
events := events.insert "import"
for e in pflags.getRepeated "log" do
events := events.insert e
let skipNames := pflags.getRepeated "skip"
let modules := pflags.getRepeated "module"
let warningOutput : Strata.WarningOutput :=
if quiet then .none else .detail
-- Serialize embedded dialect for Python subprocess
IO.FS.withTempFile fun _handle dialectFile => do
IO.FS.writeBinFile dialectFile Strata.Python.Python.toIon
let r ← Strata.pySpecsDir (events := events)
(skipNames := skipNames)
(modules := modules)
(warningOutput := warningOutput)
v[0] v[1] dialectFile |>.toBaseIO
match r with
| .ok () => pure ()
| .error msg => exitFailure msg
/-- Derive Python source file path from Ion file path.
E.g., "tests/test_foo.python.st.ion" -> "tests/test_foo.py" -/
def ionPathToPythonPath (ionPath : String) : Option String :=
if ionPath.endsWith ".python.st.ion" then
let basePath := ionPath.dropEnd ".python.st.ion".length |>.toString
some (basePath ++ ".py")
else if ionPath.endsWith ".py.ion" then
some (ionPath.dropEnd ".ion".length |>.toString)
else
none
/-- Try to read Python source file for source location reconstruction -/
def tryReadPythonSource (ionPath : String) : IO (Option (String × String)) := do
match ionPathToPythonPath ionPath with
| none => return none
| some pyPath =>
try
let content ← IO.FS.readFile pyPath
return some (pyPath, content)
catch _ =>
return none
/-- Format related position strings from metadata, if present. -/
def formatRelatedPositions (md : Imperative.MetaData Core.Expression)
(mfm : Option (String × Lean.FileMap)) : String :=
let ranges := Imperative.getRelatedFileRanges md
if ranges.isEmpty then "" else
match mfm with
| none => ""
| some (_, fm) =>
let lines := ranges.filterMap fun fr =>
if fr.range.isNone then none else
match fr.file with
| .file "" => some "\n Related location: in prelude file"
| .file _ =>
let pos := fm.toPosition fr.range.start
some s!"\n Related location: line {pos.line}, col {pos.column}"
String.join lines.toList
/-! ### pyAnalyzeLaurel result helpers
The `pyAnalyzeLaurel` command emits two structured lines on stdout:
- `RESULT: <category>` — machine-readable category, always the last line.
- `DETAIL: <detail>` — human-readable context (error message or VC counts).
Exit codes follow the common scheme (see `ExitCode` above).
A successful run exits 0 with `RESULT: Analysis success` or `RESULT: Inconclusive`. -/
/-- Determines which VC results count as successes and which count as failures
for the purposes of the `pyAnalyzeLaurel` summary and exit code.
Implementation-error results are partitioned out first; the classifier then
partitions the rest into success / failure / inconclusive.
Narrowing `isFailure` (e.g. to only `alwaysFalseAndReachable`) automatically
widens inconclusive.
Future: may be extended with `isWarning` for non-fatal diagnostic categories. -/
structure ResultClassifier where
isSuccess : Core.VCResult → Bool := (·.isSuccess)
isFailure : Core.VCResult → Bool := (·.isFailure)
private def printPyAnalyzeResult (category : String) (detail : String) : IO Unit := do
IO.println s!"DETAIL: {detail}"
IO.println s!"RESULT: {category}"
private def exitPyAnalyzeUserError {α} (message : String) : IO α := do
printPyAnalyzeResult "User error" message
IO.Process.exit ExitCode.userError
private def exitPyAnalyzeFailuresFound {α} (detail : String) : IO α := do
printPyAnalyzeResult "Failures found" detail
IO.Process.exit ExitCode.failuresFound
private def exitPyAnalyzeInternalError {α} (message : String) : IO α := do
printPyAnalyzeResult "Internal error" message
IO.Process.exit ExitCode.internalError
private def exitPyAnalyzeKnownLimitation {α} (message : String) : IO α := do
printPyAnalyzeResult "Known limitation" message
IO.Process.exit ExitCode.knownLimitation
/-- Print the final RESULT/DETAIL lines based on solver outcomes.
Always called on successful pipeline completion (as opposed to the
exit helpers above, which are called on early pipeline failure).
Classification uses successive partitioning: implementation errors are
removed first, then the classifier partitions the rest into
success / failure / inconclusive (guaranteeing disjointness).
Unreachable count is reported as supplementary info. -/
private def printPyAnalyzeSummary (vcResults : Array Core.VCResult)
(checkMode : VerificationMode := .deductive) : IO Unit := do
let classifier : ResultClassifier :=
match checkMode with
| .bugFinding | .bugFindingAssumingCompleteSpec =>
{ isSuccess := (·.isBugFindingSuccess)
isFailure := (·.isBugFindingFailure) }
| _ => {}
-- 1. Partition out implementation errors (broken results, not classifiable).
let (implError, classifiable) :=
vcResults.partition (fun r => r.isImplementationError || r.hasSMTError)
-- 2. Successive partitioning via the classifier: success → failure → inconclusive.
let (success, rest) := classifiable.partition classifier.isSuccess
let (failure, inconclusive) := rest.partition classifier.isFailure
-- 3. Unreachable is informational (not a separate partition).
let nUnreachable := vcResults.filter (·.isUnreachable) |>.size
let nImplError := implError.size
let nSuccess := success.size
let nFailure := failure.size
let nInconclusive := inconclusive.size
let unreachableStr := if nUnreachable > 0 then s!", {nUnreachable} unreachable" else ""
let implErrorStr := if nImplError > 0 then s!", {nImplError} implementation errors" else ""
let counts := s!"{nSuccess} passed, {nFailure} failed, {nInconclusive} inconclusive{unreachableStr}{implErrorStr}"
if nImplError > 0 then
exitPyAnalyzeInternalError s!"An unexpected result was produced. {counts}"
else if nFailure > 0 then
exitPyAnalyzeFailuresFound counts
else
printPyAnalyzeResult (if nInconclusive > 0 then "Inconclusive" else "Analysis success") counts
private def deriveBaseName (file : String) : String :=
let name := System.FilePath.fileName file |>.getD file
let suffixes := [".python.st.ion", ".py.ion", ".st.ion", ".st"]
match suffixes.find? (name.endsWith ·) with
| some sfx => (name.dropEnd sfx.length).toString
| none => name
def pyAnalyzeLaurelCommand : Command where
name := "pyAnalyzeLaurel"
args := [ "file" ]
flags := verifyOptionsFlags ++ [
{ name := "spec-dir",
help := "Directory containing compiled PySpec Ion files.",
takesArg := .arg "dir" },
{ name := "dispatch",
help := "Dispatch module name (e.g., servicelib).",
takesArg := .repeat "module" },
{ name := "pyspec",
help := "PySpec module name (e.g., servicelib.Storage).",
takesArg := .repeat "module" },
{ name := "keep-all-files",
help := "Store intermediate Laurel and Core programs in <dir>.",
takesArg := .arg "dir" },
{ name := "entry-point",
help := "Which procedures to verify: main (main fn only), roots (user procs with no user callers, default), or all (all user procs). Only valid in bugFinding mode.",
takesArg := .arg "mode" },
{ name := "warning-summary",
help := "Write PySpec warning summary as JSON to <file>.",
takesArg := .arg "file" }]
help := "Verify a Python Ion program via the Laurel pipeline. Translates Python to Laurel to Core, then runs SMT verification."
callback := fun v pflags => do
let verbose := pflags.getBool "verbose"
let profile := pflags.getBool "profile"
let quiet := pflags.getBool "quiet"
let outputSarif := pflags.getBool "sarif"
let filePath := v[0]
let pySourceOpt ← tryReadPythonSource filePath
let keepDir := pflags.getString "keep-all-files"
let baseName := deriveBaseName filePath
if let some dir := keepDir then
IO.FS.createDirAll dir
let dispatchModules := pflags.getRepeated "dispatch"
let pyspecModules := pflags.getRepeated "pyspec"
let specDir := pflags.getString "spec-dir" |>.getD "."
unless ← System.FilePath.isDir specDir do
exitFailure s!"spec-dir '{specDir}' does not exist or is not a directory"
let sourcePath := pySourceOpt.map (·.1)
-- Build FileMap for source position resolution.
let mfm : Option (String × Lean.FileMap) := match pySourceOpt with
| some (pyPath, srcText) => some (pyPath, .ofString srcText)
| none => none
let warningSummaryFile := pflags.getString "warning-summary"
let combinedLaurel ←
match ← Strata.pythonAndSpecToLaurel filePath dispatchModules pyspecModules sourcePath
(specDir := specDir) (profile := profile)
(quiet := quiet)
(warningSummaryFile := warningSummaryFile) |>.toBaseIO with
| .ok r => pure r
| .error (.userCode range msg) =>
let location := if range.isNone then "" else
match mfm with
| some (_, fm) =>
let pos := fm.toPosition range.start
s!" at line {pos.line}, col {pos.column}"
| none => ""
let filePath' := sourcePath.getD filePath
let mut lines := #[
s!"(set-info :file {Strata.escapeSMTStringLit filePath'})"
]
unless range.isNone do
lines := lines.push s!"(set-info :start {range.start})"
lines := lines.push s!"(set-info :stop {range.stop})"
lines := lines.push s!"(set-info :error-message {Strata.escapeSMTStringLit msg})"
for line in lines do
IO.println line
IO.FS.writeFile "user_errors.txt" (String.intercalate "\n" lines.toList ++ "\n")
exitPyAnalyzeUserError s!"{msg}{location}"
| .error (.knownLimitation msg) =>
exitPyAnalyzeKnownLimitation msg
| .error (.internal msg) =>
exitPyAnalyzeInternalError msg
if verbose then
IO.println "\n==== Laurel Program ===="
IO.println f!"{combinedLaurel}"
let keepPrefix := keepDir.map (s!"{·}/{baseName}")
let (coreProgramOption, laurelTranslateErrors, _loweredLaurel) ←
profileStep profile "Laurel to Core translation" do
Strata.translateCombinedLaurelWithLowered combinedLaurel
(keepAllFilesPrefix := keepPrefix)
let coreProgram ←
match coreProgramOption with
| none =>
exitPyAnalyzeInternalError s!"Laurel to Core translation failed: {laurelTranslateErrors}"
| some core => pure core
if verbose then
IO.println "\n==== Core Program ===="
IO.print (Core.formatProgram coreProgram)
-- Verify using Core verifier
-- --keep-all-files implies vc-directory if not explicitly set
let baseVcDir := keepDir.map (fun dir => (s!"{dir}/{baseName}" : System.FilePath))
let pyAnalyzeBase : VerifyOptions :=
{ VerifyOptions.default with
verbose := .quiet, removeIrrelevantAxioms := .Precise,
vcDirectory := baseVcDir }
let options ← parseVerifyOptions pflags pyAnalyzeBase
let isBugFinding := options.checkMode == .bugFinding
|| options.checkMode == .bugFindingAssumingCompleteSpec
-- Parse --entry-point flag (only supported in bug-finding modes).
let entryPointFlag := pflags.getString "entry-point"
let entryPoint : EntryPoint ←
if isBugFinding then
match entryPointFlag with
| some s =>
match EntryPoint.ofString? s with
| some ep => pure ep
| none =>
exitPyAnalyzeUserError s!"Invalid --entry-point value '{s}'. Must be {EntryPoint.options}."
| none => pure .roots
else
if entryPointFlag.isSome then
exitPyAnalyzeUserError s!"--entry-point is unsupported in {options.checkMode} mode"
else pure .all
-- Pick the procedures to verify and set up inlining phases.
let userSourcePath := sourcePath.getD filePath
let (_, userProcNames) :=
Strata.splitProcNames coreProgram [userSourcePath]
let (proceduresToVerify, inlinePhases) :=
if isBugFinding then
let ⟨p, i⟩ := Core.chooseEntryProceduresAndBuildInlinePhases coreProgram userProcNames entryPoint
(p, [i])
else (userProcNames, [])
let vcResults ← profileStep profile "SMT verification" do
match ← Core.verifyProgram coreProgram options
(moreFns := Strata.Python.ReFactory)
(proceduresToVerify := some proceduresToVerify)
(externalPhases := [Strata.frontEndPhase])
(prefixPhases := inlinePhases)
(keepAllFilesPrefix := keepPrefix)
|>.toBaseIO with
| .ok r => pure r.mergeByAssertion
| .error msg => exitPyAnalyzeInternalError msg
-- Print translation errors (always on stderr)
if !laurelTranslateErrors.isEmpty then
IO.eprintln "\n==== Errors ===="
for err in laurelTranslateErrors do
IO.eprintln err
-- Print per-VC results by default, unless SARIF mode is used
if !outputSarif then
let mut s := ""
for vcResult in vcResults do
let fileMap := mfm.map (·.2)
let location := match Imperative.getFileRange vcResult.obligation.metadata with
| some fr =>
if fr.range.isNone then ""
else s!"{fr.format fileMap (includeEnd? := false)}"
| none => ""
let messageSuffix := match vcResult.obligation.metadata.getPropertySummary with
| some msg => s!" - {msg}"
| none => s!" - {vcResult.obligation.label}"
let outcomeStr := vcResult.formatOutcome
let loc := if !location.isEmpty then s!"{location}: " else "unknown location: "
s := s ++ s!"{loc}{outcomeStr}{messageSuffix}\n"
IO.print s
-- Output in SARIF format if requested
if outputSarif then
let files := match mfm with
| some (pyPath, fm) => Map.empty.insert (Strata.Uri.file pyPath) fm
| none => Map.empty
Core.Sarif.writeSarifOutput options.checkMode files vcResults (filePath ++ ".sarif")
printPyAnalyzeSummary vcResults options.checkMode
def pyAnalyzeToGotoCommand : Command where
name := "pyAnalyzeToGoto"
args := [ "file" ]
help := "Translate a Strata Python Ion file to CProver GOTO JSON files."
callback := fun v _ => do
let filePath := v[0]
let pySourceOpt ← tryReadPythonSource filePath
let sourcePathForMetadata := match pySourceOpt with
| some (pyPath, _) => pyPath
| none => filePath
let sourceText := pySourceOpt.map (·.2)
let newPgm ← Strata.pythonDirectToCore filePath sourcePathForMetadata
match Core.inlineProcedures newPgm { doInline := (fun _caller callee _ => callee ≠ "main") } with
| .error e => exitInternalError e
| .ok newPgm =>
-- Type-check the full program (registers Python types like ExceptOrNone)
let Ctx := { Lambda.LContext.default with functions := Strata.Python.PythonFactory, knownTypes := Core.KnownTypes }
let Env := Lambda.TEnv.default
let (tcPgm, _) ← match Core.Program.typeCheck Ctx Env newPgm with
| .ok r => pure r
| .error e => exitInternalError s!"{e.format none}"
-- Find the main procedure
let some mainDecl := tcPgm.decls.find? fun d =>
match d with
| .proc p _ => Core.CoreIdent.toPretty p.header.name == "main"
| _ => false
| exitInternalError "No main procedure found"
let some p := mainDecl.getProc?
| exitInternalError "main is not a procedure"
-- Translate procedure to GOTO (mirrors CoreToGOTO.transformToGoto post-typecheck logic)
let baseName := deriveBaseName filePath
let procName := Core.CoreIdent.toPretty p.header.name
let axioms := tcPgm.decls.filterMap fun d => d.getAxiom?
let distincts := tcPgm.decls.filterMap fun d => match d with
| .distinct name es _ => some (name, es) | _ => none
match procedureToGotoCtx Env p sourceText (axioms := axioms) (distincts := distincts)
(varTypes := tcPgm.getVarTy?) with
| .error e => exitInternalError s!"{e}"
| .ok (ctx, liftedFuncs) =>
let extraSyms ← match collectExtraSymbols tcPgm with
| .ok s => pure (Lean.toJson s)
| .error e => exitInternalError s!"{e}"
let (symtab, goto) ← emitProcWithLifted Env procName ctx liftedFuncs extraSyms
(moduleName := baseName)
let symTabFile := s!"{baseName}.symtab.json"
let gotoFile := s!"{baseName}.goto.json"
writeJsonFile symTabFile symtab
writeJsonFile gotoFile goto
IO.println s!"Written {symTabFile} and {gotoFile}"
def pyTranslateLaurelCommand : Command where
name := "pyTranslateLaurel"
args := [ "file" ]
flags := [{ name := "pyspec",
help := "PySpec module name (e.g., servicelib.Storage).",
takesArg := .repeat "module" },
{ name := "dispatch",
help := "Dispatch module name (e.g., servicelib).",
takesArg := .repeat "module" },
{ name := "spec-dir",
help := "Directory containing compiled PySpec Ion files.",
takesArg := .arg "dir" }]
help := "Translate a Strata Python Ion file through Laurel to Strata Core. Write results to stdout."
callback := fun v pflags => do
let dispatchModules := pflags.getRepeated "dispatch"
let pyspecModules := pflags.getRepeated "pyspec"
let specDir := pflags.getString "spec-dir" |>.getD "."
unless ← System.FilePath.isDir specDir do
exitFailure s!"spec-dir '{specDir}' does not exist or is not a directory"
let coreProgram ←
match ← Strata.pyTranslateLaurel v[0] dispatchModules pyspecModules (specDir := specDir) |>.toBaseIO with
| .ok r => pure r
| .error msg => exitFailure msg
IO.print coreProgram
def pyAnalyzeLaurelToGotoCommand : Command where
name := "pyAnalyzeLaurelToGoto"
args := [ "file" ]
flags := [{ name := "pyspec",
help := "PySpec module name (e.g., servicelib.Storage).",
takesArg := .repeat "module" },
{ name := "dispatch",
help := "Dispatch module name (e.g., servicelib).",
takesArg := .repeat "module" },
{ name := "spec-dir",
help := "Directory containing compiled PySpec Ion files.",
takesArg := .arg "dir" }]
help := "Translate a Strata Python Ion file through Laurel to CProver GOTO JSON files."
callback := fun v pflags => do
let filePath := v[0]
let dispatchModules := pflags.getRepeated "dispatch"
let pyspecModules := pflags.getRepeated "pyspec"
let specDir := pflags.getString "spec-dir" |>.getD "."
unless ← System.FilePath.isDir specDir do
exitFailure s!"spec-dir '{specDir}' does not exist or is not a directory"
let (coreProgram, laurelTranslateErrors) ←
match ← Strata.pyTranslateLaurel filePath dispatchModules pyspecModules (specDir := specDir) |>.toBaseIO with
| .ok r => pure r
| .error msg => exitFailure msg
let sourceText := (← tryReadPythonSource filePath).map (·.2)
let baseName := deriveBaseName filePath
match ← Strata.inlineCoreToGotoFiles coreProgram baseName sourceText
(factory := Strata.Python.PythonFactory) |>.toBaseIO with
| .ok () => pure ()
| .error msg => exitFailure msg
def javaGenCommand : Command where
name := "javaGen"
args := [ "dialect", "package", "output-dir" ]
flags := [includeFlag]
help := "Generate Java source files from a DDM dialect definition. Accepts a dialect name (e.g. Laurel) or a dialect file path."
callback := fun v pflags => do
let fm ← pflags.buildDialectFileMap
let ld ← fm.getLoaded
let d ← if mem : v[0] ∈ ld.dialects then
pure ld.dialects[v[0]]
else
match ← Strata.readStrataFile fm v[0] with
| .dialect d => pure d
| .program _ => exitFailure "Expected a dialect file, not a program file."
match Strata.Java.generateDialect d v[1] with
| .ok files =>
Strata.Java.writeJavaFiles v[2] v[1] files
IO.println s!"Generated Java files for {d.name} in {v[2]}/{Strata.Java.packageToPath v[1]}"
| .error msg =>
exitFailure s!"Error generating Java: {msg}"
def laurelVerifyOptions : VerifyOptions := { VerifyOptions.default with solver := "z3" }
def laurelAnalyzeBinaryCommand : Command where
name := "laurelAnalyzeBinary"
args := []
help := "Verify Laurel Ion programs read from stdin and print diagnostics. Combines multiple input files."
callback := fun _ _ => do
let stdinBytes ← (← IO.getStdin).readBinToEnd
let combinedProgram ← Strata.readLaurelIonProgram stdinBytes
let diagnostics ← Strata.Laurel.analyzeToDiagnosticModels combinedProgram laurelVerifyOptions
IO.println s!"==== DIAGNOSTICS ===="
for diag in diagnostics do
IO.println s!"{Std.format diag.fileRange.file}:{diag.fileRange.range.start}-{diag.fileRange.range.stop}: {diag.message}"
def pySpecToLaurelCommand : Command where
name := "pySpecToLaurel"
args := [ "python_path", "strata_path" ]
help := "Translate a PySpec Ion file to Laurel declarations. The Ion file must already exist."
callback := fun v _ => do
let pythonFile : System.FilePath := v[0]
let strataDir : System.FilePath := v[1]
let some mod := pythonFile.fileStem
| exitFailure s!"No stem {pythonFile}"
let .ok mod := Strata.Python.Specs.ModuleName.ofString mod
| exitFailure s!"Invalid module {mod}"
let ionFile := strataDir / mod.strataFileName
let sigs ←
match ← Strata.Python.Specs.readDDM ionFile |>.toBaseIO with
| .ok t => pure t
| .error msg => exitFailure s!"Could not read {ionFile}: {msg}"
let result := Strata.Python.Specs.ToLaurel.signaturesToLaurel pythonFile sigs ""
if result.errors.size > 0 then
IO.eprintln s!"{result.errors.size} translation warning(s):"
for err in result.errors do
IO.eprintln s!" {err.file}: {err.message}"
let pgm := result.program
IO.println s!"Laurel: {pgm.staticProcedures.length} procedure(s), {pgm.types.length} type(s)"
IO.println s!"Overloads: {result.overloads.size} function(s)"
for td in pgm.types do
IO.println s!" {Strata.Laurel.formatTypeDefinition td}"
for proc in pgm.staticProcedures do
IO.println s!" {Strata.Laurel.formatProcedure proc}"
def pyResolveOverloadsCommand : Command where
name := "pyResolveOverloads"
args := [ "python_path", "dispatch_ion" ]
help := "Identify which overloaded service modules a \
Python program uses. Prints one module name per \
line to stdout."
callback := fun v _ => do
let pythonFile : System.FilePath := v[0]
let dispatchPath := v[1]
-- Read dispatch overload table
let overloads ←
match ← readDispatchOverloads #[dispatchPath] |>.toBaseIO with
| .ok (r, _) => pure r
| .error msg => exitFailure msg
-- Convert .py to Python AST
let stmts ←
IO.FS.withTempFile fun _handle dialectFile => do
IO.FS.writeBinFile dialectFile
Strata.Python.Python.toIon
match ← Strata.Python.pythonToStrata dialectFile pythonFile |>.toBaseIO with
| .ok s => pure s
| .error msg => exitFailure msg
-- Walk AST and collect modules
let state :=
Strata.Python.Specs.IdentifyOverloads.resolveOverloads
overloads stmts
for w in state.warnings do
IO.eprintln s!"warning: {w}"
let sorted := state.modules.toArray.qsort (· < ·)
for m in sorted do
IO.println m
def laurelParseCommand : Command where
name := "laurelParse"
args := [ "file" ]
help := "Parse a Laurel source file (no verification)."
callback := fun v _ => do
let _ ← Strata.readLaurelTextFile v[0]
IO.println "Parse successful"
def laurelAnalyzeCommand : Command where
name := "laurelAnalyze"
args := [ "file" ]
help := "Analyze a Laurel source file. Write diagnostics to stdout."
callback := fun v _ => do
let laurelProgram ← Strata.readLaurelTextFile v[0]
let (vcResultsOption, errors) ← Strata.Laurel.verifyProgram laurelProgram { VerifyOptions.default with solver := "z3" }
if !errors.isEmpty then
IO.println s!"==== ERRORS ===="
for err in errors do
IO.println s!"{err.message}"
match vcResultsOption with
| none => return
| some vcResults =>
IO.println s!"==== RESULTS ===="
for vc in vcResults do
IO.println s!"{vc.obligation.label}: {match vc.outcome with | .ok o => repr o | .error e => e}"
def laurelAnalyzeToGotoCommand : Command where
name := "laurelAnalyzeToGoto"
args := [ "file" ]
help := "Translate a Laurel source file to CProver GOTO JSON files."
callback := fun v _ => do
let path : System.FilePath := v[0]
let content ← IO.FS.readFile path
let laurelProgram ← Strata.parseLaurelText path content
match ← Strata.Laurel.translate {} laurelProgram with
| (none, diags) => exitFailure s!"Core translation errors: {diags.map (·.message)}"
| (some coreProgram, errors) =>
let Ctx := { Lambda.LContext.default with functions := Core.Factory, knownTypes := Core.KnownTypes }
let Env := Lambda.TEnv.default
let (tcPgm, _) ← match Core.Program.typeCheck Ctx Env coreProgram with
| .ok r => pure r
| .error e => exitInternalError s!"{e.format none}"
let procs := tcPgm.decls.filterMap fun d => d.getProc?
let funcs := tcPgm.decls.filterMap fun d =>
match d.getFunc? with
| some f =>
let name := Core.CoreIdent.toPretty f.name
if f.body.isSome && f.typeArgs.isEmpty
&& name != "Int.DivT" && name != "Int.ModT"
then some f else none
| none => none
if procs.isEmpty && funcs.isEmpty then exitInternalError "No procedures or functions found"
let baseName := deriveBaseName path.toString
let typeSyms ← match collectExtraSymbols tcPgm with
| .ok s => pure s
| .error e => exitInternalError s!"{e}"
let typeSymsJson := Lean.toJson typeSyms
let sourceText := some content
let axioms := tcPgm.decls.filterMap fun d => d.getAxiom?
let distincts := tcPgm.decls.filterMap fun d => match d with
| .distinct name es _ => some (name, es) | _ => none
let mut symtabPairs : List (String × Lean.Json) := []
let mut gotoFns : Array Lean.Json := #[]
let mut allLiftedFuncs : List Core.Function := []
for p in procs do
let procName := Core.CoreIdent.toPretty p.header.name
match procedureToGotoCtx Env p (sourceText := sourceText) (axioms := axioms) (distincts := distincts)
(varTypes := tcPgm.getVarTy?) with
| .error e => exitInternalError s!"{e}"
| .ok (ctx, liftedFuncs) =>
allLiftedFuncs := allLiftedFuncs ++ liftedFuncs
let json ← IO.ofExcept (CoreToGOTO.CProverGOTO.Context.toJson procName ctx)
match json.symtab with
| .obj m => symtabPairs := symtabPairs ++ m.toList
| _ => pure ()
match json.goto with
| .obj m =>
match m.toList.find? (·.1 == "functions") with
| some (_, .arr fns) => gotoFns := gotoFns ++ fns
| _ => pure ()
| _ => pure ()
for f in funcs ++ allLiftedFuncs do
let funcName := Core.CoreIdent.toPretty f.name
match functionToGotoCtx Env f with
| .error e => exitInternalError s!"{e}"
| .ok ctx =>
let json ← IO.ofExcept (CoreToGOTO.CProverGOTO.Context.toJson funcName ctx)
match json.symtab with
| .obj m => symtabPairs := symtabPairs ++ m.toList
| _ => pure ()
match json.goto with
| .obj m =>
match m.toList.find? (·.1 == "functions") with
| some (_, .arr fns) => gotoFns := gotoFns ++ fns
| _ => pure ()
| _ => pure ()
match typeSymsJson with
| .obj m => symtabPairs := symtabPairs ++ m.toList
| _ => pure ()
-- Deduplicate: keep first occurrence of each symbol name (proper function
-- symbols come before basic symbol references from callers)
let mut seen : Std.HashSet String := {}
let mut dedupPairs : List (String × Lean.Json) := []
for (k, v) in symtabPairs do
if !seen.contains k then
seen := seen.insert k
dedupPairs := dedupPairs ++ [(k, v)]
-- Add CBMC default symbols (architecture constants, builtins)
-- and wrap in {"symbolTable": ...} for symtab2gb
let symtabObj := dedupPairs.foldl
(fun (acc : Std.TreeMap.Raw String Lean.Json) (k, v) => acc.insert k v)
.empty
let symtab := CProverGOTO.wrapSymtab symtabObj (moduleName := baseName)
let goto := Lean.Json.mkObj [("functions", Lean.Json.arr gotoFns)]
let symTabFile := s!"{baseName}.symtab.json"
let gotoFile := s!"{baseName}.goto.json"
writeJsonFile symTabFile symtab
writeJsonFile gotoFile goto
IO.println s!"Written {symTabFile} and {gotoFile}"