Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
12e7e3e
add CFG to procedure body, no error handling, tests pass
May 5, 2026
cbeee72
eliminate toStmts and use pattern matching/error handling
May 5, 2026
723b1ae
add parser support for user-facing syntax with examples and tests
May 6, 2026
602741c
Add TODO comments for two-stage GOTO pipeline refactor
May 6, 2026
dbb6f91
add DetCFG abbrev, add obligation extraction and call extraction for …
May 6, 2026
39123b0
refactor ObligationExtraction
May 6, 2026
814530e
Address PR review: getCfg, CBMC error, CFG eval, grammar comment
May 6, 2026
051f976
integrate semantics for Call statements which could invoke procedures…
May 6, 2026
48a2a32
CST support for unstructured programs
May 6, 2026
5d3aba6
add explicit comments on funcDecl extraction for unstructured program
May 6, 2026
1b8a0e5
add ANF comment
May 6, 2026
6c7a61a
alphaEquiv for CFG
May 6, 2026
4ccddf4
no-op when inlining unstructured procedures
May 6, 2026
b295843
add more support for CFG handling
May 6, 2026
d184ea0
simplify StatementEval
May 6, 2026
70cda23
indent
May 6, 2026
10cb1eb
Address PR review: remove .stmts, refine Body interface, cleanup
May 6, 2026
58695e5
Regenerate editor syntax files for new CFG keywords (goto, cfg)
May 7, 2026
39acfc8
StatementEval returns error on nonexisting label
May 7, 2026
15cfcf0
Refine unstructured procedure cases, add comments
May 7, 2026
9d1de97
define variable infrastructures
May 7, 2026
7e20805
refactor
May 7, 2026
03c940e
Precond Elim for CFGs
May 7, 2026
eba79e4
add error branch for renameAllLocalNames in ProcedureInlining
May 7, 2026
c993332
intermediate proof fix
May 7, 2026
e6d71f7
fix proof
May 7, 2026
e5c4793
comment
May 7, 2026
3bfd467
fix WFProcedure
May 7, 2026
6d937fc
restore comments
May 7, 2026
9eb49ab
run out of fuel warning
May 7, 2026
402e43d
better comments on WF
May 7, 2026
18b41d8
error on goto with 2+ targets
May 7, 2026
d694805
refine type checker for CFG
May 7, 2026
21a86aa
simplify
May 7, 2026
09973f6
refine comments
May 7, 2026
4305a67
fix: propagate error in runCFG fallthrough and clarify ObligationExtr…
May 11, 2026
035fa8b
test: add end-to-end CFG verification and body preservation tests
May 11, 2026
74a0977
fix: wrap Procedure.body assignment in TerminationCheck.lean for Body…
May 11, 2026
8f0939d
fix: split postconditionsValid into structured/CFG fields and add Cor…
May 12, 2026
8e944c7
refactor: enrich CoreBodyExec with terminal eval and unify postcondit…
May 12, 2026
7e5884f
Merge branch 'main' into htd/unstructured-procedure
PROgram52bc May 12, 2026
cf067e8
implement type checking for CFG
May 12, 2026
39afe3e
Implement symbolic executor for CFG
May 12, 2026
c1299b8
abstract collectCmds
May 12, 2026
b9334dc
add more documentation/better abstraction
May 12, 2026
4d29cde
add metadata to unstructured programs
May 8, 2026
6f2aca0
feat: preserve loop contracts in CFG transfer metadata
May 12, 2026
6d2582c
fix: split transfer_goto grammar to enforce 1 or 2 targets at parse time
May 12, 2026
11f11eb
fix: increment var_def counter when generating nondet goto fvar
May 12, 2026
7f0d3b9
Fix binding bug in CFG Parser
May 12, 2026
12b0295
fix comments
May 12, 2026
b0640fd
refactor: use Procedure.Body.getStructured instead of raw match
May 12, 2026
b9bb6bb
simplify CFG Eval
May 13, 2026
63181c9
feat: add CFG-based Core-to-GOTO pipeline alongside direct path
May 13, 2026
891d1c4
fix comments
May 14, 2026
86e4600
Elimiate unnecessary constructor
May 14, 2026
4759712
Htd/unstructured procedure experiment (#1169)
PROgram52bc May 15, 2026
e529f8b
Merge branch 'main' into htd/unstructured-procedure
May 15, 2026
0b4c836
Merge branch 'main' into htd/unstructured-procedure
May 18, 2026
6b3c065
bump maxRecDepth
May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Examples/CFGNondet.core.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
program Core;

// Nondeterministic CFG: y is set to either 1 or 2.
procedure NondetChoice(out y : int)
spec {
ensures (y == 1 || y == 2);
}
cfg entry {
entry: {
goto left, right;
}
left: {
y := 1;
goto done;
}
right: {
y := 2;
goto done;
}
done: {
return;
}
};
48 changes: 48 additions & 0 deletions Examples/CFGSimple.core.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
program Core;

// A simple deterministic CFG: compute max of two integers.
procedure Max(x : int, y : int, out m : int)
spec {
ensures (m >= x);
ensures (m >= y);
ensures (m == x || m == y);
}
cfg entry {
entry: {
branch (x >= y) goto then_branch else else_branch;
}
then_branch: {
m := x;
goto done;
}
else_branch: {
m := y;
goto done;
}
done: {
return;
}
};

// A CFG with a simple loop: increment y until it reaches n.
procedure CountUp(n : int, out y : int)
spec {
requires (n >= 0);
ensures (y == n);
}
cfg entry {
entry: {
y := 0;
goto loop;
}
loop: {
branch (y < n) goto body else done;
}
body: {
y := y + 1;
goto loop;
}
done: {
return;
}
};
3 changes: 2 additions & 1 deletion Strata/Backends/CBMC/CoreToCBMC.lean
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ def createImplementationSymbolFromAST (func : Core.Procedure) : Except String CB

-- For now, keep the hardcoded implementation but use function name from AST
let loc : SourceLoc := { functionName := (func.header.name.toPretty), lineNum := "1" }
let stmtJsons ← (func.body.mapM (stmtToJson (I:=CoreLParams) · loc))
let bodyStmts ← func.body.getStructured
let stmtJsons ← (bodyStmts.mapM (stmtToJson (I:=CoreLParams) · loc))

let implValue := Json.mkObj [
("id", "code"),
Expand Down
1 change: 1 addition & 0 deletions Strata/Backends/CBMC/GOTO.lean
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
-/
module

import Strata.Backends.CBMC.GOTO.CoreCFGToGOTOPipeline
import Strata.Backends.CBMC.GOTO.CoreToCProverGOTO
import Strata.Backends.CBMC.GOTO.CoreToGOTOPipeline
import Strata.Backends.CBMC.GOTO.DefaultSymbols
Expand Down
304 changes: 304 additions & 0 deletions Strata/Backends/CBMC/GOTO/CoreCFGToGOTOPipeline.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
/-
Copyright Strata Contributors

SPDX-License-Identifier: Apache-2.0 OR MIT
-/
module

public import Strata.Backends.CBMC.CollectSymbols
public import Strata.Backends.CBMC.GOTO.CoreToCProverGOTO
import Strata.Backends.CBMC.GOTO.CoreToGOTOPipeline
import Strata.Transform.StructuredToUnstructured

/-! ## Core-to-GOTO translation via CFG

Alternative pipeline that translates Core procedures to CProver GOTO by going
through the CFG representation:

- **Structured body** → `stmtsToCFG` → `coreCFGToGotoTransform`
- **CFG body** → `coreCFGToGotoTransform`

This coexists with the direct path in `CoreToGOTOPipeline.lean`.
-/

namespace Strata

public section

/-! ### Rename helpers for Core commands (CmdExt) -/

private def renameCoreCallArg
(rn : Std.HashMap String String)
: Core.CallArg Core.Expression → Core.CallArg Core.Expression
| .inArg e => .inArg (renameExpr rn e)
| .inoutArg id => .inoutArg (renameIdent rn id)
| .outArg id => .outArg (renameIdent rn id)

private def renameCoreCommand
(rn : Std.HashMap String String)
: Core.Command → Core.Command
| .cmd c => .cmd (renameCmd rn c)
| .call procName callArgs md =>
.call procName (callArgs.map (renameCoreCallArg rn)) md

private partial def renameCoreStmt
(rn : Std.HashMap String String)
: Core.Statement → Core.Statement
| .cmd c => .cmd (renameCoreCommand rn c)
| .block l stmts md =>
.block l (stmts.map (renameCoreStmt rn)) md
| .ite c t e md =>
.ite (c.map (renameExpr rn)) (t.map (renameCoreStmt rn)) (e.map (renameCoreStmt rn)) md
| .loop g m i body md =>
.loop (g.map (renameExpr rn)) (m.map (renameExpr rn))
(i.map (fun (l, e) => (l, renameExpr rn e)))
(body.map (renameCoreStmt rn)) md
| .exit l md => .exit l md
| .funcDecl d md => .funcDecl d md
| .typeDecl tc md => .typeDecl tc md

private def renameCoreDetCFG
(rn : Std.HashMap String String)
(cfg : Core.DetCFG) : Core.DetCFG :=
{ cfg with blocks := cfg.blocks.map fun (label, block) =>
(label, { cmds := block.cmds.map (renameCoreCommand rn),
transfer := match block.transfer with
| .condGoto cond lt lf md =>
.condGoto (renameExpr rn cond) lt lf md
| .finish md => .finish md }) }

/-! ### Core-specific CFG-to-GOTO translation -/

/--
Translate a Core `DetCFG` to CProver GOTO instructions.

Like `detCFGToGotoTransform` but handles `Core.Command` (which includes
`CmdExt.call`). The CFG should already have identifiers renamed via
`renameCoreDetCFG`.
-/
def coreCFGToGotoTransform
(_Env : Core.Expression.TyEnv) (functionName : String)
(cfg : Core.DetCFG)
(trans : Imperative.GotoTransform Core.Expression.TyEnv)
: Except Std.Format (Imperative.GotoTransform Core.Expression.TyEnv) := do
let toExpr := Lambda.LExpr.toGotoExprCtx
(TBase := ⟨Core.ExpressionMetadata, Unit⟩) []
-- Verify entry block is first
match cfg.blocks with
| (firstLabel, _) :: _ =>
if firstLabel != cfg.entry then
throw f!"[coreCFGToGotoTransform] Entry label '{cfg.entry}' does not match \
first block label '{firstLabel}'."
| [] => pure ()
let mut trans := trans
let mut pendingPatches : Array (Nat × String) := #[]
let mut labelMap : Std.HashMap String Nat := {}
let mut loopContracts : Std.HashMap String (Imperative.MetaData Core.Expression) := {}
for (label, block) in cfg.blocks do
labelMap := labelMap.insert label trans.nextLoc
let srcLoc : CProverGOTO.SourceLocation :=
{ CProverGOTO.SourceLocation.nil with function := functionName }
trans := Imperative.emitLabel label srcLoc trans
-- Translate each command
for cmd in block.cmds do
match cmd with
| .cmd c =>
trans ← Imperative.Cmd.toGotoInstructions trans.T functionName c trans
| .call procName callArgs _md =>
let lhs := Core.CallArg.getLhs callArgs
let args := Core.CallArg.getInputExprs callArgs
let argExprs ← args.mapM toExpr
let lhsExpr := match lhs with
| id :: _ =>
let name := Core.CoreIdent.toPretty id
let ty := match trans.T.context.types.find? id with
| some lty =>
match lty.toMonoTypeUnsafe.toGotoType with
| .ok gty => gty
| .error _ =>
dbg_trace s!"warning: type conversion failed for {name}, defaulting to Integer"
.Integer
| none =>
dbg_trace s!"warning: no type found for {name}, defaulting to Integer"
.Integer
CProverGOTO.Expr.symbol name ty
| [] => CProverGOTO.Expr.symbol "" .Empty
let calleeExpr := CProverGOTO.Expr.symbol procName
(CProverGOTO.Ty.mkCode (argExprs.map (·.type)) lhsExpr.type)
let callCode := CProverGOTO.Code.functionCall lhsExpr calleeExpr argExprs
let inst : CProverGOTO.Instruction :=
{ type := .FUNCTION_CALL, code := callCode, locationNum := trans.nextLoc }
trans := { trans with
instructions := trans.instructions.push inst
nextLoc := trans.nextLoc + 1 }
-- Translate the transfer command
match block.transfer with
| .condGoto cond lt lf md =>
let transferSrcLoc := Imperative.metadataToSourceLoc md functionName trans.sourceText
let cond_expr ← toExpr cond
let hasLoopContract := md.any fun elem =>
elem.fld == Imperative.MetaData.specLoopInvariant ||
elem.fld == Imperative.MetaData.specDecreases
if hasLoopContract then
loopContracts := loopContracts.insert label md
let (trans', falseIdx) :=
Imperative.emitCondGoto (CProverGOTO.Expr.not cond_expr) transferSrcLoc trans
trans := trans'
pendingPatches := pendingPatches.push (falseIdx, lf)
let (trans', trueIdx) := Imperative.emitUncondGoto transferSrcLoc trans
trans := trans'
pendingPatches := pendingPatches.push (trueIdx, lt)
| .finish _md =>
pure ()
-- Second pass: resolve labels and annotate backward-edge GOTOs with loop contracts
let mut resolvedPatches : List (Nat × Nat) := []
for (idx, label) in pendingPatches do
match labelMap[label]? with
| some targetLoc =>
resolvedPatches := (idx, targetLoc) :: resolvedPatches
if let some md := loopContracts[label]? then
let instLoc := trans.instructions[idx]!.locationNum
if targetLoc ≤ instLoc then
let mut guard := trans.instructions[idx]!.guard
for elem in md do
if elem.fld == Imperative.MetaData.specLoopInvariant then
if let .expr e := elem.value then
let gotoExpr ← toExpr e
guard := guard.setNamedField "#spec_loop_invariant" gotoExpr
if elem.fld == Imperative.MetaData.specDecreases then
if let .expr e := elem.value then
let gotoExpr ← toExpr e
guard := guard.setNamedField "#spec_decreases" gotoExpr
trans := { trans with
instructions := trans.instructions.set! idx
{ trans.instructions[idx]! with guard := guard } }
| none =>
throw f!"[coreCFGToGotoTransform] Unresolved label '{label}' referenced \
by GOTO at instruction index {idx}."
return Imperative.patchGotoTargets trans resolvedPatches

/-! ### Pipeline wrapper -/

/--
Translate a Core procedure to CProver GOTO via the CFG representation.

Mirrors `procedureToGotoCtx` but routes through `stmtsToCFG` +
`coreCFGToGotoTransform` instead of the direct Stmt-to-GOTO path.
-/
def procedureToGotoCtxViaCFG
(Env : Core.Expression.TyEnv) (p : Core.Procedure)
(sourceText : Option String := none)
(axioms : List Core.Axiom := [])
(distincts :
List (Core.Expression.Ident × List Core.Expression.Expr) := [])
: Except Std.Format
(CoreToGOTO.CProverGOTO.Context × List Core.Function) := do
let pname := Core.CoreIdent.toPretty p.header.name
if !p.header.typeArgs.isEmpty then
.error f!"[procedureToGotoCtxViaCFG] Polymorphic procedures unsupported."
let ret_ty := CProverGOTO.Ty.Empty
let formals := p.header.inputs.keys.map Core.CoreIdent.toPretty
let formals_tys ← p.header.inputs.values.mapM Lambda.LMonoTy.toGotoType
let new_formals := formals.map (CProverGOTO.mkFormalSymbol pname ·)
let formals_tys : Map String CProverGOTO.Ty := formals.zip formals_tys
let outputs := p.header.outputs.keys.map Core.CoreIdent.toPretty
let new_outputs := outputs.map (CProverGOTO.mkLocalSymbol pname ·)
let locals_from_body := match p.body with
| .structured ss => (Imperative.Block.definedVars ss).map Core.CoreIdent.toPretty
| .cfg c => c.blocks.flatMap (fun (_, blk) =>
blk.cmds.flatMap Core.Command.definedVars)
|>.map Core.CoreIdent.toPretty
let new_locals := locals_from_body.map (CProverGOTO.mkLocalSymbol pname ·)
let rn : Std.HashMap String String :=
(formals.zip new_formals ++ outputs.zip new_outputs ++ locals_from_body.zip new_locals).foldl
(init := {}) fun m (k, v) => m.insert k v
-- Seed the type environment with renamed input and output parameter types
let inputEntries : Map Core.Expression.Ident Core.Expression.Ty :=
(new_formals.zip p.header.inputs.values).map fun (n, ty) =>
(((n : Core.CoreIdent)), .forAll [] ty)
let outputEntries : Map Core.Expression.Ident Core.Expression.Ty :=
(new_outputs.zip p.header.outputs.values).map fun (n, ty) =>
(((n : Core.CoreIdent)), .forAll [] ty)
let Env' : Core.Expression.TyEnv :=
Lambda.TEnv.addInNewestContext (T := ⟨Core.ExpressionMetadata, Unit⟩)
Env (inputEntries ++ outputEntries)
-- Emit axioms as ASSUME instructions
let mut axiomInsts : Array CProverGOTO.Instruction := #[]
let mut axiomLoc : Nat := 0
for ax in axioms do
let gotoExpr ← Lambda.LExpr.toGotoExprCtx
(TBase := ⟨Core.ExpressionMetadata, Unit⟩) [] ax.e
if gotoExpr.hasUnsupportedQuantifierTypes then continue
axiomInsts := axiomInsts.push
{ type := .ASSUME, locationNum := axiomLoc,
guard := gotoExpr,
sourceLoc := { CProverGOTO.SourceLocation.nil with
function := pname, comment := s!"axiom {ax.name}" } }
axiomLoc := axiomLoc + 1
-- Emit distinct declarations as pairwise != ASSUME instructions
for (dname, exprs) in distincts do
let gotoExprs ← exprs.mapM
(Lambda.LExpr.toGotoExprCtx (TBase := ⟨Core.ExpressionMetadata, Unit⟩) [])
for i in List.range gotoExprs.length do
for j in List.range gotoExprs.length do
if i < j then
let ei := gotoExprs[i]!
let ej := gotoExprs[j]!
let neqExpr : CProverGOTO.Expr :=
{ id := .binary .NotEqual, type := .Boolean, operands := [ei, ej] }
let srcLoc : CProverGOTO.SourceLocation :=
{ CProverGOTO.SourceLocation.nil with
function := pname
comment := s!"distinct {Core.CoreIdent.toPretty dname}" }
axiomInsts := axiomInsts.push
{ type := .ASSUME, locationNum := axiomLoc, guard := neqExpr, sourceLoc := srcLoc }
axiomLoc := axiomLoc + 1
-- Build the CFG (from structured body or use existing CFG)
let (cfg, liftedFuncs) ← match p.body with
| .structured ss => do
let (liftedFuncs, body) ← collectFuncDecls ss
let renamedBody := body.map (renameCoreStmt rn)
let cfg := Imperative.stmtsToCFG renamedBody
pure (cfg, liftedFuncs)
| .cfg c => do
let cfg := renameCoreDetCFG rn c
pure (cfg, [])
-- Translate CFG to GOTO
let ans ← coreCFGToGotoTransform Env' pname cfg
{ instructions := axiomInsts, nextLoc := axiomLoc, T := Env', sourceText := sourceText }
let ending_insts : Array CProverGOTO.Instruction :=
#[{ type := .END_FUNCTION, locationNum := ans.nextLoc + 1 }]
let pgm := { name := pname,
parameterIdentifiers := new_formals.toArray,
instructions := ans.instructions ++ ending_insts }
-- Translate procedure contracts
let mut contracts : List (String × Lean.Json) := []
let preExprs := p.spec.preconditions.values.filter (fun c => c.attr == .Default)
|>.map (fun c => renameExpr rn c.expr)
let postExprs := p.spec.postconditions.values.filter (fun c => c.attr == .Default)
|>.map (fun c => renameExpr rn c.expr)
let toGotoExpr := Lambda.LExpr.toGotoExprCtx
(TBase := ⟨Core.ExpressionMetadata, Unit⟩) []
if !preExprs.isEmpty then
let preGoto ← preExprs.mapM toGotoExpr
let preJson ← (preGoto.mapM CProverGOTO.exprToJson).mapError (fun e => f!"{e}")
contracts := contracts ++ [("#spec_requires",
Lean.Json.mkObj [("id", ""), ("sub", Lean.Json.arr preJson.toArray)])]
if !postExprs.isEmpty then
let postGoto ← postExprs.mapM toGotoExpr
let postJson ← (postGoto.mapM CProverGOTO.exprToJson).mapError (fun e => f!"{e}")
contracts := contracts ++ [("#spec_ensures",
Lean.Json.mkObj [("id", ""), ("sub", Lean.Json.arr postJson.toArray)])]
-- Build localTypes map for output parameters
let output_tys ← p.header.outputs.values.mapM Lambda.LMonoTy.toGotoType
let localTypes : Std.HashMap String CProverGOTO.Ty :=
(outputs.zip output_tys).foldl (init := {}) fun m (k, v) => m.insert k v
let ctx : CoreToGOTO.CProverGOTO.Context :=
{ program := pgm, formals := formals_tys, ret := ret_ty,
locals := outputs ++ locals_from_body, contracts := contracts,
localTypes := localTypes }
return (ctx, liftedFuncs)

end -- public section

end Strata
Loading
Loading