Skip to content

Commit 420ba51

Browse files
joehendrixclaude
andcommitted
Refactor pipeline timing: withPhase/withRepeatedPhase combinators, fatal abort via EIO
Replace manual profileStep/startPhase calls with structured withPhase combinator that handles nesting, timing, and profile output. Add withRepeatedPhase for accumulating iteration-level timing (preprocess, smtEncode, solver) without per-iteration overhead. Key changes: - PipelineM is now ReaderT PipelineContext (EIO Unit); fatal messages abort via throw rather than shouldAbortRef polling - Phase simplified to Array String (removed PhaseName/Ord/index) - withPhase saves/restores parent phase and repeated-phases state - withRepeatedPhase accumulates count+duration, flushed as [profile] lines - All output goes to stdout with flush for reliable capture on timeout - pythonAndSpecToLaurel takes PipelineContext directly (not Option) - Verifier.verify/verifySingleEnv accept optional PipelineContext Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent abf311d commit 420ba51

14 files changed

Lines changed: 224 additions & 170 deletions

File tree

Strata/Languages/Core/Verifier.lean

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import Strata.Transform.LoopElim
2121
import Strata.Transform.ANFEncoder
2222
import Strata.Languages.Core.ObligationExtraction
2323
public import Strata.Transform.IrrelevantAxioms
24-
import Strata.Util.Profile
24+
public import Strata.Pipeline.Messages
25+
26+
open Strata.Pipeline (PipelineContext)
2527

2628
---------------------------------------------------------------------
2729

