Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions lib/passes.ml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ module PassManager = struct
invariants = Invariants.presupposes [ Params ] ~establishes:[ SSA ];
}

let cfa_reduction =
{
name = "cfa-reduction";
apply = Proc Transforms.Cfa_reduction.reduce_procedure;
doc = "Performs reduction of acyclic CFA";
invariants = Invariants.presupposes [ SSA ];
}

let remove_unreachable_blocks =
{
name = "remove-unreachable-block";
Expand Down Expand Up @@ -453,6 +461,7 @@ module PassManager = struct
read_uninit false;
read_uninit true;
sssa;
cfa_reduction;
sva;
full_ssa;
chc_infer_invariants;
Expand Down
104 changes: 104 additions & 0 deletions lib/transforms/cfa_reduction.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
open Lang
open Lang.Common

(* Assumes program is in SSA, acyclic and pure. *)
let construct_final_edge proc =
(* Termination condition for each edge. *)
let term : (IDSet.elt, Var.t) Hashtbl.t = Hashtbl.create 30 in

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

term is too generic, a longer name like "termination_condition" would be better.

Procedure.fold_blocks_topo_fwd
(fun final_edge id block ->
let preds = Procedure.blocks_pred proc id |> Iter.to_list in

(* Compute reachability of this block. *)
let reachable =
(* Always reachable if no predecessors. *)
if List.is_empty preds then Expr.BasilExpr.boolconst true
(* Otherwise, reachability is ANY of the predecessors reachability. *)
else
Expr.BasilExpr.applyintrin ~op:`OR
(preds
|> List.filter_map (fst %> Hashtbl.get term)
|> List.map Expr.BasilExpr.rvar)
in
Comment on lines +10 to +22

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally prefer pattern matching rather than explicit conditions. I've also inlined the preds definition because it's only used here.

Suggested change
let preds = Procedure.blocks_pred proc id |> Iter.to_list in
(* Compute reachability of this block. *)
let reachable =
(* Always reachable if no predecessors. *)
if List.is_empty preds then Expr.BasilExpr.boolconst true
(* Otherwise, reachability is ANY of the predecessors reachability. *)
else
Expr.BasilExpr.applyintrin ~op:`OR
(preds
|> List.filter_map (fst %> Hashtbl.get term)
|> List.map Expr.BasilExpr.rvar)
in
(* Compute reachability of this block. *)
let reachable =
match Procedure.blocks_pred proc id |> Iter.to_list with
(* Always reachable if no predecessors. *)
| [] -> Expr.BasilExpr.boolconst true
(* Otherwise, reachability is ANY of the predecessors reachability. *)
| preds ->
Expr.BasilExpr.applyintrin ~op:`OR
(preds
|> List.filter_map (fst %> Hashtbl.get term)
|> List.map Expr.BasilExpr.rvar)
in


(* Create ITE statements from phi nodes. *)
let ites : Program.stmt =
let al =
block.phis
|> List.map (function
| ({ lhs; rhs = hd :: tl } : Var.t Block.phi) ->

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a subtlety here that the block ID in hd is not used and it's just assumed to be the negation of everything else. Is this worth a comment?

Regardless, pattern matching on (_, hd_var) instead of hd will make this more clear and avoid the snd.

let ite =
List.fold_left
(fun acc (in_edge, var) ->
Expr.BasilExpr.ifthenelse
(Expr.BasilExpr.rvar @@ Hashtbl.find term in_edge)
(Expr.BasilExpr.rvar var) acc)
(Expr.BasilExpr.rvar @@ snd hd)
Comment on lines +31 to +36

@katrinafyi katrinafyi Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local open of BasilExpr can avoid some repetition

Suggested change
List.fold_left
(fun acc (in_edge, var) ->
Expr.BasilExpr.ifthenelse
(Expr.BasilExpr.rvar @@ Hashtbl.find term in_edge)
(Expr.BasilExpr.rvar var) acc)
(Expr.BasilExpr.rvar @@ snd hd)
List.fold_left
(fun acc (in_edge, var) ->
let cond = Hashtbl.find term in_edge in
Expr.BasilExpr.(ifthenelse (rvar cond) (rvar var) acc))
(Expr.BasilExpr.rvar @@ snd hd)

I also add a local variable because I like to keep things in the local open as small as possible.

tl
in
(lhs, ite)
| _ -> failwith "Encountered phi node with no rhs.")
in
Stmt.Instr_Assign { attrib = Attrib.empty; al }
in

(* Add a fresh termination variable to assign the condition. *)
let termination_var = Procedure.fresh_var proc ~pure:true Types.Boolean in
Hashtbl.add term id termination_var;

(* Construct our termination condition by combining
initial reachability with any assumes along this edge. *)
let termination_cond =
Expr.BasilExpr.applyintrin ~op:`AND
([ reachable ]
@ (Block.stmts_iter block
|> Iter.filter_map (function
| Stmt.Instr_Assume { body; branch = true } -> Some body
| _ -> None)
|> Iter.to_list))
Comment on lines +54 to +58

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to factor out this filter (and avoid the repetition in the later inverse filter) by using a partition.

For example

      let guard_expressions, non_guard_stmts =
        Block.stmts_iter block |> Iter.to_list
        |> List.partition_map_either (function
          | Stmt.Instr_Assume { body; branch = true } -> Left body
          | stmt -> Right stmt)

This will also simplify the final list concats in this function.

in
let termination =
Stmt.Instr_Assign
{
attrib = Attrib.empty;
al = [ (termination_var, termination_cond) ];
}
in

(* Final edge is existing statements plus:
1. the ites for the new edge being merged in.
2. the statements for the new edge body.
3. an assignment to the termination variable.
*)
final_edge
@ List.concat
[
[ ites ];
(* Filter out the now unnecessary guards. *)
block.stmts |> Vector.to_list
|> List.filter (fun s ->
not
@@
match s with
| Stmt.Instr_Assume { branch = true } -> true
| _ -> false);
[ termination ];
])
List.empty proc

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
List.empty proc
[] proc


let reduce_procedure (proc : Program.proc) : Program.proc =
(* Constructed reduced edge to replace procedure blocks. *)
let final_edge = construct_final_edge proc in

let out_proc =
proc |> Procedure.iter_blocks |> Iter.map fst
|> Iter.fold (fun acc id -> Procedure.remove_block acc id) proc
in
let out_proc, id = Procedure.fresh_block out_proc ~stmts:final_edge () in

(* Make this the entry and return block. *)
let out_proc = Procedure.set_entry_block out_proc id in
Procedure.PG.map_graph
(fun g ->
Procedure.G.add_edge g (Procedure.Vert.End id) Procedure.Vert.Return)
out_proc
Comment on lines +93 to +104

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just shadow proc to avoid accidentally re-using it.

Suggested change
let out_proc =
proc |> Procedure.iter_blocks |> Iter.map fst
|> Iter.fold (fun acc id -> Procedure.remove_block acc id) proc
in
let out_proc, id = Procedure.fresh_block out_proc ~stmts:final_edge () in
(* Make this the entry and return block. *)
let out_proc = Procedure.set_entry_block out_proc id in
Procedure.PG.map_graph
(fun g ->
Procedure.G.add_edge g (Procedure.Vert.End id) Procedure.Vert.Return)
out_proc
let proc =
proc |> Procedure.iter_blocks |> Iter.map fst
|> Iter.fold Procedure.remove_block proc
in
let proc, id = Procedure.fresh_block proc ~stmts:final_edge () in
(* Make this the entry and return block. *)
let proc = Procedure.set_entry_block proc id in
Procedure.PG.map_graph
(fun g ->
Procedure.G.add_edge g (Procedure.Vert.End id) Procedure.Vert.Return)
proc

23 changes: 23 additions & 0 deletions test/cram/cfa_reduction.il
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
prog entry @f1;

proc @f1(a:bv64) -> (c:bv64)
[
block %a [
var x:bv64 := a;
goto(%b, %c);
];
block %b [
guard(bvuge(x,0:bv64));
var x := bvadd(x:bv64,1:bv64);
goto(%exit);
];
block %c [
guard(bvult(x,0:bv64));
var x := bvsub(x:bv64,1:bv64);
goto(%exit);
];
block %exit [
var c:bv64 := x;
return;
];
];
5 changes: 5 additions & 0 deletions test/cram/cfa_reduction.sexp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(load-il "./cfa_reduction.il")
(run-transforms "ssa")
(run-transforms "cfa-reduction")
(run-transforms "simplify")
(dump-il "./out.il")
21 changes: 21 additions & 0 deletions test/cram/cfa_reduction.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
$ bincaml script ./cfa_reduction.sexp
(load-il ./cfa_reduction.il)
(run-transforms ssa)
(run-transforms cfa-reduction)
(run-transforms simplify)
(dump-il ./out.il)
$ cat ./out.il
proc @f1(a:bv64) -> (c:bv64) { }


[
block %block [
var v:bool := true;
var v_2:bool := booland(v:bool, boolnot(bvult(a:bv64, 0x0:bv64)));
var x_6:bv64 := if v_2:bool then bvadd(a:bv64, 0x1:bv64) else bvsub(a:bv64,
0x1:bv64);
var c:bv64 := x_6:bv64;
return;
]
];
prog entry @f1;
2 changes: 2 additions & 0 deletions test/cram/dune
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
ll_spec.sexp
repeated_ssa.il
repeated_ssa.sexp
cfa_reduction.il
cfa_reduction.sexp
../../bin/main.exe))

(cram
Expand Down
Loading