Skip to content

Commit 98b6916

Browse files
authored
Let --keep-all-files emit all intermediate representations after each pass of Laurel and Core (#840)
This patch makes `--keep-all-files` emit all intermediate representations after each pass of Laurel and Core. :) By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 90372f0 commit 98b6916

9 files changed

Lines changed: 120 additions & 71 deletions

File tree

Strata/Languages/Core/Verifier.lean

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -935,29 +935,47 @@ def verifySingleEnv (pE : Program × Env) (options : VerifyOptions)
935935
/-- Run the Strata Core verification pipeline on a program: transform,
936936
type-check, partially evaluate, and discharge proof obligations via SMT.
937937
All program-wide transformations that occur before any analyses
938-
(including type inference) should be placed here. -/
938+
(including type inference) should be placed here.
939+
940+
When `keepAllFilesPrefix` is provided, the program state after each pipeline
941+
phase is written to `{prefix}.{n}.{phaseName}.core.st` (numbered from 1). -/
939942
def verify (program : Program)
940943
(tempDir : System.FilePath)
941944
(proceduresToVerify : Option (List String) := none)
942945
(options : VerifyOptions := VerifyOptions.default)
943946
(moreFns : @Lambda.Factory CoreLParams := Lambda.Factory.default)
944947
(externalPhases : List AbstractedPhase := [])
945948
(prefixPhases : List PipelinePhase := [])
949+
(keepAllFilesPrefix : Option String := none)
946950
: EIO DiagnosticModel VCResults := do
947951
let profile := options.profile
948952
let factory ← EIO.ofExcept (Core.Factory.addFactory moreFns)
949953
let pipelinePhases := prefixPhases ++ corePipelinePhases (procs := proceduresToVerify) (factory := some factory)
950954
let phases := pipelinePhases.map (·.phase)
951955
let finalProgram ← profileStep profile " Program transformations" do
952-
let passes := fun prog => do
953-
let mut current := prog
954-
for pp in pipelinePhases do
955-
let (_changed, next) ← pp.transform current
956+
if let some pfx := keepAllFilesPrefix then
957+
if let some parent := (System.FilePath.mk pfx).parent then
958+
IO.toEIO (fun e => DiagnosticModel.fromFormat f!"{e}")
959+
(IO.FS.createDirAll parent)
960+
let mut current := program
961+
let mut state : Transform.CoreTransformState := .emp
962+
let mut step := 0
963+
for pp in pipelinePhases do
964+
let (result, newState) := Transform.runWith current (fun prog => do
965+
let (_, next) ← pp.transform prog
966+
return next) state
967+
match result with
968+
| .ok next =>
956969
current := next
957-
return current
958-
match Transform.run program passes with
959-
| .ok prog => .ok prog
960-
| .error e => .error (DiagnosticModel.fromFormat f!"❌ Transform Error. {e}")
970+
state := newState
971+
step := step + 1
972+
if let some pfx := keepAllFilesPrefix then
973+
let path := s!"{pfx}.{step}.{pp.phase.name}.core.st"
974+
IO.toEIO (fun e => DiagnosticModel.fromFormat f!"{e}")
975+
(IO.FS.writeFile path (toString current ++ "\n"))
976+
| .error e =>
977+
throw (DiagnosticModel.fromFormat f!"❌ Transform Error. {e}")
978+
.ok current
961979
-- Build the axiom relevance cache once (post-transform, so declarations are
962980
-- stable). The cache is reused across all verification environments and goals.
963981
let axiomCache? ← profileStep profile " Build axiom relevance cache" do
@@ -992,7 +1010,6 @@ def typeCheck (ictx : InputContext) (env : Program) (options : Core.VerifyOption
9921010
Except DiagnosticModel Core.Program := do
9931011
let (program, errors) := TransM.run ictx (translateProgram env)
9941012
if errors.isEmpty then
995-
-- dbg_trace f!"AST: {program}"
9961013
Core.typeCheck options program moreFns
9971014
else
9981015
.error <| DiagnosticModel.fromFormat s!"DDM Transform Error: {repr errors}"
@@ -1017,13 +1034,15 @@ def verify
10171034
(options : Core.VerifyOptions := Core.VerifyOptions.default)
10181035
(moreFns : @Lambda.Factory Core.CoreLParams := Lambda.Factory.default)
10191036
(externalPhases : List Core.AbstractedPhase := [])
1037+
(keepAllFilesPrefix : Option String := none)
10201038
: IO Core.VCResults := do
10211039
let (program, errors) := Core.getProgram env ictx
10221040
if errors.isEmpty then
10231041
let runner tempDir :=
10241042
EIO.toIO (fun dm => IO.Error.userError (toString (dm.format (some ictx.fileMap))))
10251043
(Core.verify program tempDir proceduresToVerify options moreFns
1026-
(externalPhases := externalPhases))
1044+
(externalPhases := externalPhases)
1045+
(keepAllFilesPrefix := keepAllFilesPrefix))
10271046
match options.vcDirectory with
10281047
| .none =>
10291048
IO.FS.withTempDir runner

