Skip to content

Commit a729eb7

Browse files
PROgram52bcjoehendrix
authored andcommitted
Procedure.Body sum type with parser, translator, and consumer threading (split of #1196) (#1342)
## Summary This PR introduces `Procedure.Body := .structured (List Statement) | .cfg DetCFG` — a sum type letting a Strata Core procedure carry either a structured statement-list body or a deterministic control-flow graph. It includes the type infrastructure, consumer threading across the verification pipeline, the DDM parser/translator support for the new `cfg` syntax, and CFG example programs. This PR is split out of [#1196](#1196) as a self-contained PR that can land independently on `main2`. It contributes ~714 LoC of additions vs `main2` (compared to ~5,567 LoC on [#1196](#1196)). It is **independent** of the companion split [#1341 (htd/unstructured-infra)](#1341) — the two PRs touch disjoint files (only `ProcBodyVerifyCorrect.lean` overlaps in different regions, mergeable cleanly). This PR does **not** introduce the imperative-DL infrastructure (metadata-bearing transfers, `EvalDetBlock` constructor renames, etc.) — those live in the companion [#1341 `htd/unstructured-infra`](#1341) PR. Without that, this PR's `CoreBodyExec` semantic relation has only the `.structured` constructor; the `.cfg` constructor is added when both branches merge. ## What's added The 44 files modified group into 5 layers: ### Type infrastructure (`Procedure.lean`, ~110 LoC) - `inductive Procedure.Body := .structured (List Statement) | .cfg DetCFG` plus `Inhabited` instance - `abbrev DetCFG := Imperative.CFG String (Imperative.DetBlock String Command Expression)` - 7 projection helpers: `getStructured`, `getCfg`, `getVars`, `isAbstract`, `isStructured`, `isCfg`, `structuredLength` - `HasVarsPure`/`HasVarsImp` instances on `Body` and `DetCFG` - `DetCFG.eraseTypes`, `DetCFG.stripMetaData` - `Procedure.body` field flips from `List Statement` to `Procedure.Body` (default `.structured []`) - `Procedure.eraseTypes`/`stripMetaData`/`getVars` retrofits ### WF predicate adaptation - `WF.lean`: `wfstmts`/`wfloclnd`/`bodyExitsCovered` rewritten as `∀ ss, body = .structured ss → ...` - `ProcBodyVerifyCorrect.lean`: new `procToVerifyStmt_is_structured` lemma bridging the sum type to the verification pipeline's `.structured` requirement ### Semantic relation - `StatementSemantics.lean`: `CoreBodyExec` with `.structured` constructor; `EvalCommand.call_sem` updated to use it - The `.cfg` constructor is left for [#1196](#1196) since it depends on `EvalDetBlock` from the companion infra PR ### Consumer threading (~150 LoC across 15 files) **Real CFG handling** (cannot be safely stubbed): - `CallGraph.lean`: traverses CFG arms to extract calls - `ProcedureInlining.lean`: handles `.structured` arm with the existing logic; throws on `.cfg` **`.cfg`-arm stubs** (error/passthrough/throw, replaceable when [#1196](#1196) merges): - `StatementEval.lean`, `ProcedureType.lean`, `ProcedureEval.lean`, `Verifier.lean`, `ObligationExtraction.lean`, `FormatCore.lean` - `Transform/PrecondElim.lean`, `CoreTransform.lean`, `LoopElim.lean`, `ANFEncoder.lean`, `TerminationCheck.lean`, `ProcBodyVerify.lean`, `CoreSpecification.lean` ### DDM parser support (`DDMTransform/Grammar.lean`, ~54 LoC) - New parser categories: `Transfer`, `CFGBlock`, `CFGBlocks`, `CFGBody` - `command_cfg_procedure` operator parsing `procedure name ... cfg ENTRY { ... }` syntax - Transfer commands: `transfer_goto`, `transfer_nondet_goto`, `transfer_cond_goto` (using `branch (cond) goto LT else goto LF` to avoid `if`-collision with structured syntax), `transfer_return` ### DDM translator (`DDMTransform/Translate.lean`, ~115 LoC) - `translateCFGBlock`, `translateCFGBlocks`, `translateCFGBody`, `translateTransfer` build `Procedure.Body.cfg` from parsed AST - `translateProcedure`/`translateBlockCommand` updated to route `.structured` vs `.cfg` body shapes ### Examples and tooling - `Examples/CFGSimple.core.st` — sample procedure (`Max(x,y)` computing maximum of two integers via CFG) - `Examples/CFGNondet.core.st` — sample with nondeterministic transfer - `docs/Architecture.md`, `docs/verso/IRTranslationPhilosophyDoc.lean` — documentation updates - `editors/emacs/core-st-mode.el`, `editors/vscode/syntaxes/core-st.tmLanguage.json` — keyword highlighting for `cfg`/`branch`/`goto`/`return` literals ## Comparison vs [#1196](#1196) | Feature | This PR | [#1196](#1196) | |---|:---:|:---:| | `Procedure.Body` sum type + `DetCFG` abbreviation + `body` field flip | ✅ | ✅ | | Body projection helpers (`getStructured`, `getVars`, etc.) | ✅ | ✅ | | `WF.lean` adaptation for sum-type body | ✅ | ✅ | | `procToVerifyStmt_is_structured` bridge lemma | ✅ | ✅ | | `CoreBodyExec` (`.structured` arm) | ✅ | ✅ | | `CoreBodyExec` (`.cfg` arm) | — (needs infra) | ✅ | | `CallGraph.lean` CFG traversal | ✅ | ✅ | | `ProcedureInlining.lean` sum-type handling | ✅ | ✅ | | `.cfg`-arm stubs in 13 consumers | ✅ | ✅ replaced by full impl | | Full `.cfg`-arm implementations (PrecondElim, ProcedureEval, etc.) | — | ✅ | | DDM `cfg ENTRY { ... }` parser syntax | ✅ | ✅ | | DDM `translateCFG*` translator | ✅ | ✅ | | CFG examples (`Examples/CFG{Simple,Nondet}.core.st`) | ✅ | ✅ | | Editor/docs syntax-highlighting updates | ✅ | ✅ | | Imperative DL: metadata-bearing transfers, `EvalDetBlock` rename | — (in companion PR) | ✅ | | Translator metadata propagation | — (in companion PR) | ✅ | | GOTO backend CFG pipeline | — | ✅ | | CFG-specific test suites | — | ✅ | | Lambda framework theorems | — | ✅ (via main2 merges) | ## Build status - `lake build`: green (490 jobs) - `lake test --exclude Languages.Python`: green (modulo the pre-existing CI-managed `ion-java-1.11.11.jar` test fixture) - 0 sorries, 0 axioms across all modified files ## Test plan - [x] `lake build` succeeds locally on a fresh worktree - [x] `lake test --exclude Languages.Python` succeeds (the only failure is the env-managed `ion-java` jar download which CI handles) - [x] No new sorries or axioms in any file - [x] Existing structured-procedure tests continue to pass (the `.body := .structured body` adapter at `Translate.lean:1683`/`1702` keeps existing parsing/verification paths working) - [ ] CI passes - [ ] Pairs with the companion [#1341 `htd/unstructured-infra`](#1341) PR — either can land first; see comparison table above - [ ] After this and the infra PR land, [#1196](#1196) can be rebased to ~2,000 LoC of remaining CFG-pipeline-specific content (CFG test suites, GOTO backend, full `.cfg`-arm implementations replacing stubs)
1 parent f090775 commit a729eb7

41 files changed

Lines changed: 684 additions & 147 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Examples/CFGNondet.core.st

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
program Core;
2+
3+
// Nondeterministic CFG: y is set to either 1 or 2.
4+
procedure NondetChoice(out y : int)
5+
spec {
6+
ensures (y == 1 || y == 2);
7+
}
8+
cfg entry {
9+
entry: {
10+
goto left, right;
11+
}
12+
left: {
13+
y := 1;
14+
goto done;
15+
}
16+
right: {
17+
y := 2;
18+
goto done;
19+
}
20+
done: {
21+
return;
22+
}
23+
};

Examples/CFGSimple.core.st

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
program Core;
2+
3+
// A simple deterministic CFG: compute max of two integers.
4+
procedure Max(x : int, y : int, out m : int)
5+
spec {
6+
ensures (m >= x);
7+
ensures (m >= y);
8+
ensures (m == x || m == y);
9+
}
10+
cfg entry {
11+
entry: {
12+
branch (x >= y) goto then_branch else goto else_branch;
13+
}
14+
then_branch: {
15+
m := x;
16+
goto done;
17+
}
18+
else_branch: {
19+
m := y;
20+
goto done;
21+
}
22+
done: {
23+
return;
24+
}
25+
};
26+
27+
// A CFG with a simple loop: increment y until it reaches n.
28+
procedure CountUp(n : int, out y : int)
29+
spec {
30+
requires (n >= 0);
31+
ensures (y == n);
32+
}
33+
cfg entry {
34+
entry: {
35+
y := 0;
36+
goto loop;
37+
}
38+
loop: {
39+
branch (y < n) goto body else goto done;
40+
}
41+
body: {
42+
y := y + 1;
43+
goto loop;
44+
}
45+
done: {
46+
return;
47+
}
48+
};

Strata/Backends/CBMC/CoreToCBMC.lean

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ def createImplementationSymbolFromAST (func : Core.Procedure) : Except String CB
340340

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

345346
let implValue := Json.mkObj [
346347
("id", "code"),

Strata/Backends/CBMC/GOTO/CoreToCProverGOTO.lean

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,11 @@ def transformToGoto (cprog : Core.Program) : Except Format CProverGOTO.Context :
204204
if !p.header.typeArgs.isEmpty then
205205
throw f!"[transformToGoto] Translation for polymorphic Strata Core procedures is unimplemented."
206206

207-
let cmds ← p.body.mapM
207+
-- TODO: This pass could be split into a two-stage transformation:
208+
-- 1. structured → cfg (via StructuredToUnstructured)
209+
-- 2. cfg → CProverGOTO (always operates on CFG, no pattern matching needed)
210+
let bodyStmts ← p.body.getStructured.mapError fun s => f!"{s}"
211+
let cmds ← bodyStmts.mapM
208212
(fun b => match b with
209213
| .cmd (.cmd c) => return c
210214
| _ => throw f!"[transformToGoto] We can process Strata Core commands only, not statements! \
@@ -221,7 +225,7 @@ def transformToGoto (cprog : Core.Program) : Except Format CProverGOTO.Context :
221225
let formals_renamed := formals.zip new_formals
222226
let formals_tys : Map String CProverGOTO.Ty := formals.zip formals_tys
223227

224-
let locals := (Imperative.Block.definedVars p.body).map Core.CoreIdent.toPretty
228+
let locals := (Imperative.Block.definedVars bodyStmts).map Core.CoreIdent.toPretty
225229
let new_locals := locals.map (fun l => CProverGOTO.mkLocalSymbol pname l)
226230
let locals_renamed := locals.zip new_locals
227231

Strata/Backends/CBMC/GOTO/CoreToGOTOPipeline.lean

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,17 @@ namespace Strata
4444

4545
public section
4646

47-
private def renameIdent (rn : Std.HashMap String String) (id : Core.CoreIdent) : Core.CoreIdent :=
47+
/-- Rewrite a `Core.CoreIdent` according to a name-substitution map. Names absent
48+
from `rn` pass through unchanged; metadata is preserved. -/
49+
def renameIdent (rn : Std.HashMap String String) (id : Core.CoreIdent) : Core.CoreIdent :=
4850
match rn[id.name]? with
4951
| some new => ⟨new, id.metadata⟩
5052
| none => id
5153

52-
private partial def renameExpr
54+
/-- Rewrite all free variable names in a `Core.Expression.Expr` according to `rn`.
55+
Recurses through binders and other expression forms; bound-variable indices are
56+
unchanged. -/
57+
partial def renameExpr
5358
(rn : Std.HashMap String String)
5459
: Core.Expression.Expr → Core.Expression.Expr
5560
| .fvar m name ty => .fvar m (renameIdent rn name) ty
@@ -60,7 +65,9 @@ private partial def renameExpr
6065
| .eq m e1 e2 => .eq m (renameExpr rn e1) (renameExpr rn e2)
6166
| e => e
6267

63-
private def renameCmd
68+
/-- Apply a name-substitution map to identifiers and expressions inside an
69+
`Imperative.Cmd` over Core expressions. -/
70+
def renameCmd
6471
(rn : Std.HashMap String String)
6572
: Imperative.Cmd Core.Expression → Imperative.Cmd Core.Expression
6673
| .init name ty e md => .init (renameIdent rn name) ty (e.map (renameExpr rn)) md
@@ -106,7 +113,7 @@ private def hasCallStmt : List Core.Statement → Bool
106113
Collect all funcDecl statements from a procedure body (recursively)
107114
and return them as Core.Functions, stripping them from the body.
108115
-/
109-
private def collectFuncDecls : List Core.Statement →
116+
def collectFuncDecls : List Core.Statement →
110117
Except Std.Format (List Core.Function × List Core.Statement)
111118
| [] => return ([], [])
112119
| .funcDecl decl _ :: rest => do
@@ -263,7 +270,11 @@ def procedureToGotoCtx
263270
: Except Std.Format
264271
(CoreToGOTO.CProverGOTO.Context × List Core.Function) := do
265272
-- Lift local function declarations out of the body
266-
let (liftedFuncs, body) ← collectFuncDecls p.body
273+
-- TODO: This pass could be split into a two-stage transformation:
274+
-- 1. structured → cfg (via StructuredToUnstructured)
275+
-- 2. cfg → CProverGOTO (always operates on CFG, no pattern matching needed)
276+
let bodyStmts ← p.body.getStructured.mapError fun s => f!"{s}"
277+
let (liftedFuncs, body) ← collectFuncDecls bodyStmts
267278
let pname := Core.CoreIdent.toPretty p.header.name
268279
if !p.header.typeArgs.isEmpty then
269280
.error f!"[procedureToGotoCtx] Polymorphic procedures unsupported."

Strata/Languages/C_Simp/Verify.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def loop_elimination_function(f : C_Simp.Function) : Core.Procedure :=
169169
outputs := [("return", f.ret_ty)]},
170170
spec := {preconditions := core_preconditions,
171171
postconditions := core_postconditions},
172-
body := f.body.map loop_elimination_statement}
172+
body := .structured (f.body.map loop_elimination_statement)}
173173

