Add Concrete Syntax for Unstructured Programs in Strata Core#1132
Add Concrete Syntax for Unstructured Programs in Strata Core#1132PROgram52bc wants to merge 54 commits into
Conversation
Address PR review comments: note that the GOTO backend could be refactored into a two-stage pipeline (structured→cfg, then cfg→GOTO) to eliminate the pattern matching on Procedure.Body.
- Add Body.getCfg accessor (mirrors getStructured) - CoreToCBMC: throw error on CFG body instead of returning [] - StatementEval: interpret CFG bodies by linearizing blocks - Grammar: add comment explaining why 'branch' is used instead of 'if' (DDM registers tokens globally, causing conflict with if-statement)
… with CFG as body
tautschnig
left a comment
There was a problem hiding this comment.
Two things that are classified as "CFG (deferred)" / "Proof (deferred)" in the table, but whose current code shape is load-bearing for what downstream clients currently believe about verification results — I think both deserve a more visible marker before merge, because the tool will silently accept CFG procedures with neither an error nor a warning that "your CFG procedure was not verified":
1. ProcedureCorrect.postconditionsValid is vacuously true for CFG procedures. At CoreSpecification.lean:198-201, the match
CoreStepStar π φ
(.stmts (match proc.body with | .structured ss => ss | .cfg _ => []) ρ₀)
(.terminal ρ')yields CoreStepStar π φ (.stmts [] ρ₀) (.terminal ρ') for CFG procedures. That derivation forces ρ' = ρ₀ immediately, so the conclusion
(∀ label check, … → ρ'.eval ρ'.store check.expr = some HasBool.tt) ∧
ρ'.hasFailure = Bool.falsebecomes "postconditions hold on ρ₀ and ρ₀.hasFailure = false" — which is just a restatement of ProcEnvWF plus the no-failure hypothesis. Nothing about the CFG body was actually verified.
AssertValidInProcedure is correctly dispatched to Lang.coreCFG for CFG bodies (great — that came out of thread 25), so asserts inside the CFG are covered. But postconditions on the procedure boundary are not, and the stacked vacuity (plus WFProcedureProp being unconstructible for CFG per thread 38) makes this hole invisible to anyone who reads only ProcedureCorrect's signature and assumes it means what it says for both body kinds.
Concrete asks (details inline):
- Introduce
ProcedureCorrect.postconditionsValidspecifically for CFG viaCoreBodyExec/CoreCFGStepStar, or - Add an explicit
postconditionsValid_CFG_is_vacuoustheorem as a stability marker so that the gap shows up at proof-development time if someone adds CFG-specific reasoning elsewhere, or - At minimum, split the current
postconditionsValidinto a_structuredand_cfg := Trueso a future CFG postcondition proof lands in an unambiguous slot rather than overwriting the old one.
2. runCFG silently returns the current env on a non-terminal interpreter result. At StatementEval.lean:791 the final | _ => env arm catches the case where Imperative.runStmt … (.stmts cmdStmts env) returns .stmts … (remaining statements, i.e., inner fuel exhausted or not-fully-reducible). That silently discards the computation. Thread 36 (fuel exhaustion) was addressed only for the outer runCFG fuel; the inner runStmt's incomplete-reduction path still falls through. Match needs to propagate the error. Inline.
Other smaller items:
extractFromDetCFGresets path conditions per block (thread 37, marked resolved-with-TODO). The current comment says "conservative (no false negatives)". That's true in the VC-generation sense (no assert is dropped), but it does produce false alarms:assume x > 0in block A followed bygoto Bdoes not constrain the verifier in B, so an obligation in B that needsx > 0will fail to discharge. The comment is slightly misleading — flag the user-visible consequence.- Examples don't have end-to-end verification tests.
Examples/CFGSimple.core.st/Examples/CFGNondet.core.stonly appear inCFGParseTests.lean(parse-shape checks). SinceprocToVerifyStmtthrows on CFG, they cannot bestrata verify-ed today. Worth a#guard_msgsfixture that confirmsstrata verify <cfg example>produces a clean diagnostic ("CFG verification not yet supported") rather than silently succeeding or crashing — this pins down the current state and flags when the deferred work lands. CoreBodyExecis partially dead. Introduced as the unifier for body execution, used inEvalCommand.call_sem(line 398 of StatementSemantics.lean), but not used inpostconditionsValid. If the plan is to routepostconditionsValidthrough it once theevalaccessibility obstacle is resolved (per the TODO at CoreSpecification.lean:187-193), consider filing a tracking issue so the API isn't left half-wired..match proc.body with | .structured ss => ss | .cfg _ => []pattern appears in at least three places.CoreSpecification.lean:199,ProcBodyVerifyCorrect.lean:661-662(the newh_body_match),Core.lean:98-102. AnProcedure.Body.structuredOrEmpty : Body → List Statementhelper would centralize the semantics-of-CFG-as-empty and make the vacuity visible at one source location. Trade-off: it also makes the vacuity easier to miss. I'd lean toward extracting + adding a short docstring saying "returns[]for CFG; this is the semantics-of-absence used in several structured-only passes pending proper CFG handling."
Proof-coverage suggestions:
Three small theorems that would make the soundness gap (and any future closing of it) explicit:
/-- CFG procedures satisfy `postconditionsValid` trivially because the
structured-only predicate is instantiated on an empty statement list.
This theorem is **not** a progress statement — it is a stability marker
for the current (incomplete) verification. See TODO at :187-193. -/
theorem postconditionsValid_CFG_is_vacuous
{π : String → Option Procedure}
{φ : CoreEval → PureFunc Expression → CoreEval}
(proc : Procedure) (_ : proc.body.isCfg)
(p : Program)
(h_wf : WF.WFProcedureProp p proc)
(ρ₀ ρ' : Env Expression)
(h_env : ProcEnvWF proc ρ₀)
(h_step : CoreStepStar π φ
(.stmts (match proc.body with | .structured ss => ss | .cfg _ => []) ρ₀)
(.terminal ρ')) :
ρ' = ρ₀ := by
cases hb : proc.body with
| structured => simp [Procedure.Body.isCfg, hb] at *
| cfg _ => simp [hb] at h_step; …/-- Round-trip for `DetCFG.stripMetaData`: semantics is unchanged by stripping. -/
theorem DetCFG.stripMetaData_preserves_eval (π φ cfg σ δ σ' f) :
CoreCFGStepStar π φ cfg (.cont cfg.entry σ false) (.terminal σ' f) ↔
CoreCFGStepStar π φ cfg.stripMetaData (.cont cfg.stripMetaData.entry σ false)
(.terminal σ' f)/-- `transformDetCFG` is a no-op on the CFG shape (only inserts assert
commands, never changes control flow). -/
theorem transformDetCFG_preserves_entry_and_blocks (F cfg) :
let (_, cfg') := transformDetCFG F cfg
cfg'.entry = cfg.entry ∧
cfg'.blocks.map (·.1) = cfg.blocks.map (·.1)All three are simp / rfl-shaped once the constructors unfold. The first makes the current soundness hole provable-as-a-lemma (and hence impossible to forget when someone later extends the verifier).
Refactor callouts:
CoreEvalCmds(StatementSemantics.lean:320-328) duplicates the list-of-commands fold thatEvalCommanddoes statement-by-statement. Might be worth a generalCoreEvalCmds_of_EvalCommandlemma so that properties ofEvalCommandlift automatically.- The CFG-related instances on
Procedure.Body(HasVarsImp,HasVarsPure) were split so that the genericHasVarsImp Expression DetCFGlives next to theProcedure.Bodyinstance inProcedure.lean. Thread 26/27 asked forDetCFG.eraseTypes/stripMetaDatainBasicBlock.lean; the generic parts moved butDetCFG.eraseTypesremained inProcedure.lean(with the reason: dependency onCommand.eraseTypes). Consider an intermediate fileStrata/Languages/Core/DetCFG.leanso the Core-flavoured but generic-over-statement parts live together.
…action comment StatementEval.lean: When Imperative.runStmt returns a non-terminal result (e.g. from inner fuel exhaustion), the catch-all arm silently returned the input environment, discarding all block side effects with no diagnostic. Replace | _ => env with CmdEval.updateError using the .Misc pattern. ObligationExtraction.lean: Rewrite the doc comment on extractFromDetCFG to clarify that path-condition restart causes false alarms (false positives), not just imprecision, and reference dominator-based propagation as follow-up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test 1 — Type-checking rejection: confirms Core.typeCheck rejects a CFG procedure with "expected structured body, got CFG" diagnostic. Test 2 — CFG body preservation: confirms a parsed CFG procedure retains .cfg body (isCfg=true, isStructured=false) with correct entry label and block count, guarding against the prior bug where type-checking collapsed CFG bodies to .structured []. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… type TerminationCheck.lean (from main) constructs a Procedure with body := stmts, but this branch changed Procedure.body from List Statement to Procedure.Body. Wrap with .structured to fix the merge-queue CI build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eBodyExec TODOs The postconditionsValid field in ProcedureCorrect was vacuously true for CFG procedures — the match expression produced [] for CFGs, making CoreStepStar terminate immediately with rho' = rho0. Split into two explicit fields: - postconditionsValid_structured: for structured bodies, uses CoreStepStar on the statement list (preserves existing proven behavior) - postconditionsValid_cfg: for CFG bodies, uses CoreCFGStepStar directly, making the soundness gap explicit and non-vacuous Updated procBodyVerify_procedureCorrect proof to construct all three fields. The CFG field is discharged by contradiction since procToVerifyStmt only succeeds for structured bodies. Added TODO comments on CoreBodyExec documenting: - Should be wired into postconditionsValid_cfg - The cfg constructor drops terminal eval (limiting postcondition support) - An equivalence theorem structured_iff_CoreStepStar would bridge the two representations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ionsValid Add a δ' : CoreEval output parameter to CoreBodyExec so it exposes the terminal evaluator. The structured constructor outputs ρ'.eval; the cfg constructor passes through the initial δ (since CoreCFGStepStar does not track eval changes). This enables unifying postconditionsValid_structured and postconditionsValid_cfg back into a single postconditionsValid field that uses CoreBodyExec to abstract over both body kinds. Updated call_sem to use the terminal eval (δ_final) from CoreBodyExec when checking postconditions, and updated procBodyVerify_procedureCorrect proof to invert the CoreBodyExec.structured constructor and reconcile the initial environment via ProcEnvWF.noFailure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
structural checks (unique labels, entry exists, valid targets) and expression-level type-checking.
- Uses a fuel-measure and throws error on running out of fuel - No path-merging - Added #guard_msg tests 1. Trivial CFG (single finish block) 2. Linear CFG (assignment + goto + postcondition holds) 3. Missing block error 4. Fuel exhaustion on loop back-edge 5. Postcondition failure (non-trivial proof obligation) 6. Diamond CFG with symbolic branch (two paths, two proof obligations with path conditions)
During structured-to-CFG lowering, loop invariants and decreases measures were only preserved as lowered assert commands, losing the connection to the original spec. Downstream CFG passes that need to recover the contract (e.g., for invariant inference or refinement) had no way to distinguish spec-level asserts from user asserts. Add `MetaData.specLoopInvariant` and `MetaData.specDecreases` fields and attach them to the loop entry block's transfer command metadata. The existing assert lowering is preserved (both representations coexist), so this is a non-breaking addition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The grammar previously accepted `goto label1, label2, ..., labelN;` via CommaSepBy, but the translator rejected >2 targets at translation time. This meant users could write syntactically valid programs that failed during elaboration with a confusing error. Split into two grammar ops: - `transfer_goto (label : Ident)` — unconditional goto (1 target) - `transfer_nondet_goto (label1 : Ident, label2 : Ident)` — nondet (2 targets) The concrete syntax is unchanged: `goto a;` and `goto a, b;` parse as before. `goto a, b, c;` now fails at parse time with a clear syntax error instead of passing through to translation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
translateTransfer now increments the var_def counter after reading it for the $__nondet_N variable name, so multiple nondeterministic gotos in the same procedure get distinct names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 5 instances of the "match body with structured/cfg-error" pattern with the existing getStructured helper, which was defined but unused. Uses .mapError where the caller's error type differs from Except String. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `procedureToGotoCtxViaCFG`, a parallel pipeline that translates Core
procedures to CProver GOTO via the CFG representation:
Structured body → stmtsToCFG → coreCFGToGotoTransform → GOTO
CFG body → coreCFGToGotoTransform → GOTO
This coexists with the existing direct path in `procedureToGotoCtx`, which
remains unchanged.
Changes:
1. CFGToCProverGOTO.lean — close two gaps in `detCFGToGotoTransform`:
- Source locations: transfer command metadata is now used to derive
source locations via `metadataToSourceLoc` (previously ignored as
`_md`).
- Loop contracts: backward-edge GOTOs targeting loop entry blocks are
annotated with `#spec_loop_invariant` and `#spec_decreases` named
fields on the guard, matching CBMC's DFCC expectations. A post-
processing pass detects loop entries (by presence of contract
metadata on their condGoto transfer) and annotates GOTOs whose
target location ≤ source location (i.e., backward edges).
2. CoreToGOTOPipeline.lean — make `renameIdent`, `renameExpr`,
`renameCmd`, and `collectFuncDecls` non-private so the new pipeline
can reuse them.
3. CoreCFGToGOTOPipeline.lean (new) — contains:
- Rename helpers for Core commands (`CmdExt`): `renameCoreCommand`,
`renameCoreStmt`, `renameCoreDetCFG`.
- `coreCFGToGotoTransform`: Core-specific CFG-to-GOTO translation
that handles `CmdExt.call` (emits FUNCTION_CALL instructions) and
delegates `CmdExt.cmd` to `Cmd.toGotoInstructions`. Also handles
source locations and loop contract annotation.
- `procedureToGotoCtxViaCFG`: full pipeline wrapper mirroring
`procedureToGotoCtx` — renaming, type environment, axioms,
distinct declarations, contracts, lifted functions — but routing
through `stmtsToCFG` + `coreCFGToGotoTransform`.
4. E2E_CFGPipeline.lean (new) — 11 equivalence tests that run both
pipelines on the same Core programs and compare:
- Semantic instruction types (DECL, ASSIGN, ASSERT, ASSUME, etc.)
match between direct and CFG paths.
- Contract annotations (#spec_requires, #spec_ensures) match.
- Both paths produce valid, non-null JSON output.
Test programs cover: simple assert, var decl/assign, if-then-else,
contracts, axioms/distinct, free specs, cover, bitvector ops,
assume, multiple commands, and CFG-only output validation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Description of changes:
Prior to this PR, the only way to obtain unstructured programs was to apply
StructuredToUnstructuredtransformation..core.stfile examples illustrating the syntax of unstructured programs.Procedure.bodyto handle thecfgcase based on the contextAdaptation Methods for
Procedure.bodyUsespipelines, graph-level inlining).
Procedure.Bodysum type.(Bolded items are updates since the initial PR draft)
postconditionsValidadapted withCoreBodyExecTransform/CoreSpecification.leanProcedureCorrect.postconditionsValidTransform/ProcBodyVerifyCorrect.leanprocToVerifyStmt_is_structured(new helper theorem)sswitnesses viasubstTransform/ProcBodyVerifyCorrect.leanprocBodyVerify_procedureCorrectLanguages/Core/ObligationExtraction.leanextractGo_okproofLanguages/Core/ProcedureEval.leanevalLanguages/Core/ProcedureType.leantypeCheck,checkModificationRightsextractCallsFromStatementsvsextractCallsFromDetCFGLanguages/Core/CallGraph.leanextractCallsFromProcedureblockToCSTvsdetCFGToCSTLanguages/Core/DDMTransform/FormatCore.leanprocToCSTrunStmtvsrunCFGLanguages/Core/StatementEval.leanCommand.runCallCoreBodyExecwith two constructors, andCoreStepStarvsCoreCFGStepStarLanguages/Core/StatementSemantics.leanEvalCommand.call_semLang.corevsLang.coreCFGTransform/CoreSpecification.leanAssertValidInProceduretransformStmtsvstransformDetCFGTransform/PrecondElim.leanprecondElimeraseTypes,stripMetaData,getVarsimplemented and used for CFGLanguages/Core/Procedure.leaneraseTypes,stripMetaData,getVarsextractFromStatementsvsextractFromDetCFGLanguages/Core/ObligationExtraction.leanextractObligationsreturn .noneon CFG, skip inlining, because the inlined procedure could be structured or unstructured. Semantics is largely undefined.Transform/ProcedureInlining.leaninlineCallCmdstmtsToCFGvs identity, latter is a no-op.StrataTest/.../Loops.leansingleCFGTransform/LoopElim.leanloopElim[]for CFG (no local funcDecls in CFG bodies)Languages/Core/Core.leanbuildEnvBackends/CBMC/GOTO/CoreToCProverGOTO.leantransformToGotoBackends/CBMC/GOTO/CoreToGOTOPipeline.leanprocedureToGotoCtx.erroron CFG, Two-stage pipeline for GOTOStrataTest/.../E2E_CoreToGOTO.leancoreToGotoJsonWithSummary.erroron CFG, left unimplemented for two-stage pipelineBackends/CBMC/CoreToCBMC.leancreateImplementationSymbolFromASTLanguages/Core/ObligationExtraction.leanextractFromDetCFGthrowon CFG, due to incompatible return type ofList Statementinstead ofList CommandTransform/CoreTransform.leanrunProgramthrowon CFG, VC for CFG bodies to be implemented, proof to be adapted as well.Transform/ProcBodyVerify.leanprocToVerifyStmtTransform/ANFEncoder.leananfEncodeProgramTransform/ProcedureInlining.leanrenameAllLocalNamesStrataTest/Boole/global_readonly_call.leancallHelperwfstmts,wfloclnd,bodyExitsCoveredconditioned on structured procedures, props for CFG to be implementedLanguages/Core/WF.leanWFProcedurePropBy submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.