Strata/Languages/Laurel/LaurelToCoreTranslator.lean

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -699,51 +699,79 @@ abbrev TranslateResultWithLaurel := (Option Core.Program) × (List DiagnosticMod
699699

700700
/--
701701
Translate Laurel Program to Core Program, also returning the lowered Laurel program.
702+
703+
When `keepAllFilesPrefix` is provided, the program state after each named
704+
Laurel-to-Laurel pass is written to `{prefix}.{n}.{passName}.laurel.st`
705+
(numbered from 1).
702706
-/
703-
def translateWithLaurel (options: LaurelTranslateOptions) (program : Program): TranslateResultWithLaurel :=
707+
def translateWithLaurel (options: LaurelTranslateOptions) (program : Program)
708+
(keepAllFilesPrefix : Option String := none)
709+
: IO TranslateResultWithLaurel := do
704710
let program := { program with
705711
staticProcedures := coreDefinitionsForLaurel.staticProcedures ++ program.staticProcedures
706712
}
713+
if let some pfx := keepAllFilesPrefix then
714+
if let some parent := (System.FilePath.mk pfx).parent then
715+
IO.FS.createDirAll parent
716+
let stepRef ← IO.mkRef (0 : Nat)
717+
let emit (name : String) (p : Program) : IO Unit :=
718+
match keepAllFilesPrefix with
719+
| some pfx => do
720+
let n ← stepRef.modifyGet (fun n => (n, n + 1))
721+
IO.FS.writeFile s!"{pfx}.{n}.{name}.laurel.st"
722+
((formatProgram p).pretty ++ "\n")
723+
| none => pure ()
724+
725+
-- Step 0: the input program before any passes
726+
emit "Initial" program
707727

708-
-- dbg_trace "=== Initial Laurel program ==="
709-
-- dbg_trace (toString (Std.Format.pretty (Std.ToFormat.format program)))
710-
-- dbg_trace "================================="
711728
let result := resolve program
712729
let resolutionErrors: List DiagnosticModel := if options.emitResolutionErrors then result.errors.toList else []
713730
let (program, model) := (result.program, result.model)
731+
emit "Resolve" program
714732
let diamondErrors := validateDiamondFieldAccesses model program
715733

716734
let (program, nonCompositeDiags) := filterNonCompositeModifies model program
735+
emit "FilterNonCompositeModifies" program
717736

718737
let program := heapParameterization model program
719738
let result := resolve program (some model)
720739
let (program, model) := (result.program, result.model)
740+
emit "HeapParameterization" program
721741

722742
let program := typeHierarchyTransform model program
723743
let result := resolve program (some model)
724744
let (program, model) := (result.program, result.model)
745+
emit "TypeHierarchyTransform" program
725746
let (program, modifiesDiags) := modifiesClausesTransform model program
726747
let result := resolve program (some model)
727748
let (program, model) := (result.program, result.model)
728749
let result := resolve program (some model)
729750
let (program, model) := (result.program, result.model)
751+
emit "ModifiesClausesTransform" program
730752
let program := inferHoleTypes model program
753+
emit "InferHoleTypes" program
731754
let program := eliminateHoles program
755+
emit "EliminateHoles" program
732756
let program := desugarShortCircuit model program
757+
emit "DesugarShortCircuit" program
733758
let program := liftExpressionAssignments model program
759+
emit "LiftExpressionAssignments" program
734760
let program := eliminateReturnsInExpressionTransform program
735761
let result := resolve program (some model)
736762
let (program, model) := (result.program, result.model)
763+
emit "EliminateReturns" program
737764

738765
let (program, constrainedTypeDiags) := constrainedTypeElim model program
739766
let result := resolve program (some model)
740767
let (program, model) := (result.program, result.model)
768+
emit "ConstrainedTypeElim" program
741769

742770
let initState : TranslateState := {model := model }
743771
let (coreProgramOption, translateState) := runTranslateM initState (translateLaurelToCore options program)
744772
let allDiagnostics := resolutionErrors ++ diamondErrors ++ nonCompositeDiags ++ modifiesDiags ++ constrainedTypeDiags ++ translateState.diagnostics
745773
let coreProgramOption := if translateState.coreProgramHasSuperfluousErrors then none else coreProgramOption
746-
(coreProgramOption, allDiagnostics, program)
774+
return (coreProgramOption, allDiagnostics, program)
747775
where
748776

749777
/--
@@ -824,17 +852,17 @@ def translateWithLaurel (options: LaurelTranslateOptions) (program : Program): T
824852
/--
825853
Translate Laurel Program to Core Program
826854
-/
827-
def translate (options: LaurelTranslateOptions) (program : Program): TranslateResult :=
828-
let (core, diags, _) := translateWithLaurel options program
829-
(core, diags)
855+
def translate (options: LaurelTranslateOptions) (program : Program): IO TranslateResult := do
856+
let (core, diags, _) translateWithLaurel options program
857+
return (core, diags)
830858

831859
/--
832860
Verify a Laurel program using an SMT solver
833861
-/
834862
def verifyToVcResults (program : Program)
835863
(options : VerifyOptions := .default)
836864
: IO (Option VCResults × List DiagnosticModel) := do
837-
let (coreProgramOption, translateDiags) := translate {} program
865+
let (coreProgramOption, translateDiags) translate {} program
838866

839867
match coreProgramOption with
840868
| some coreProgram =>

Strata/Languages/Python/PySpecPipeline.lean

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -359,18 +359,24 @@ public def splitProcNames (prog : Core.Program)
359359
(preludeNames, userProcNames)
360360

361361
/-- Like `translateCombinedLaurel` but also returns the lowered Laurel program
362-
(after all Laurel-to-Laurel passes, before translation to Core). -/
362+
(after all Laurel-to-Laurel passes, before translation to Core).
363+
364+
When `keepAllFilesPrefix` is provided, the program state after each named
365+
Laurel pass is written to `{prefix}.{n}.{passName}.laurel.st`. -/
363366
public def translateCombinedLaurelWithLowered (combined : Laurel.Program)
364-
: (Option Core.Program × List DiagnosticModel × Laurel.Program) :=
365-
let (coreOption, errors, lowered) := Laurel.translateWithLaurel { inlineFunctionsWhenPossible := true } combined
366-
(coreOption.map appendCorePartOfRuntime, errors, lowered)
367+
(keepAllFilesPrefix : Option String := none)
368+
: IO (Option Core.Program × List DiagnosticModel × Laurel.Program) := do
369+
let (coreOption, errors, lowered) ←
370+
Laurel.translateWithLaurel { inlineFunctionsWhenPossible := true } combined
371+
(keepAllFilesPrefix := keepAllFilesPrefix)
372+
return (coreOption.map appendCorePartOfRuntime, errors, lowered)
367373

368374
/-- Translate a combined Laurel program to Core and prepend the full
369375
runtime prelude. -/
370376
public def translateCombinedLaurel (combined : Laurel.Program)
371-
: (Option Core.Program × List DiagnosticModel) :=
372-
let (coreOption, errors, _) := translateCombinedLaurelWithLowered combined
373-
(coreOption, errors)
377+
: IO (Option Core.Program × List DiagnosticModel) := do
378+
let (coreOption, errors, _) translateCombinedLaurelWithLowered combined
379+
return (coreOption, errors)
374380

375381
/-- Errors from the pyAnalyzeLaurel pipeline. -/
376382
public inductive PipelineError where
@@ -399,7 +405,7 @@ public instance : ToString PipelineError where
399405
The optional `sourcePath` overrides the file path embedded in
400406
Laurel metadata (useful when the Ion file was generated from a
401407
`.py` source and you want line numbers to refer to the original). -/
402-
public def pyAnalyzeLaurel
408+
public def pythonAndSpecToLaurel
403409
(pythonIonPath : String)
404410
(dispatchModules : Array String := #[])
405411
(pyspecModules : Array String := #[])

Strata/SimpleAPI.lean

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,11 @@ dialect into the dialect-specific AST for the Core dialect. This can fail with
233233
an error message if the input program contains constructs that are not yet
234234
supported.
235235
-/
236-
def laurelToCore (p : Laurel.Program) : Except String Core.Program :=
237-
let (coreOpt, diags) := Laurel.translate { emitResolutionErrors := true } p
236+
def laurelToCore (p : Laurel.Program) : IO (Except String Core.Program) := do
237+
let (coreOpt, diags) Laurel.translate { emitResolutionErrors := true } p
238238
match coreOpt with
239-
| some core => .ok core
240-
| none => .error s!"Laurel to Core translation failed: {diags.map (·.message)}"
239+
| some core => return .ok core
240+
| none => return .error s!"Laurel to Core translation failed: {diags.map (·.message)}"
241241

242242
/-! ### Transformation of Core programs -/
243243

@@ -327,10 +327,12 @@ def Core.verifyProgram
327327
(proceduresToVerify : Option (List String) := none)
328328
(externalPhases : List Core.AbstractedPhase := [])
329329
(prefixPhases : List Core.PipelinePhase := [])
330+
(keepAllFilesPrefix : Option String := none)
330331
: EIO String Core.VCResults := do
331332
let runVerification (tempDir : System.FilePath) : IO Core.VCResults :=
332333
EIO.toIO (IO.Error.userError ∘ toString)
333-
(Core.verify program tempDir proceduresToVerify options moreFns externalPhases prefixPhases)
334+
(Core.verify program tempDir proceduresToVerify options moreFns externalPhases prefixPhases
335+
(keepAllFilesPrefix := keepAllFilesPrefix))
334336
let ioAction := match options.vcDirectory with
335337
| .some vcDir => IO.FS.createDirAll vcDir *> runVerification vcDir
336338
| .none => IO.FS.withTempDir runVerification
@@ -539,7 +541,7 @@ def pySpecsDir (sourceDir strataDir dialectFile : System.FilePath)
539541
/-! ### Python-to-Core via Laurel pipeline -/
540542

541543
/-- Translate a Python Ion file all the way to Core. Composes
542-
`pyAnalyzeLaurel` (Python → combined Laurel) and
544+
`pythonAndSpecToLaurel` (Python → combined Laurel) and
543545
`translateCombinedLaurel` (Laurel → Core with prelude). -/
544546
def pyTranslateLaurel
545547
(pythonIonPath : String)
@@ -548,10 +550,10 @@ def pyTranslateLaurel
548550
(specDir : System.FilePath := ".")
549551
: EIO String (Core.Program × List DiagnosticModel) := do
550552
let laurel ←
551-
matchpyAnalyzeLaurel pythonIonPath dispatchModules pyspecModules (specDir := specDir) |>.toBaseIO with
553+
matchpythonAndSpecToLaurel pythonIonPath dispatchModules pyspecModules (specDir := specDir) |>.toBaseIO with
552554
| .ok r => pure r
553555
| .error err => throw (toString err)
554-
let (coreOption, laurelTranslateErrors) := translateCombinedLaurel laurel
556+
let (coreOption, laurelTranslateErrors) ← IO.toEIO (fun e => s!"{e}") (translateCombinedLaurel laurel)
555557
match coreOption with
556558
| none => throw s!"Laurel to Core translation failed: {laurelTranslateErrors}"
557559
| some core => pure (core, laurelTranslateErrors)

StrataMain.lean

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ def pyAnalyzeLaurelCommand : Command where
524524
| some (pyPath, srcText) => some (pyPath, .ofString srcText)
525525
| none => none
526526
let combinedLaurel ←
527-
match ← Strata.pyAnalyzeLaurel filePath dispatchModules pyspecModules sourcePath
527+
match ← Strata.pythonAndSpecToLaurel filePath dispatchModules pyspecModules sourcePath
528528
(specDir := specDir) (profile := profile)
529529
(quiet := quiet) |>.toBaseIO with
530530
| .ok r => pure r
@@ -558,17 +558,12 @@ def pyAnalyzeLaurelCommand : Command where
558558
IO.println "\n==== Laurel Program ===="
559559
IO.println f!"{combinedLaurel}"
560560

561-
if let some dir := keepDir then
562-
let path := s!"{dir}/{baseName}.laurel"
563-
IO.FS.writeFile path ((Laurel.formatProgram combinedLaurel).pretty ++ "\n")
561+
let keepPrefix := keepDir.map (s!"{·}/{baseName}")
564562

565-
let (coreProgramOption, laurelTranslateErrors, loweredLaurel) ←
563+
let (coreProgramOption, laurelTranslateErrors, _loweredLaurel) ←
566564
profileStep profile "Laurel to Core translation" do
567-
pure (Strata.translateCombinedLaurelWithLowered combinedLaurel)
568-
569-
if let some dir := keepDir then
570-
let path := s!"{dir}/{baseName}.lowered.laurel"
571-
IO.FS.writeFile path ((Laurel.formatProgram loweredLaurel).pretty ++ "\n")
565+
Strata.translateCombinedLaurelWithLowered combinedLaurel
566+
(keepAllFilesPrefix := keepPrefix)
572567

573568
let coreProgram ←
574569
match coreProgramOption with
@@ -586,10 +581,6 @@ def pyAnalyzeLaurelCommand : Command where
586581
let (_preludeNames, userProcNames) :=
587582
Strata.splitProcNames coreProgram [userSourcePath]
588583

589-
if let some dir := keepDir then
590-
let path := s!"{dir}/{baseName}.core"
591-
IO.FS.writeFile path (toString coreProgram)
592-
593584
-- Verify using Core verifier
594585
-- --keep-all-files implies vc-directory if not explicitly set
595586
let baseVcDir := keepDir.map (fun dir => (s!"{dir}/{baseName}" : System.FilePath))
@@ -611,7 +602,9 @@ def pyAnalyzeLaurelCommand : Command where
611602
(moreFns := Strata.Python.ReFactory)
612603
(proceduresToVerify := some userProcNames)
613604
(externalPhases := [Strata.frontEndPhase])
614-
(prefixPhases := inlinePhases) |>.toBaseIO with
605+
(prefixPhases := inlinePhases)
606+
(keepAllFilesPrefix := keepPrefix)
607+
|>.toBaseIO with
615608
| .ok r => pure r
616609
| .error msg => exitPyAnalyzeInternalError msg
617610

@@ -883,7 +876,7 @@ def laurelAnalyzeToGotoCommand : Command where
883876
let path : System.FilePath := v[0]
884877
let content ← IO.FS.readFile path
885878
let laurelProgram ← Strata.parseLaurelText path content
886-
match Strata.Laurel.translate {} laurelProgram with
879+
match Strata.Laurel.translate {} laurelProgram with
887880
| (none, diags) => exitFailure s!"Core translation errors: {diags.map (·.message)}"
888881
| (some coreProgram, errors) =>
889882
let Ctx := { Lambda.LContext.default with functions := Core.Factory, knownTypes := Core.KnownTypes }
@@ -1008,7 +1001,7 @@ def laurelToCoreCommand : Command where
10081001
help := "Translate a Laurel source file to Core and print to stdout."
10091002
callback := fun v _ => do
10101003
let laurelProgram ← Strata.readLaurelTextFile v[0]
1011-
let (coreProgramOption, errors) := Strata.Laurel.translate {} laurelProgram
1004+
let (coreProgramOption, errors) Strata.Laurel.translate {} laurelProgram
10121005
if !errors.isEmpty then
10131006
IO.println s!"Core translation errors: {errors.map (·.message)}"
10141007
match coreProgramOption with

StrataTest/Languages/Laurel/Examples/Fundamentals/T8c_BodilessInlining.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ procedure caller() {
3232
#guard_msgs in
3333
#eval show IO String from do
3434
let laurelProg ← Strata.parseLaurelText "test.laurel" laurelSource
35-
let coreProg ← match Strata.laurelToCore laurelProg with
35+
let coreProg ← match Strata.laurelToCore laurelProg with
3636
| .ok p => pure p
3737
| .error e => throw (IO.userError s!"Translation failed: {e}")
3838
let inlined ← match Strata.Core.inlineProcedures coreProg {} with

0 commit comments

Comments
 (0)