174174

175175
def loop_elimination(program : C_Simp.Program) : Core.Program :=

Strata/Languages/Core/CallGraph.lean

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,17 @@ def extractCallsFromFunction (func : Function) : List String :=
230230
| some body => extractFunctionCallsFromExpr body
231231
| none => []
232232

233+
/-- Extract procedure calls from a CmdExt -/
234+
def extractCallsFromCmdExt (cmd : Command) : List String :=
235+
match cmd with
236+
| .call procName _ _ => [procName]
237+
| .cmd _ => []
238+
233239
mutual
234240
/-- Extract procedure calls from a single statement -/
235241
def extractCallsFromStatement (stmt : Statement) : List String :=
236242
match stmt with
237-
| .cmd (.call procName _ _) => [procName]
238-
| .cmd _ => []
243+
| .cmd c => extractCallsFromCmdExt c
239244
| .block _ body _ => extractCallsFromStatements body
240245
| .ite _ thenBody elseBody _ =>
241246
extractCallsFromStatements thenBody ++
@@ -253,9 +258,16 @@ def extractCallsFromStatements (stmts : List Statement) : List String :=
253258
extractCallsFromStatements rest
254259
end
255260

261+
/-- Extract procedure calls from a deterministic CFG -/
262+
def extractCallsFromDetCFG (cfg : DetCFG) : List String :=
263+
cfg.blocks.flatMap fun (_, blk) =>
264+
blk.cmds.flatMap extractCallsFromCmdExt
265+
256266
/-- Extract all procedure calls from a procedure's body -/
257267
def extractCallsFromProcedure (proc : Procedure) : List String :=
258-
extractCallsFromStatements proc.body
268+
match proc.body with
269+
| .structured ss => extractCallsFromStatements ss
270+
| .cfg c => extractCallsFromDetCFG c
259271

