diff --git a/language/evm/move-to-yul/src/experiments.rs b/language/evm/move-to-yul/src/experiments.rs index 0e8063acf5..fea671b987 100644 --- a/language/evm/move-to-yul/src/experiments.rs +++ b/language/evm/move-to-yul/src/experiments.rs @@ -24,4 +24,8 @@ impl Experiment { /// during tests this is off. /// Retention: permanent pub const CAPTURE_SOURCE_INFO: &'static str = "capture-source-info"; + /// Transform control flow graph to structured control flow. + /// This is off by default for now, we might want to make it default + /// if the performance improvement is significant. + pub const APPLY_CFG_TO_SCF: &'static str = "apply-cfg-to-scf"; } diff --git a/language/evm/move-to-yul/src/functions.rs b/language/evm/move-to-yul/src/functions.rs index 3fe8ae6221..3bad32f765 100644 --- a/language/evm/move-to-yul/src/functions.rs +++ b/language/evm/move-to-yul/src/functions.rs @@ -14,6 +14,7 @@ use move_stackless_bytecode::{ function_target_pipeline::FunctionVariant, stackless_bytecode::{Bytecode, Constant, Label, Operation}, stackless_control_flow_graph::{BlockContent, BlockId, StacklessControlFlowGraph}, + stackless_structured_control_flow::StacklessStructuredControlFlow, }; use sha3::{Digest, Keccak256}; use std::collections::{btree_map::Entry, BTreeMap}; @@ -105,30 +106,119 @@ impl<'a> FunctionGenerator<'a> { // Compute control flow graph, entry block, and label map let code = target.data.code.as_slice(); let cfg = StacklessControlFlowGraph::new_forward(code); - let entry_bb = Self::get_actual_entry_block(&cfg); let label_map = Self::compute_label_map(&cfg, code); - // Emit state machine to represent control flow. - // TODO: Eliminate the need for this, see also - // https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2 - if cfg.successors(entry_bb).iter().all(|b| cfg.is_dummmy(*b)) { - // In this trivial case, we have only one block and can omit the state machine - if let BlockContent::Basic { lower, upper } = cfg.content(entry_bb) { - for offs in *lower..*upper + 1 { - self.bytecode(ctx, fun_id, target, &label_map, &code[offs as usize], false); + if !ctx.options.apply_cfg_to_scf() { + let entry_bb = Self::get_actual_entry_block(&cfg); + // Emit state machine to represent control flow. + // TODO: Eliminate the need for this, see also + // https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2 + if cfg.successors(entry_bb).iter().all(|b| cfg.is_dummmy(*b)) { + // In this trivial case, we have only one block and can omit the state machine + if let BlockContent::Basic { lower, upper } = cfg.content(entry_bb) { + for offs in *lower..*upper + 1 { + self.bytecode( + ctx, + fun_id, + target, + &label_map, + &code[offs as usize], + false, + ); + } + } else { + panic!("effective entry block is not basic") } } else { - panic!("effective entry block is not basic") + emitln!(ctx.writer, "let $block := {}", entry_bb); + emit!(ctx.writer, "for {} true {} "); + ctx.emit_block(|| { + emitln!(ctx.writer, "switch $block"); + for blk_id in &cfg.blocks() { + if let BlockContent::Basic { lower, upper } = cfg.content(*blk_id) { + // Emit code for this basic block. + emit!(ctx.writer, "case {} ", blk_id); + ctx.emit_block(|| { + for offs in *lower..*upper + 1 { + self.bytecode( + ctx, + fun_id, + target, + &label_map, + &code[offs as usize], + true, + ); + } + }) + } + } + }) } } else { - emitln!(ctx.writer, "let $block := {}", entry_bb); - emit!(ctx.writer, "for {} true {} "); - ctx.emit_block(|| { - emitln!(ctx.writer, "switch $block"); - for blk_id in &cfg.blocks() { - if let BlockContent::Basic { lower, upper } = cfg.content(*blk_id) { - // Emit code for this basic block. - emit!(ctx.writer, "case {} ", blk_id); + let prover_graph = cfg.to_prover_graph(); + let mut reduced_cfg = cfg.clone(); + let loop_map = reduced_cfg.reduce_cfg_loop(&prover_graph); + + let mut scf_top_sort = StacklessStructuredControlFlow::new(&reduced_cfg).top_sort; + while let Some(blocks) = scf_top_sort.pop() { + for blk_id in blocks { + if let Some(one_loop) = loop_map.get(&blk_id) { + emitln!(ctx.writer, "let $block := {}", one_loop.loop_header); + emit!(ctx.writer, "for {} true {} "); + ctx.emit_block(|| { + if let BlockContent::Basic { lower, upper } = + cfg.content(one_loop.loop_header) + { + ctx.emit_block(|| { + for offs in *lower..*upper + 1 { + self.bytecode( + ctx, + fun_id, + target, + &label_map, + &code[offs as usize], + true, + ); + } + }) + } + for loop_body_blk_id in &one_loop.loop_body { + if let BlockContent::Basic { lower, upper } = + cfg.content(*loop_body_blk_id) + { + ctx.emit_block(|| { + for offs in *lower..*upper + 1 { + self.bytecode( + ctx, + fun_id, + target, + &label_map, + &code[offs as usize], + true, + ); + } + }) + } + } + if let BlockContent::Basic { lower, upper } = + cfg.content(one_loop.loop_latch) + { + ctx.emit_block(|| { + for offs in *lower..*upper + 1 { + self.bytecode( + ctx, + fun_id, + target, + &label_map, + &code[offs as usize], + true, + ); + } + }) + } + }); + } else if let BlockContent::Basic { lower, upper } = cfg.content(blk_id) { + emitln!(ctx.writer, "let $block := {}", blk_id); ctx.emit_block(|| { for offs in *lower..*upper + 1 { self.bytecode( @@ -143,7 +233,7 @@ impl<'a> FunctionGenerator<'a> { }) } } - }) + } } }); emitln!(ctx.writer) diff --git a/language/evm/move-to-yul/src/options.rs b/language/evm/move-to-yul/src/options.rs index 55f44b7d71..923a0f2b75 100644 --- a/language/evm/move-to-yul/src/options.rs +++ b/language/evm/move-to-yul/src/options.rs @@ -62,4 +62,9 @@ impl Options { pub fn generate_source_info(&self) -> bool { !self.testing || self.experiment_on(Experiment::CAPTURE_SOURCE_INFO) } + + /// Returns true if control flow graph to structured control flow is applied. + pub fn apply_cfg_to_scf(&self) -> bool { + self.experiment_on(Experiment::APPLY_CFG_TO_SCF) + } } diff --git a/language/evm/move-to-yul/tests/ControlStructures.exp.apply-cfg-to-scf b/language/evm/move-to-yul/tests/ControlStructures.exp.apply-cfg-to-scf new file mode 100644 index 0000000000..fa596a40b6 --- /dev/null +++ b/language/evm/move-to-yul/tests/ControlStructures.exp.apply-cfg-to-scf @@ -0,0 +1,370 @@ +/* ======================================= + * Generated by Move-To-Yul compiler v0.0 + * ======================================= */ + + +object "A2_M" { + code { + codecopy(0, dataoffset("A2_M_deployed"), datasize("A2_M_deployed")) + return(0, datasize("A2_M_deployed")) + } + object "A2_M_deployed" { + code { + mstore(0, memoryguard(160)) + if iszero(lt(calldatasize(), 4)) + { + let selector := $Shr(calldataload(0), 224) + switch selector + case 0x70e8dbcb + { + // f(uint64) + if callvalue() + { + $Abort(99) + } + let param_0 := abi_decode_tuple_$u64$(4, calldatasize()) + A2_M_f(param_0) + let memPos := mload(0) + let memEnd := abi_encode_tuple_(memPos) + return(memPos, sub(memEnd, memPos)) + } + case 0xb3e8e519 + { + // g(uint64) + if callvalue() + { + $Abort(99) + } + let param_0 := abi_decode_tuple_$u64$(4, calldatasize()) + A2_M_g(param_0) + let memPos := mload(0) + let memEnd := abi_encode_tuple_(memPos) + return(memPos, sub(memEnd, memPos)) + } + case 0xf5f96a87 + { + // h1(uint64) + if callvalue() + { + $Abort(99) + } + let param_0 := abi_decode_tuple_$u64$(4, calldatasize()) + let ret_0 := A2_M_h1(param_0) + let memPos := mload(0) + let memEnd := abi_encode_tuple_$u64$(memPos, ret_0) + return(memPos, sub(memEnd, memPos)) + } + case 0x410dac7e + { + // h2(uint64) + if callvalue() + { + $Abort(99) + } + let param_0 := abi_decode_tuple_$u64$(4, calldatasize()) + A2_M_h2(param_0) + let memPos := mload(0) + let memEnd := abi_encode_tuple_(memPos) + return(memPos, sub(memEnd, memPos)) + } + default {} + } + $Abort(97) + function A2_M_f(x) { + let $t1, $t2, $t3, $t4, $t5, $t6, $t7, $t8 + let $block := 4 + for {} true {} { + { + // label L7 + // $t1 := 0 + $t1 := 0 + // $t2 := >($t0, $t1) + $t2 := $Gt(x, $t1) + // if ($t2) goto L0 else goto L2 + switch $t2 + case 0 { $block := 3 } + default { $block := 2 } + } + { + // label L0 + // $t3 := 2 + $t3 := 2 + // $t4 := %($t0, $t3) + $t4 := $Mod(x, $t3) + // $t5 := 0 + $t5 := 0 + // $t6 := ==($t4, $t5) + $t6 := $Eq($t4, $t5) + // if ($t6) goto L3 else goto L5 + switch $t6 + case 0 { $block := 6 } + default { $block := 5 } + } + { + // label L7 + // $t1 := 0 + $t1 := 0 + // $t2 := >($t0, $t1) + $t2 := $Gt(x, $t1) + // if ($t2) goto L0 else goto L2 + switch $t2 + case 0 { $block := 3 } + default { $block := 2 } + } + { + // label L3 + // $t7 := 1 + $t7 := 1 + // $t0 := +($t0, $t7) + x := $AddU64(x, $t7) + // goto L6 + $block := 7 + } + { + // label L5 + // $t8 := 2 + $t8 := 2 + // $t0 := -($t0, $t8) + x := $Sub(x, $t8) + // goto L6 + $block := 7 + } + { + // label L6 + // goto L7 + $block := 4 + } + { + // label L6 + // goto L7 + $block := 4 + } + } + { + // label L2 + // return () + leave + } + } + + function A2_M_g(x) { + let $t1, $t2, $t3, $t4, $t5 + { + // label L4 + // return () + leave + } + let $block := 4 + for {} true {} { + { + // label L3 + // $t1 := 1 + $t1 := 1 + // $t2 := >=($t0, $t1) + $t2 := $GtEq(x, $t1) + // if ($t2) goto L0 else goto L2 + switch $t2 + case 0 { $block := 3 } + default { $block := 2 } + } + { + // label L2 + // $t4 := 0 + $t4 := 0 + // $t5 := ==($t0, $t4) + $t5 := $Eq(x, $t4) + // if ($t5) goto L4 else goto L3 + switch $t5 + case 0 { $block := 4 } + default { $block := 5 } + } + { + // label L3 + // $t1 := 1 + $t1 := 1 + // $t2 := >=($t0, $t1) + $t2 := $GtEq(x, $t1) + // if ($t2) goto L0 else goto L2 + switch $t2 + case 0 { $block := 3 } + default { $block := 2 } + } + { + // label L2 + // $t4 := 0 + $t4 := 0 + // $t5 := ==($t0, $t4) + $t5 := $Eq(x, $t4) + // if ($t5) goto L4 else goto L3 + switch $t5 + case 0 { $block := 4 } + default { $block := 5 } + } + } + } + + function A2_M_h1(x) -> $result { + let tmp_$1, $t2, $t3, $t4, $t5 + { + // $t2 := 0 + $t2 := 0 + // $t3 := >($t0, $t2) + $t3 := $Gt(x, $t2) + // if ($t3) goto L0 else goto L2 + switch $t3 + case 0 { $block := 3 } + default { $block := 2 } + } + { + // label L0 + // $t4 := 1 + $t4 := 1 + // $t1 := $t4 + tmp_$1 := $t4 + // goto L3 + $block := 5 + } + { + // label L2 + // $t5 := 2 + $t5 := 2 + // $t1 := $t5 + tmp_$1 := $t5 + // goto L3 + $block := 5 + } + { + // label L3 + // return $t1 + $result := tmp_$1 + leave + } + } + + function A2_M_h2(x) { + let $t1, $t2, $t3 + { + // $t1 := 0 + $t1 := 0 + // $t2 := >($t0, $t1) + $t2 := $Gt(x, $t1) + // if ($t2) goto L0 else goto L2 + switch $t2 + case 0 { $block := 3 } + default { $block := 2 } + } + { + // label L2 + // return () + leave + } + { + // label L0 + // $t3 := 1 + $t3 := 1 + // abort($t3) + $Abort($t3) + } + } + + function abi_encode_tuple_(headStart ) -> tail { + tail := add(headStart, 0) + } + function abi_decode_tuple_$u64$(headStart, dataEnd) -> value_0 { + if slt(sub(dataEnd, headStart), 32) { $Abort(96) } + { + let offset := 0 + value_0 := abi_decode_u64(add(headStart, offset), dataEnd) + } + } + function abi_decode_u64(offset, end) -> value { + value := calldataload(offset) + validator_u64(value) + } + function validator_u64(value) { + if iszero(eq(value, cleanup_u64(value))) { $Abort(95) } + } + function cleanup_u64(value) -> cleaned { + cleaned := and(value, 0xffffffffffffffff) + } + function abi_encode_tuple_$u64$(headStart ,value_0) -> tail { + tail := add(headStart, 32) + abi_encode_u64(value_0, add(headStart, 0)) + } + function abi_encode_u64(value, pos) { + mstore(pos, cleanup_u64(value)) + } + function $Abort(code) { + mstore(0, code) + revert(24, 8) // TODO: store code as a string? + } + function $AbortBuiltin() { + $Abort(sub(0, 1)) + } + function $AddU64(x, y) -> r { + if lt(sub(0xffffffffffffffff, x), y) { $AbortBuiltin() } + r := add(x, y) + } + function $Sub(x, y) -> r { + if lt(x, y) { $AbortBuiltin() } + r := sub(x, y) + } + function $Mod(x, y) -> r { + if eq(y, 0) { $AbortBuiltin() } + r := mod(x, y) + } + function $Shr(x, y) -> r { + r := shr(y, x) + } + function $Gt(x, y) -> r { + r := gt(x, y) + } + function $GtEq(x, y) -> r { + r := or(gt(x, y), eq(x, y)) + } + function $Eq(x, y) -> r { + r := eq(x, y) + } + } + } +} + + +!! Errors compiling Yul + +Warning: Yul is still experimental. Please use the output with care. +Error: Variable not found or variable not lvalue. + --> :215:31: + | +215 | case 0 { $block := 3 } + | ^^^^^^ + +Error: Variable not found or variable not lvalue. + --> :216:31: + | +216 | default { $block := 2 } + | ^^^^^^ + +Error: Variable not found or variable not lvalue. + --> :225:21: + | +225 | $block := 5 + | ^^^^^^ + +Error: Variable not found or variable not lvalue. + --> :234:21: + | +234 | $block := 5 + | ^^^^^^ + +Error: Variable not found or variable not lvalue. + --> :253:31: + | +253 | case 0 { $block := 3 } + | ^^^^^^ + +Error: Variable not found or variable not lvalue. + --> :254:31: + | +254 | default { $block := 2 } + | ^^^^^^ diff --git a/language/evm/move-to-yul/tests/ControlStructures.move b/language/evm/move-to-yul/tests/ControlStructures.move index d08bb59bad..1bfb8a9523 100644 --- a/language/evm/move-to-yul/tests/ControlStructures.move +++ b/language/evm/move-to-yul/tests/ControlStructures.move @@ -1,4 +1,7 @@ // Tests basic control structures. +// +// experiment: apply-cfg-to-scf +// #[contract] module 0x2::M { #[callable] diff --git a/language/move-prover/bytecode/src/lib.rs b/language/move-prover/bytecode/src/lib.rs index 20b9833695..2007ef5f05 100644 --- a/language/move-prover/bytecode/src/lib.rs +++ b/language/move-prover/bytecode/src/lib.rs @@ -38,9 +38,11 @@ pub mod pipeline_factory; pub mod reaching_def_analysis; pub mod read_write_set_analysis; pub mod spec_instrumentation; +pub mod stackifier_topological_sort; pub mod stackless_bytecode; pub mod stackless_bytecode_generator; pub mod stackless_control_flow_graph; +pub mod stackless_structured_control_flow; pub mod usage_analysis; pub mod verification_analysis; pub mod verification_analysis_v2; diff --git a/language/move-prover/bytecode/src/stackifier_topological_sort.rs b/language/move-prover/bytecode/src/stackifier_topological_sort.rs new file mode 100644 index 0000000000..9afb277a03 --- /dev/null +++ b/language/move-prover/bytecode/src/stackifier_topological_sort.rs @@ -0,0 +1,168 @@ +// Copyright (c) The Diem Core Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::stackless_control_flow_graph::BlockId; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; + +#[derive(Clone, Debug)] +pub struct StackifierDependency { + pub num_prec: usize, + pub succ_set: HashSet, +} + +#[derive(Clone, Debug)] +pub struct StackifierTopologicalSort { + pub last_popped_block: Option, + top: HashMap, + // complete topology will not change while popping + complete_top: HashMap, +} + +impl StackifierDependency { + pub fn new() -> StackifierDependency { + StackifierDependency { + num_prec: 0, + succ_set: HashSet::new(), + } + } +} + +impl StackifierTopologicalSort { + #[inline] + pub fn new() -> StackifierTopologicalSort { + StackifierTopologicalSort { + last_popped_block: Option::None, + top: HashMap::new(), + complete_top: HashMap::new(), + } + } + + #[inline] + pub fn len(&self) -> usize { + self.top.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.top.is_empty() + } + + pub fn add_dependency(&mut self, prec: BlockId, succ: BlockId) { + Self::add_dependency_impl(&mut self.top, prec, succ); + Self::add_dependency_impl(&mut self.complete_top, prec, succ); + } + + fn add_dependency_impl( + top: &mut HashMap, + prec: BlockId, + succ: BlockId, + ) { + match top.entry(prec) { + Entry::Vacant(e) => { + let mut dep = StackifierDependency::new(); + dep.succ_set.insert(succ); + e.insert(dep); + } + Entry::Occupied(e) => { + e.into_mut().succ_set.insert(succ); + } + } + match top.entry(succ) { + Entry::Vacant(e) => { + let mut dep = StackifierDependency::new(); + dep.num_prec = 1; + e.insert(dep); + } + Entry::Occupied(e) => { + e.into_mut().num_prec += 1; + } + } + } + + pub fn pop(&mut self) -> Option> { + if let Some(peek_blocks) = self.peek() { + for peek_block in &peek_blocks { + self.remove(*peek_block); + self.last_popped_block = Some(*peek_block); + } + return Some(peek_blocks); + } + None + } + + pub fn remove(&mut self, prec: BlockId) -> Option { + let result = self.top.remove(&prec); + if let Some(ref p) = result { + for s in &p.succ_set { + if let Some(y) = self.top.get_mut(s) { + y.num_prec -= 1; + } + } + } + result + } + + /// Instead of picking random block w/o precessor after popping the last block, + /// we prioritize blocks with last popped block as their only precessor. + /// Also if the prioritized block is also the only successor of last popped block, + /// we will keep nesting them into one vector until either condition goes false. + pub fn peek(&self) -> Option> { + let mut priority_succ: Option = None; + if let Some(last_block) = self.last_popped_block { + priority_succ = self.find_priority_succ(last_block); + } + + // prioritize priority_succ if any, otherwise + // run original topological sorting. + let mut nested_blocks: Vec = vec![]; + if let Some(priority_succ_blk_id) = priority_succ { + nested_blocks.push(priority_succ_blk_id); + let mut curr_block = priority_succ_blk_id; + if let Some(curr_dependency) = self.complete_top.get(&curr_block) { + let mut succ_set = &curr_dependency.succ_set; + // nest blocks iff 1. curr_block has only one succ; + // && 2. the succ has only one precessor + while let Some(next_priority_succ_blk_id) = self.find_priority_succ(curr_block) { + if succ_set.len() != 1 { + break; + } + nested_blocks.push(next_priority_succ_blk_id); + curr_block = next_priority_succ_blk_id; + if let Some(dependency) = self.complete_top.get(&curr_block) { + succ_set = &dependency.succ_set; + } + } + } + } else if let Some(next_succ) = self + .top + .iter() + .filter(|&(_, v)| v.num_prec == 0) + .map(|(k, _)| k) + .next() + .cloned() + { + nested_blocks.push(next_succ); + } else { + return None; + } + Some(nested_blocks) + } + + // priority succ is a block whose only precessor is last block + pub fn find_priority_succ(&self, last_block: BlockId) -> Option { + let mut priority_succ = None; + if let Some(last_block_dependency) = self.complete_top.get(&last_block) { + for succ in &last_block_dependency.succ_set { + if let Some(complete_succ_dependency) = self.complete_top.get(succ) { + let succ_prec_num = complete_succ_dependency.num_prec; + if succ_prec_num == 1 { + priority_succ = Some(*succ); + break; + } + } + } + } + priority_succ + } +} diff --git a/language/move-prover/bytecode/src/stackless_control_flow_graph.rs b/language/move-prover/bytecode/src/stackless_control_flow_graph.rs index ea582703bc..45eb883c92 100644 --- a/language/move-prover/bytecode/src/stackless_control_flow_graph.rs +++ b/language/move-prover/bytecode/src/stackless_control_flow_graph.rs @@ -6,6 +6,7 @@ use crate::{ function_target::FunctionTarget, + graph::{Graph as ProverGraph, NaturalLoop}, stackless_bytecode::{Bytecode, Label}, }; use move_binary_format::file_format::CodeOffset; @@ -16,10 +17,10 @@ type Map = BTreeMap; type Set = BTreeSet; pub type BlockId = CodeOffset; -#[derive(Debug)] -struct Block { - successors: Vec, - content: BlockContent, +#[derive(Clone, Debug)] +pub struct Block { + pub successors: Vec, + pub content: BlockContent, } #[derive(Copy, Clone, Debug)] @@ -31,10 +32,11 @@ pub enum BlockContent { Dummy, } +#[derive(Clone, Debug)] pub struct StacklessControlFlowGraph { - entry_block_id: BlockId, - blocks: Map, - backward: bool, + pub entry_block_id: BlockId, + pub blocks: Map, + pub backward: bool, } const DUMMY_ENTRANCE: BlockId = 0; @@ -232,6 +234,37 @@ impl StacklessControlFlowGraph { } } +impl StacklessControlFlowGraph { + pub fn to_prover_graph(&self) -> ProverGraph { + let nodes: Vec = self.blocks.keys().copied().collect(); + let mut edges: Vec<(BlockId, BlockId)> = vec![]; + for (block_id, block) in &self.blocks { + for successor_id in &block.successors { + edges.push((*block_id, *successor_id)); + } + } + ProverGraph::new(DUMMY_ENTRANCE, nodes, edges) + } + + pub fn reduce_cfg_loop( + &mut self, + graph: &ProverGraph, + ) -> Map> { + let loops: Option>> = graph.compute_reducible(); + let mut loop_map: Map> = Map::new(); + if loops.is_some() { + for one_loop in loops.unwrap() { + self.blocks.remove(&one_loop.loop_latch); + for loop_body_block in &one_loop.loop_body { + self.blocks.remove(loop_body_block); + } + loop_map.insert(one_loop.loop_header, one_loop); + } + } + loop_map + } +} + // CFG dot graph generation struct DotCFGBlock<'env> { block_id: BlockId, diff --git a/language/move-prover/bytecode/src/stackless_structured_control_flow.rs b/language/move-prover/bytecode/src/stackless_structured_control_flow.rs new file mode 100644 index 0000000000..cd9923f313 --- /dev/null +++ b/language/move-prover/bytecode/src/stackless_structured_control_flow.rs @@ -0,0 +1,23 @@ +// Copyright (c) The Diem Core Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::stackifier_topological_sort::StackifierTopologicalSort; +use crate::stackless_control_flow_graph::StacklessControlFlowGraph; + +pub struct StacklessStructuredControlFlow { + pub top_sort: StackifierTopologicalSort, +} + +impl StacklessStructuredControlFlow { + pub fn new(cfg: &StacklessControlFlowGraph) -> Self { + let mut topological_sort = StackifierTopologicalSort::new(); + for (block_id, block) in &cfg.blocks { + for successor_id in &block.successors { + topological_sort.add_dependency(*block_id, *successor_id); + } + } + Self { + top_sort: topological_sort, + } + } +}