@@ -1021,23 +1023,23 @@ def verifySingleEnv (oblProgram : Program)
10211023
-- irrelevant axiom removal to determine which axioms to prune.
10221024
(axiomProgram : Option Program := .none)
10231025
(externalPhases : List AbstractedPhase := [])
1024-
(corePhases : List AbstractedPhase := coreAbstractedPhases) :
1026+
(corePhases : List AbstractedPhase := coreAbstractedPhases)
1027+
(pipelineCtx : Option PipelineContext := none) :
10251028
EIO DiagnosticModel (VCResults × Statistics) := do
10261029
-- Build SMT encoding context from the obligations program itself
10271030
let E ← EIO.ofExcept (Core.buildEnv options oblProgram moreFns (registerCustomFunctions := true) |>.map (·.1))
10281031
let p := E.program
1029-
let profile := options.profile
1032+
let pctx ← match pipelineCtx with
1033+
| some ctx => pure ctx
1034+
| none => (PipelineContext.create (outputMode := .quiet) : BaseIO _)
1035+
10301036
-- Extract obligations from the obligations program via ObligationExtraction
10311037
let obligations ← match Core.ObligationExtraction.extractObligations oblProgram with
10321038
| .ok obs => pure obs
10331039
| .error e => .error (DiagnosticModel.fromFormat f!"ObligationExtraction error: {e}")
10341040
let mut stats : Statistics := ({} : Statistics)
10351041
|>.increment s!"{Evaluator.Stats.verify_numObligations}" obligations.size
10361042
let mut results := (#[] : VCResults)
1037-
let mut preprocessNs : Nat := 0
1038-
let mut smtEncodeNs : Nat := 0
1039-
let mut solverNs : Nat := 0
1040-
let mut peResolvedCount : Nat := 0
10411043
for obligation in obligations do
10421044
-- Determine which checks to perform based on metadata or check mode/amount
10431045
let (satisfiabilityCheck, validityCheck) :=
@@ -1051,10 +1053,8 @@ def verifySingleEnv (oblProgram : Program)
10511053
| .deductive, _ =>
10521054
if obligation.property.passWhenUnreachable then (false, true) else (true, false)
10531055
| .bugFinding, _ => (true, false)
1054-
let t0 ← IO.monoNanosNow
1055-
let (obligation, peSatResult?, peValResult?) ← preprocessObligation obligation p options satisfiabilityCheck validityCheck axiomCache axiomNames axiomProgram
1056-
let t1 ← IO.monoNanosNow
1057-
preprocessNs := preprocessNs + (t1 - t0)
1056+
let (obligation, peSatResult?, peValResult?) ← pctx.withRepeatedPhase "preprocess" do
1057+
preprocessObligation obligation p options satisfiabilityCheck validityCheck axiomCache axiomNames axiomProgram
10581058
-- If evaluator resolved both checks, we're done, unless we always want to generate SMT queries
10591059
if not options.alwaysGenerateSMT then
10601060
if let (some peSat, some peVal) := (peSatResult?, peValResult?) then
@@ -1070,7 +1070,6 @@ def verifySingleEnv (oblProgram : Program)
10701070
let result : VCResult := { obligation, outcome := .ok outcome, verbose := options.verbose,
10711071
checkLevel := options.checkLevel, checkMode := options.checkMode, lexprModel := [] }
10721072
results := results.push result
1073-
peResolvedCount := peResolvedCount + 1
10741073
if result.isFailure || result.isImplementationError || result.isTimeout then
10751074
if options.verbose >= .debug then
10761075
let prog := f!"\n\n[DEBUG] Evaluated program:\n{Core.formatProgram p}"
@@ -1080,10 +1079,8 @@ def verifySingleEnv (oblProgram : Program)
10801079
-- Need the solver for at least one check
10811080
let needSatCheck := satisfiabilityCheck && peSatResult?.isNone
10821081
let needValCheck := validityCheck && peValResult?.isNone
1083-
let t2 ← IO.monoNanosNow
1084-
let maybeTerms := ProofObligation.toSMTTerms E obligation { SMT.Context.default with uniqueBoundNames := options.uniqueBoundNames } options.useArrayTheory
1085-
let t3 ← IO.monoNanosNow
1086-
smtEncodeNs := smtEncodeNs + (t3 - t2)
1082+
let maybeTerms ← pctx.withRepeatedPhase "smtEncode" do
1083+
pure (ProofObligation.toSMTTerms E obligation { SMT.Context.default with uniqueBoundNames := options.uniqueBoundNames } options.useArrayTheory)
10871084
match maybeTerms with
10881085
| .error err =>
10891086
let result := { obligation,
@@ -1099,12 +1096,10 @@ def verifySingleEnv (oblProgram : Program)
10991096
if options.stopOnFirstError then break
11001097
| .ok (assumptionTerms, varDefs, varDecls, obligationTerm, ctx, encStats) =>
11011098
stats := stats.merge encStats
1102-
let t4IO.monoNanosNow
1103-
let result ← getObligationResult assumptionTerms obligationTerm ctx obligation p options
1099+
let resultpctx.withRepeatedPhase "solver" do
1100+
getObligationResult assumptionTerms obligationTerm ctx obligation p options
11041101
counter tempDir needSatCheck needValCheck (externalPhases ++ corePhases)
11051102
(varDefinitions := varDefs) (varDeclarations := varDecls)
1106-
let t5 ← IO.monoNanosNow
1107-
solverNs := solverNs + (t5 - t4)
11081103
-- Merge evaluator results with solver results
11091104
let result := match result.outcome with
11101105
| .ok solverOutcome =>
@@ -1120,11 +1115,6 @@ def verifySingleEnv (oblProgram : Program)
11201115
let prog := f!"\n\n[DEBUG] Evaluated program:\n{Core.formatProgram p}"
11211116
dbg_trace f!"\n\nResult: {result}\n{prog}"
11221117
if options.stopOnFirstError then break
1123-
if profile then
1124-
let _ ← (IO.println s!"[profile] Preprocess obligations: {nsToMs preprocessNs}ms" |>.toBaseIO)
1125-
let _ ← (IO.println s!"[profile] SMT encoding: {nsToMs smtEncodeNs}ms" |>.toBaseIO)
1126-
let _ ← (IO.println s!"[profile] Solver/file writing: {nsToMs solverNs}ms" |>.toBaseIO)
1127-
let _ ← (IO.println s!"[profile] Obligations: {obligations.size} total, {peResolvedCount} resolved by evaluator" |>.toBaseIO)
11281118
return (results, stats)
11291119

11301120
/-- Run the Strata Core verification pipeline on a program: transform,
@@ -1142,12 +1132,17 @@ def verify (program : Program)
11421132
(externalPhases : List AbstractedPhase := [])
11431133
(prefixPhases : List PipelinePhase := [])
11441134
(keepAllFilesPrefix : Option String := none)
1135+
(pipelineCtx : Option PipelineContext := none)
11451136
: EIO DiagnosticModel VCResults := do
11461137
let profile := options.profile
1138+
let pctx ← match pipelineCtx with
1139+
| some ctx => pure ctx
1140+
| none => (PipelineContext.create (outputMode := .quiet) : BaseIO _)
1141+
11471142
let factory ← EIO.ofExcept (Core.Factory.addFactory moreFns)
11481143
let pipelinePhases := prefixPhases ++ corePipelinePhases (procs := proceduresToVerify) (options := options) (moreFns := moreFns)
11491144
let phases := pipelinePhases.map (·.phase)
1150-
let (oblProgram, pipelineStats) ← profileStep profile " Program transformations" do
1145+
let (oblProgram, pipelineStats) ← pctx.withPhase "programTransformations" do
11511146
if let some pfx := keepAllFilesPrefix then
11521147
if let some parent := (System.FilePath.mk pfx).parent then
11531148
IO.toEIO (fun e => DiagnosticModel.fromFormat f!"{e}")
@@ -1172,23 +1167,17 @@ def verify (program : Program)
11721167
throw e
11731168
.ok (current, state.statistics)
11741169
let allStats := pipelineStats
1175-
-- Extract axiom names from the original program. The oblProgram (output of
1176-
-- toCoreProofObligationProgram) inlines axioms as assume statements but does
1177-
-- not preserve axiom declarations, so we use the pre-transform program for
1178-
-- axiom identity.
11791170
let axiomNames := program.decls.filterMap fun decl =>
11801171
match decl with | .ax a _ => some a.name | _ => none
1181-
-- Build the axiom relevance cache from the original program (which has
1182-
-- axiom declarations). The cache is reused across all obligations.
1183-
let axiomCache? ← profileStep profile " Build axiom relevance cache" do
1172+
let axiomCache? ← pctx.withPhase "buildAxiomCache" do
11841173
pure (if options.removeIrrelevantAxioms == .Off then .none
11851174
else .some (IrrelevantAxioms.Cache.build program))
11861175
let counter ← IO.toEIO (fun e => DiagnosticModel.fromFormat f!"{e}") (IO.mkRef 0)
1187-
let VCss ← profileStep profile " VC discharge" do
1176+
let VCss ← pctx.withPhase "vcDischarge" do
11881177
if options.checkOnly then
11891178
pure []
11901179
else
1191-
pure [← verifySingleEnv oblProgram moreFns options counter tempDir axiomCache? axiomNames (axiomProgram := program) externalPhases phases]
1180+
pure [← verifySingleEnv oblProgram moreFns options counter tempDir axiomCache? axiomNames (axiomProgram := program) externalPhases phases (pipelineCtx := pipelineCtx)]
11921181
let allStats := VCss.foldl (fun acc (_, s) => acc.merge s) allStats
11931182
if profile then
11941183
let _ ← (IO.println allStats.format |>.toBaseIO)

Strata/Languages/Laurel/LaurelCompilationPipeline.lean

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import Strata.Languages.Laurel.EliminateValueReturns
1212
import Strata.Languages.Laurel.ConstrainedTypeElim
1313
import Strata.Languages.Laurel.TypeAliasElim
1414
import Strata.Languages.Core.Verifier
15-
import Strata.Util.Profile
1615
import Strata.Util.Statistics
1716

1817
/-!
@@ -144,7 +143,8 @@ When `keepAllFilesPrefix` is provided (via the `PipelineM` context), the
144143
program state after each named Laurel pass is written to
145144
`{prefix}.{n}.{passName}.laurel.st`.
146145
-/
147-
private def runLaurelPasses (options : LaurelTranslateOptions) (program : Program)
146+
private def runLaurelPasses (options : LaurelTranslateOptions)
147+
(pctx : Strata.Pipeline.PipelineContext) (program : Program)
148148
: PipelineM (Program × SemanticModel × List DiagnosticModel × Statistics) := do
149149
let program := { program with
150150
staticProcedures := coreDefinitionsForLaurel.staticProcedures ++ program.staticProcedures,
@@ -172,8 +172,7 @@ private def runLaurelPasses (options : LaurelTranslateOptions) (program : Progra
172172
let mut allStats : Statistics := {}
173173

174174
for pass in laurelPipeline do
175-
let (program', diags, stats) ← profileStep options.profile s!" {pass.name}" do
176-
pure (pass.run program model)
175+
let (program', diags, stats) ← pctx.withPhase pass.name do pure (pass.run program model)
177176
program := program'
178177
allDiags := allDiags ++ diags
179178
allStats := allStats.merge stats
@@ -193,9 +192,13 @@ When `keepAllFilesPrefix` is provided, the program state after each named
193192
Laurel-to-Laurel pass is written to `{prefix}.{n}.{passName}.laurel.st`.
194193
-/
195194
def translateWithLaurel (options : LaurelTranslateOptions) (program : Program)
196-
: IO TranslateResultWithLaurel :=
195+
(pipelineCtx : Option Strata.Pipeline.PipelineContext := none)
196+
: IO TranslateResultWithLaurel := do
197+
let pctx ← match pipelineCtx with
198+
| some ctx => pure ctx
199+
| none => Strata.Pipeline.PipelineContext.create (outputMode := .quiet)
197200
runPipelineM options.keepAllFilesPrefix do
198-
let (program, model, passDiags, stats) ← runLaurelPasses options program
201+
let (program, model, passDiags, stats) ← runLaurelPasses options pctx program
199202
let ordered := orderProgram program
200203

201204
-- This early return is a simple way to protect against duplicative errors. Without this return,

Strata/Languages/Laurel/LaurelToCoreTranslator.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public import Strata.Languages.Laurel.CoreDefinitionsForLaurel
2525
public import Strata.Languages.Laurel.CoreGroupingAndOrdering
2626
import Strata.DDM.Util.DecimalRat
2727
import Strata.DL.Imperative.Stmt
28+
import Strata.Pipeline.Messages
2829
import Strata.DL.Imperative.MetaData
2930
import Strata.DL.Lambda.LExpr
3031
import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator

Strata/Languages/Python/PySpecPipeline.lean

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public import Strata.Pipeline.Messages
1818
import Strata.Languages.Python.Specs.IdentifyOverloads
1919
import Strata.Languages.Python.Specs.ToLaurel
2020
import Strata.Util.DecideProp
21-
import Strata.Util.Profile
2221
import all Strata.DDM.Util.String
2322

2423
/-! ## PySpec Pipeline
@@ -45,37 +44,34 @@ public structure PySpecLaurelResult where
4544
exhaustiveClasses : Std.HashSet String := {}
4645
deriving Inhabited
4746

48-
/-- A default pipeline context for sub-pipelines that don't need output. -/
49-
private def defaultPipelineContext : BaseIO Pipeline.PipelineContext :=
50-
Pipeline.PipelineContext.create (outputMode := .quiet)
51-
5247
/-- Emit a pipeline message. Tags with the current phase from state.
53-
Sets `shouldAbort` if the kind's impact is fatal.
48+
Throws `()` if the kind's impact is fatal, aborting the pipeline.
5449
In verbose mode, prints the message immediately to stderr. -/
5550
public def emitMessage (kind : Pipeline.MessageKind) (message : String)
5651
(file : System.FilePath := default) (loc : SourceRange := default) : Pipeline.PipelineM Unit := do
5752
let ctx ← read
5853
let phase ← ctx.currentPhaseRef.get
5954
ctx.messagesRef.modify (·.push { file, loc, phase, kind, message })
60-
if kind.impact.isFatal then
61-
ctx.shouldAbortRef.set true
6255
if ctx.outputMode == .verbose then
6356
let tag := if kind.impact.isFatal then "error" else "warning"
6457
let indent := String.replicate ((phase.depth - 1) * 2) ' '
6558
let _ ← (do IO.eprintln s!"{indent}[{tag}] {file}: {message}"; (← IO.getStderr).flush : IO Unit).toBaseIO
59+
if kind.impact.isFatal then
60+
throw ()
6661

6762
/-- Append a batch of messages to the pipeline state.
63+
Throws `()` if any message has fatal impact.
6864
In verbose mode, prints each message immediately to stderr. -/
6965
public def addMessages (msgs : Array Pipeline.PipelineMessage) : Pipeline.PipelineM Unit := do
7066
let ctx ← read
7167
ctx.messagesRef.modify (· ++ msgs)
72-
if msgs.any (·.kind.impact.isFatal) then
73-
ctx.shouldAbortRef.set true
7468
if ctx.outputMode == .verbose then
7569
for msg in msgs do
7670
let tag := if msg.kind.impact.isFatal then "error" else "warning"
7771
let indent := String.replicate ((msg.phase.depth - 1) * 2) ' '
7872
let _ ← (do IO.eprintln s!"{indent}[{tag}] {msg.file}: {msg.message}"; (← IO.getStderr).flush : IO Unit).toBaseIO
73+
if msgs.any (·.kind.impact.isFatal) then
74+
throw ()
7975

8076
/-! ### Private Helpers -/
8177

@@ -237,7 +233,7 @@ private def buildPySpecLaurelM (pyspecEntries : Array (String × String))
237233
public def buildPySpecLaurel
238234
(ctx : Pipeline.PipelineContext)
239235
(pyspecEntries : Array (String × String))
240-
(overloads : OverloadTable) : BaseIO PySpecLaurelResult :=
236+
(overloads : OverloadTable) : EIO Unit PySpecLaurelResult :=
241237
buildPySpecLaurelM pyspecEntries overloads |>.run ctx
242238

243239
/-- Read dispatch Ion files and merge their overload tables. -/
@@ -260,7 +256,7 @@ private def readDispatchOverloadsM
260256
/-- Read dispatch Ion files and merge their overload tables. -/
261257
public def readDispatchOverloads
262258
(ctx : Pipeline.PipelineContext)
263-
(dispatchPaths : Array String) : BaseIO OverloadTable :=
259+
(dispatchPaths : Array String) : EIO Unit OverloadTable :=
264260
readDispatchOverloadsM dispatchPaths |>.run ctx
265261

266262
/-- Resolve a module name to a `(modulePrefix, ionPath)` pair for
@@ -434,9 +430,11 @@ public def splitProcNames (prog : Core.Program)
434430
public def translateCombinedLaurelWithLowered (combined : Laurel.Program)
435431
(keepAllFilesPrefix : Option String := none)
436432
(profile : Bool := false)
433+
(pipelineCtx : Option Pipeline.PipelineContext := none)
437434
: IO (Option Core.Program × List DiagnosticModel × Laurel.Program × Statistics) := do
438435
let (coreOption, errors, lowered, stats) ←
439-
Laurel.translateWithLaurel { inlineFunctionsWhenPossible := true, keepAllFilesPrefix, profile } combined
436+
Laurel.translateWithLaurel { inlineFunctionsWhenPossible := true, keepAllFilesPrefix, profile }
437+
combined (pipelineCtx := pipelineCtx)
440438
return (coreOption.map appendCorePartOfRuntime, errors, lowered, stats)
441439

442440
/-- Translate a combined Laurel program to Core and prepend the full
@@ -534,22 +532,22 @@ public def pythonAndSpecToLaurel
534532
(pyspecModules : Array String := #[])
535533
(sourcePath : Option String := none)
536534
(specDir : System.FilePath := ".")
537-
(profile : Bool := false)
535+
(pipelineCtx : Pipeline.PipelineContext)
538536
: BaseIO PythonToLaurelResult := do
539537
let stmts ←
540-
matchprofileStep profile "Read Python Ion"
538+
matchpipelineCtx.withPhase "readPythonIon"
541539
(Python.readPythonStrata pythonIonPath |>.toBaseIO) with
542540
| .ok r => pure r
543541
| .error msg => return .failure (.internal msg) #[]
544542

545-
let ctx ← defaultPipelineContext
546-
let result ←
547-
profileStep profile "Resolve and build Laurel prelude" $
548-
resolveAndBuildLaurelPrelude dispatchModules pyspecModules stmts specDir |>.run ctx
549-
let pyspecWarnings ← ctx.messagesRef.get
543+
let resultOrAbort ←
544+
pipelineCtx.withPhase "resolveAndBuildPrelude"
545+
(resolveAndBuildLaurelPrelude dispatchModules pyspecModules stmts specDir |>.run pipelineCtx).toBaseIO
546+
let pyspecWarnings ← pipelineCtx.messagesRef.get
550547

551-
if ← ctx.shouldAbortRef.get then
552-
return .failure (.internal "Pipeline aborted due to fatal errors") pyspecWarnings
548+
let result ← match resultOrAbort with
549+
| .error () => return .failure (.internal "Pipeline aborted due to fatal errors") pyspecWarnings
550+
| .ok r => pure r
553551

554552
let preludeInfo := buildPreludeInfo result
555553
let metadataPath := sourcePath.getD pythonIonPath

Strata/Languages/Python/Specs.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ def shouldSkip (name : String) : PySpecM Bool := do
356356
let nameIdent := { pythonModule := toString ctx.currentModule, name }
357357
return nameIdent ∈ ctx.skipNames
358358

359-
private def pySpecParsingPhase : Phase := Phase.base "pySpecParsing" 1
359+
private def pySpecParsingPhase : Phase := Phase.base "pySpecParsing"
360360

361361
def specErrorAt (file : System.FilePath) (loc : SourceRange) (message : String) : PySpecM Unit := do
362362
let e : PipelineMessage := { file, loc, phase := pySpecParsingPhase, kind := .pySpecParsingError, message }

Strata/Languages/Python/Specs/ToLaurel.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ abbrev ToLaurelM := ReaderT ToLaurelContext (StateM ToLaurelState)
9090
/-- Report an error during translation. Phase is set to pySpecToLaurel since
9191
this monad always runs during that phase. -/
9292
def reportError (kind : MessageKind) (loc : SourceRange) (message : String) : ToLaurelM Unit := do
93-
let phase := Phase.base "pySpecToLaurel" 2
93+
let phase := Phase.base "pySpecToLaurel"
9494
let e : PipelineMessage := ⟨(←read).filepath, loc, phase, kind, message⟩
9595
modify fun s => { s with errors := s.errors.push e }
9696

0 commit comments

Comments
 (0)