260272
@[expose] abbrev ProcedureCG := CallGraph
261273
@[expose] abbrev FunctionCG := CallGraph

Strata/Languages/Core/DDMTransform/FormatCore.lean

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,12 @@ def procToCST {M} [Inhabited M] (proc : Core.Procedure) : ToCSTM M (Command M) :
989989
⟨default, none⟩
990990
else
991991
⟨default, some (Spec.spec_mk default specAnn)⟩
992-
let bodyCST ← blockToCST proc.body
992+
let bodyStmts ← match proc.body with
993+
| .structured ss => pure ss
994+
| .cfg _ => do
995+
ToCSTM.logError "procToCST" "CFG bodies not yet supported in CST conversion" proc.header.name.toPretty
996+
pure []
997+
let bodyCST ← blockToCST bodyStmts
993998
let body : Ann (Option (CoreDDM.Block M)) M := ⟨default, some bodyCST⟩
994999
modify ToCSTContext.popScope
9951000
pure (.command_procedure default name typeArgs arguments spec body)

Strata/Languages/Core/DDMTransform/Grammar.lean

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ namespace Strata
2424

2525
-- Sequence operations and lambda/application syntax increase the grammar size enough
2626
-- to require higher recursion and heartbeat limits.
27-
set_option maxRecDepth 10000
27+
set_option maxRecDepth 20000
2828
set_option maxHeartbeats 400000
2929

3030
/- DDM support for parsing and pretty-printing Strata Core -/
@@ -469,6 +469,59 @@ op datatype_decl (name : Ident,
469469
op command_datatypes (@[nonempty] datatypes : NewlineSepBy DatatypeDecl) : Command =>
470470
datatypes ";\n";
471471

472+
// =====================================================================
473+
// CFG (Unstructured Control Flow) Syntax
474+
// =====================================================================
475+
476+
// Transfer commands: how a basic block ends
477+
category Transfer;
478+
479+
// Unconditional goto: exactly one target.
480+
op transfer_goto (label : Ident) : Transfer =>
481+
"goto " label ";";
482+
483+
// Nondeterministic goto: exactly two targets chosen nondeterministically.
484+
op transfer_nondet_goto (label1 : Ident, label2 : Ident) : Transfer =>
485+
"goto " label1 ", " label2 ";";
486+
487+
// Conditional goto (deterministic: condition selects between two targets)
488+
// NOTE: We use "branch" instead of "if" to avoid ambiguity with the
489+
// structured if-statement syntax. The DDM parser registers tokens globally,
490+
// so "if (" in Transfer would conflict with "if (" in Statement.
491+
op transfer_cond_goto (c : Expr, lt : Ident, lf : Ident) : Transfer =>
492+
"branch (" c ") goto " lt " else " "goto " lf ";";
493+
494+
// Return/finish (terminate execution)
495+
op transfer_return : Transfer =>
496+
"return;";
497+
498+
// A single CFG basic block: label, commands, transfer
499+
category CFGBlock;
500+
@[scope(cmds)]
501+
op cfg_block (label : Ident, cmds : Seq Statement, tr : Transfer) : CFGBlock =>
502+
label ":" " {\n" indent(2, cmds) " " tr "\n}";
503+
504+
// A list of CFG blocks
505+
category CFGBlocks;
506+
op cfg_blocks_one (b : CFGBlock) : CFGBlocks => b;
507+
op cfg_blocks_cons (b : CFGBlock, rest : CFGBlocks) : CFGBlocks =>
508+
b "\n" rest;
509+
510+
// CFG body: entry label + blocks
511+
category CFGBody;
512+
op cfg_body (entry : Ident, blocks : CFGBlocks) : CFGBody =>
513+
"cfg " entry " {\n" indent(2, blocks) "\n}";
514+
515+
// Procedure with CFG body
516+
op command_cfg_procedure (name : Ident,
517+
typeArgs : Option TypeArgs,
518+
@[scope(typeArgs)] b : Bindings,
519+
@[scope(b)] s : Option Spec,
520+
@[scope(b)] body : CFGBody) :
521+
Command =>
522+
@[prec(10)] "procedure " name typeArgs b "\n"
523+
s body ";\n";
524+
472525
#end
473526

474527
---------------------------------------------------------------------

0 commit comments

Comments
 (0)