///|
#valtype
pub struct CfgEdge {
kind : CfgEdgeKind
block : BlockId
} derive(Eq, Debug)
///|
pub impl Show for CfgEdge with fn output(self, logger) {
let { kind, block, } = self
logger.write_string("{kind: ")
kind.output(logger)
logger.write_string(", block: ")
block.output(logger)
logger.write_char('}')
}
///|
pub struct HotBlockInfo {
id : BlockId
nodes : Array[NodeId]
preds : Array[CfgEdge]
succs : Array[CfgEdge]
is_synthetic : Bool
region_holder : NodeId
start_slot : Int32
end_slot : Int32
} derive(Eq, Debug)
///|
pub impl Show for HotBlockInfo with fn output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
#valtype
pub struct CfgRegionRootBlock {
region_holder : NodeId
slot : Int32
block : BlockId
} derive(Eq, Debug)
///|
pub impl Show for CfgRegionRootBlock with fn output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
pub struct HotCfg {
operands_expanded : Bool
blocks : Array[HotBlockInfo]
node_to_block : Array[BlockId]
region_roots : Array[CfgRegionRootBlock]
mut entry_block : BlockId
mut exit_block : BlockId
mut exceptional_exit_block : BlockId?
} derive(Eq, Debug)
///|
priv struct CfgBuilder {
cfg : HotCfg
label_targets : Array[BlockId]
conservative_exception_entries : Bool
operand_seen : Array[Bool]?
mut source_order : HotSourceOrderFacts?
mut operand_minima : Array[Int32]?
continuation_targets : Array[Array[LabelId]?]
}
///|
priv struct CfgRegionBuild {
entry_block : BlockId?
exit_blocks : Array[BlockId]
}
///|
#valtype
priv struct CfgRegionSegment {
block_id : BlockId
end_slot : Int32
last_node : NodeId
}
///|
// Scalar rows avoid a separately allocated record for every segment. Keep the
// public CFG block and root mappings unchanged; this arena is builder-local.
#valtype
priv struct CfgRegionSegments {
rows : Array[Int32]
}
///|
fn CfgRegionSegments::new() -> CfgRegionSegments {
{ rows: [], }
}
///|
#inline
fn CfgRegionSegments::push(
self : CfgRegionSegments,
block_id : BlockId,
end_slot : Int32,
last_node : NodeId,
) -> Unit {
self.rows.push(block_id)
self.rows.push(end_slot)
self.rows.push(last_node)
}
///|
#inline
fn CfgRegionSegments::length(self : CfgRegionSegments) -> Int32 {
self.rows.length() / 3
}
///|
#inline
fn CfgRegionSegments::block_id(
self : CfgRegionSegments,
index : Int32,
) -> BlockId {
self.rows[index * 3]
}
///|
#inline
fn CfgRegionSegments::get(
self : CfgRegionSegments,
index : Int32,
) -> CfgRegionSegment {
let offset = index * 3
{
block_id: self.rows[offset],
end_slot: self.rows[offset + 1],
last_node: self.rows[offset + 2],
}
}
///|
fn cfg_builder_new(
func : HotFunc,
conservative_exception_entries : Bool,
expand_operand_control : Bool,
) -> CfgBuilder {
let node_count = hot_node_count(func)
let node_to_block = Array::make(node_count, -1)
let mut has_continuations = false
// cfg_build verifies side-table references before reaching this constructor.
// Every valid Continuation owns a branch-table entry, including empty handlers.
// Nonempty tables remain conservative: br_table and retired payloads also use
// this arena, so retain the full live-node scan in that case.
if node_count > 0 && !func.branch_tables.is_empty() {
for node in 0.. NodeId {
region_ref.holder
}
///|
fn cfg_builder_add_block(
builder : CfgBuilder,
nodes : Array[NodeId],
is_synthetic : Bool,
region_holder~ : NodeId = HOT_NONE_NODE,
start_slot~ : Int32 = -1,
end_slot~ : Int32 = -1,
) -> BlockId {
let id = builder.cfg.blocks.length()
builder.cfg.blocks.push({
id,
nodes,
preds: [],
succs: [],
is_synthetic,
region_holder,
start_slot,
end_slot,
})
for node_id in nodes {
builder.cfg.node_to_block[node_id] = id
}
id
}
///|
fn cfg_builder_add_edge(
builder : CfgBuilder,
from : BlockId,
to : BlockId,
kind : CfgEdgeKind,
) -> Unit {
builder.cfg.blocks[from].succs.push({ kind, block: to, })
builder.cfg.blocks[to].preds.push({ kind, block: from, })
}
///|
fn cfg_builder_label_target_set(
builder : CfgBuilder,
label_id : LabelId,
block_id : BlockId,
) -> Unit {
builder.label_targets[label_id] = block_id
}
///|
fn cfg_builder_label_target_get(
builder : CfgBuilder,
label_id : LabelId,
) -> BlockId {
if label_id == HOT_IMPLICIT_FUNCTION_LABEL {
return builder.cfg.exit_block
}
match builder.label_targets.get(label_id) {
Some(block_id) if block_id >= 0 => block_id
_ =>
abort(
"cfg build requires every referenced hot label to resolve to a built block",
)
}
}
///|
fn cfg_builder_exception_label_target_get(
builder : CfgBuilder,
label_id : LabelId,
) -> BlockId {
if label_id == HOT_IMPLICIT_FUNCTION_LABEL {
return cfg_builder_exceptional_exit_block(builder)
}
cfg_builder_label_target_get(builder, label_id)
}
///|
fn cfg_builder_exceptional_exit_block(builder : CfgBuilder) -> BlockId {
match builder.cfg.exceptional_exit_block {
Some(block_id) => block_id
None => {
let block_id = cfg_builder_add_block(builder, [], true)
builder.cfg.exceptional_exit_block = Some(block_id)
block_id
}
}
}
///|
fn cfg_builder_connect_exits(
builder : CfgBuilder,
exits : Array[BlockId],
target : BlockId?,
) -> Unit {
match target {
Some(block_id) =>
for exit_block in exits {
cfg_builder_add_edge(
builder,
exit_block,
block_id,
CfgEdgeKind::FallthroughEdge,
)
}
None => ()
}
}
///|
fn cfg_region_segment_break_after_drop_operand_control(
func : HotFunc,
node_id : NodeId,
) -> Bool {
if node_id < 0 || !hot_node_is_live(func, node_id) {
return false
}
if hot_node_get(func, node_id).op != HotOp::Drop {
return false
}
let child_id = hot_child_get(func, node_id, 0)
cfg_dropped_branch_operand(func, node_id) is Some(_) ||
ssa_simple_drop_result_if_allowed(func, node_id, 0, child_id) ||
ssa_simple_drop_value_block_allowed(func, node_id, 0, child_id)
}
///|
fn cfg_dropped_branch_operand(func : HotFunc, node_id : NodeId) -> NodeId? {
if hot_node_get(func, node_id).op != HotOp::Drop {
return None
}
let child = hot_child_get(func, node_id, 0)
if child < 0 || !hot_node_is_live(func, child) {
return None
}
match hot_node_get(func, child).op {
HotOp::BrIf
| HotOp::BrOnNull
| HotOp::BrOnNonNull
| HotOp::BrOnCast
| HotOp::BrOnCastFail => Some(child)
_ => None
}
}
///|
// Every selected carried value has value_order < current_order, and its own
// allocation order is <= value_order. If all future operand orders are at
// least every earlier root's current_order, no dependency can be selected.
// Memoize only operand minima, without effects/local rows, for one immutable
// CFG build. Shared operands are visited once; -2 is conservative on a cycle.
fn cfg_operand_minimum_order(
func : HotFunc,
node_id : NodeId,
minima : Array[Int32],
visits~ : Array[Int32]? = None,
) -> Int32 {
if node_id == HOT_NONE_NODE {
return hot_node_count(func)
}
if node_id < 0 || !hot_node_is_live(func, node_id) {
return -2
}
if minima[node_id] != -1 {
return minima[node_id]
}
minima[node_id] = -2
if visits is Some(count) {
count[0] += 1
}
let node = hot_node_get(func, node_id)
// The collector carries only nonpure results. Pure leaves and void nodes
// cannot contribute their own order; every operand is still visited below.
let pure_leaf = match node.op {
HotOp::Const | HotOp::RefNull | HotOp::RefFunc =>
effects_mask_for_flags(node.flags) == 0U
_ => false
}
let mut minimum = if pure_leaf || hot_node_result_arity(func, node_id) == 0 {
hot_node_count(func)
} else {
node.order
}
for child in hot_lower_impl_input_info(func, node_id).nodes {
minimum = minimum.min(
cfg_operand_minimum_order(func, child, minima, visits~),
)
}
minima[node_id] = minimum
minimum
}
///|
fn cfg_region_may_need_source_order(
func : HotFunc,
roots : Array[NodeId],
minima : Array[Int32],
) -> Bool {
let mut previous_order = 0
for root in roots {
if cfg_operand_minimum_order(func, root, minima) < previous_order {
return true
}
previous_order = previous_order.max(
hot_node_get(func, root).order.min(root),
)
}
false
}
///|
// The segment extent is already known. Copy once into its own exact-sized
// primitive row instead of growing an empty array one node at a time.
fn cfg_copy_segment_nodes(
nodes : Array[NodeId],
start : Int32,
end : Int32,
) -> Array[NodeId] {
if end == start + 1 {
[nodes[start]]
} else {
nodes[start:end].to_owned()
}
}
///|
fn cfg_builder_region_segments(
builder : CfgBuilder,
func : HotFunc,
region_ref : HotRegionRef,
handler_target~ : BlockId? = None,
) -> CfgRegionSegments {
let segments = CfgRegionSegments::new()
let region_holder = cfg_builder_region_holder(region_ref)
let root_count = hot_region_root_count(func, region_ref)
let expanded = builder.operand_seen is Some(_)
let ordered_nodes : Array[NodeId] = []
let root_slots : Array[Int32] = []
let roots : Array[NodeId] = []
if expanded {
for slot = 0; slot < root_count; slot = slot + 1 {
roots.push(hot_region_get(func, region_ref, slot))
}
}
let facts = if expanded && root_count > 1 {
let minima = match builder.operand_minima {
Some(minima) => minima
None => {
let minima = Array::make(hot_node_count(func), -1)
builder.operand_minima = Some(minima)
minima
}
}
// Facts from an earlier region do not establish a dependency here.
// Reuse the immutable operand minima to prove each region separately.
if cfg_region_may_need_source_order(func, roots, minima) {
match builder.source_order {
Some(facts) => Some(facts)
None => {
let facts = HotSourceOrderFacts::new(func)
builder.source_order = Some(facts)
Some(facts)
}
}
} else {
None
}
} else {
None
}
let suffix_minima = match facts {
Some(facts) => hot_source_order_future_suffix_minima(func, roots, facts)
None => None
}
for slot = 0; slot < root_count; slot = slot + 1 {
let root_id = if expanded {
roots[slot]
} else {
hot_region_get(func, region_ref, slot)
}
match builder.operand_seen {
Some(seen) => {
// Even after another region initialized facts, the final root has
// no future statement to constrain. Operand expansion still runs.
if slot + 1 < root_count && facts is Some(_) {
let facts = facts.unwrap()
for
dependency in hot_source_order_preceding_dependencies(
func,
root_id,
roots,
slot + 1,
suffix_minima,
facts,
) {
cfg_builder_collect_operand_nodes(
builder, func, dependency, slot, seen, ordered_nodes, root_slots,
)
}
}
cfg_builder_collect_operand_nodes(
builder, func, root_id, slot, seen, ordered_nodes, root_slots,
)
}
None => {
ordered_nodes.push(root_id)
root_slots.push(slot)
}
}
}
let mut start = 0
while start < ordered_nodes.length() {
let mut end = start + 1
while end < ordered_nodes.length() {
let previous = ordered_nodes[end - 1]
let previous_op = hot_node_get(func, previous).op
if cfg_op_is_terminator(previous_op) ||
(
handler_target is Some(_) &&
(
previous_op is HotOp::Call ||
previous_op is HotOp::CallIndirect ||
previous_op is HotOp::CallRef
)
) {
break
}
let next = ordered_nodes[end]
if hot_is_control_node(func, next) {
break
}
if cfg_expression_has_continuation_targets(
func,
previous,
builder.continuation_targets,
) ||
(
previous_op is HotOp::Drop &&
cfg_region_segment_break_after_drop_operand_control(func, previous)
) {
break
}
end = end + 1
}
let nodes = cfg_copy_segment_nodes(ordered_nodes, start, end)
let block_id = cfg_builder_add_block(
builder,
nodes,
false,
region_holder~,
start_slot=root_slots[start],
end_slot=root_slots[end - 1] + 1,
)
for index = start; index < end; index = index + 1 {
let slot = root_slots[index]
// Expanded operands reuse the immutable root snapshot already built
// for source ordering. Without expansion, every listed node is a root.
if !expanded || ordered_nodes[index] == roots[slot] {
builder.cfg.region_roots.push({ region_holder, slot, block: block_id, })
}
}
segments.push(block_id, root_slots[end - 1] + 1, ordered_nodes[end - 1])
start = end
}
segments
}
///|
// Repeated entry evaluations can share one canonical CFG owner only when they
// are straight-line. Keep branching, caught-call and label scopes on their
// existing structured paths; do not invent a loop to approximate repetition.
fn cfg_repeat_entry_is_straight_line(
func : HotFunc,
root : NodeId,
seen : @set.Set[NodeId],
) -> Bool {
let work = [root]
while !work.is_empty() {
let id = work.pop().unwrap()
if id < 0 || seen.contains(id) {
continue
}
seen.add(id)
let node = hot_node_get(func, id)
if (node.flags & (HOT_FLAG_CALL | HOT_FLAG_BRANCH | HOT_FLAG_TERMINATOR)) !=
0U ||
(hot_is_control_node(func, id) && node.op != HotOp::Block) {
return false
}
// Entry-bearing blocks can forward an earlier captured stack value. That
// value needs its existing structured ownership, rather than replay here.
if node.op == HotOp::Block && node.imm1 > 0 {
return false
}
for child in hot_child_span(func, id) {
work.push(child)
}
}
true
}
///|
fn cfg_builder_collect_inline_entry(
builder : CfgBuilder,
func : HotFunc,
node_id : NodeId,
root_slot : Int32,
occurrence_seen : @set.Set[NodeId],
nodes : Array[NodeId],
root_slots : Array[Int32],
) -> Unit {
if node_id < 0 || occurrence_seen.contains(node_id) {
return
}
occurrence_seen.add(node_id)
if builder.operand_seen is Some(seen) {
// Body consumers forward these already-produced lanes. They must not
// replay the producer after the loop label is entered.
seen[node_id] = true
}
let node = hot_node_get(func, node_id)
if node.op == HotOp::Block {
for slot in 0.. 1 {
match builder.source_order {
Some(facts) => Some(facts)
None => {
let facts = HotSourceOrderFacts::new(func)
builder.source_order = Some(facts)
Some(facts)
}
}
} else {
None
}
let suffix_minima = match facts {
Some(facts) => hot_source_order_future_suffix_minima(func, roots, facts)
None => None
}
for slot in 0.. Unit {
let count = hot_label_branch_arity(func, node.imm0)
let mut slot = 0
while slot < count {
let producer = func.children[node.child_base + slot]
let mut end = slot + 1
while end < count && func.children[node.child_base + end] == producer {
end += 1
}
let span = hot_node_result_arity(func, producer).max(1)
let evaluations = (end - slot + span - 1) / span
if evaluations > 1 {
// Reuse only within this producer group. A wide earlier producer must
// not impose its retained set capacity on later small producers.
let occurrence_seen : @set.Set[NodeId] = @set.Set([])
if cfg_repeat_entry_is_straight_line(func, producer, occurrence_seen) {
// All actions remain in one prefix block. Reuse this sparse mark set
// for the proof and for every complete evaluation in this prefix.
for _ in 0.. Unit {
if node_id < 0 || !hot_node_is_live(func, node_id) || seen[node_id] {
return
}
seen[node_id] = true
let node = hot_node_get(func, node_id)
// The control header owns its condition and value operands. Its branch
// regions are built separately with their actual control-flow edges.
if node.op == HotOp::Loop {
cfg_builder_collect_loop_entries(
builder, func, node, root_slot, seen, nodes, root_slots,
)
}
for index = 0; index < node.child_count; index = index + 1 {
let slot = hot_child_evaluation_slot(node, index)
if !use_def_region_slot(node, slot) {
cfg_builder_collect_operand_nodes(
builder,
func,
hot_child_get(func, node_id, slot),
root_slot,
seen,
nodes,
root_slots,
)
}
}
nodes.push(node_id)
root_slots.push(root_slot)
}
///|
fn cfg_builder_region_build_empty(
builder : CfgBuilder,
region_ref : HotRegionRef,
force_entry : Bool,
) -> CfgRegionBuild {
if !force_entry {
return { entry_block: None, exit_blocks: [], }
}
let block_id = cfg_builder_add_block(
builder,
[],
true,
region_holder=cfg_builder_region_holder(region_ref),
start_slot=0,
end_slot=0,
)
{ entry_block: Some(block_id), exit_blocks: [block_id], }
}
///|
fn cfg_builder_maybe_continuation(
builder : CfgBuilder,
region_ref : HotRegionRef,
slot : Int32,
next_block : BlockId?,
) -> (BlockId?, Bool) {
match next_block {
Some(block_id) => (Some(block_id), false)
None => {
let block_id = cfg_builder_add_block(
builder,
[],
true,
region_holder=cfg_builder_region_holder(region_ref),
start_slot=slot,
end_slot=slot,
)
(Some(block_id), true)
}
}
}
///|
fn cfg_builder_branch_targets(
builder : CfgBuilder,
func : HotFunc,
node_id : NodeId,
) -> Array[BlockId] {
let targets : Array[BlockId] = []
match hot_node_get(func, node_id).op {
HotOp::BrTable => {
for label_id in hot_branch_table_targets_for_node(func, node_id) {
targets.push(cfg_builder_label_target_get(builder, label_id))
}
match hot_branch_target(func, node_id) {
Some(label_id) =>
targets.push(cfg_builder_label_target_get(builder, label_id))
None => ()
}
}
_ =>
match hot_branch_target(func, node_id) {
Some(label_id) =>
targets.push(cfg_builder_label_target_get(builder, label_id))
None => ()
}
}
targets
}
///|
fn cfg_builder_exception_branch_targets(
builder : CfgBuilder,
func : HotFunc,
node_id : NodeId,
) -> Array[BlockId] {
let targets : Array[BlockId] = []
match hot_branch_target(func, node_id) {
Some(label_id) =>
targets.push(cfg_builder_exception_label_target_get(builder, label_id))
None => ()
}
targets
}
///|
fn cfg_builder_exception_target(
builder : CfgBuilder,
handler_target : BlockId?,
) -> BlockId {
match handler_target {
Some(block_id) => block_id
None => cfg_builder_exceptional_exit_block(builder)
}
}
///|
fn cfg_builder_connect_result_if(
builder : CfgBuilder,
func : HotFunc,
if_id : NodeId,
current_block : BlockId,
continuation : BlockId,
handler_target : BlockId?,
) -> Unit {
cfg_builder_label_target_set(
builder,
hot_control_node_label(func, if_id),
continuation,
)
let then_build = cfg_builder_build_region(
builder,
func,
hot_region_then_ref(func, if_id),
handler_target,
force_entry=true,
)
cfg_builder_add_edge(
builder,
current_block,
then_build.entry_block.unwrap(),
CfgEdgeKind::FallthroughEdge,
)
cfg_builder_connect_exits(builder, then_build.exit_blocks, Some(continuation))
match hot_region_else_ref(func, if_id) {
Some(else_ref) => {
let else_build = cfg_builder_build_region(
builder,
func,
else_ref,
handler_target,
force_entry=true,
)
cfg_builder_add_edge(
builder,
current_block,
else_build.entry_block.unwrap(),
CfgEdgeKind::FallthroughEdge,
)
cfg_builder_connect_exits(
builder,
else_build.exit_blocks,
Some(continuation),
)
}
None =>
cfg_builder_add_edge(
builder,
current_block,
continuation,
CfgEdgeKind::FallthroughEdge,
)
}
}
///|
fn cfg_builder_process_block(
builder : CfgBuilder,
func : HotFunc,
region_ref : HotRegionRef,
segments : CfgRegionSegments,
segment_idx : Int32,
handler_target : BlockId?,
exits : Array[BlockId],
) -> Unit {
let segment = segments.get(segment_idx)
let current_block = segment.block_id
let next_block = if segment_idx + 1 < segments.length() {
Some(segments.block_id(segment_idx + 1))
} else {
None
}
let last_node = segment.last_node
let node = hot_node_get(func, last_node)
if !builder.continuation_targets.is_empty() {
for root in builder.cfg.blocks[current_block].nodes {
for
label in cfg_expression_continuation_targets(
func,
root,
builder.continuation_targets,
) {
cfg_builder_add_edge(
builder,
current_block,
cfg_builder_label_target_get(builder, label),
CfgEdgeKind::BranchEdge,
)
}
}
}
match node.op {
HotOp::Block => {
let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
builder,
region_ref,
segment.end_slot,
next_block,
)
cfg_builder_label_target_set(
builder,
hot_control_node_label(func, last_node),
continuation.unwrap(),
)
let body_build = cfg_builder_build_region(
builder,
func,
hot_region_body_ref(func, last_node),
handler_target,
force_entry=true,
)
cfg_builder_add_edge(
builder,
current_block,
body_build.entry_block.unwrap(),
CfgEdgeKind::FallthroughEdge,
)
cfg_builder_connect_exits(builder, body_build.exit_blocks, continuation)
if synthetic_exit {
exits.push(continuation.unwrap())
}
}
HotOp::Loop => {
// The forced body entry is allocated next. Branches enter the body;
// they must not evaluate the initial stack arguments again.
cfg_builder_label_target_set(
builder,
hot_control_node_label(func, last_node),
builder.cfg.blocks.length(),
)
let body_build = cfg_builder_build_region(
builder,
func,
hot_region_body_ref(func, last_node),
handler_target,
force_entry=true,
)
cfg_builder_add_edge(
builder,
current_block,
body_build.entry_block.unwrap(),
CfgEdgeKind::FallthroughEdge,
)
cfg_builder_connect_exits(builder, body_build.exit_blocks, next_block)
if next_block == None {
for exit_block in body_build.exit_blocks {
exits.push(exit_block)
}
}
}
HotOp::If => {
let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
builder,
region_ref,
segment.end_slot,
next_block,
)
cfg_builder_connect_result_if(
builder,
func,
last_node,
current_block,
continuation.unwrap(),
handler_target,
)
if synthetic_exit {
exits.push(continuation.unwrap())
}
}
HotOp::Drop => {
let operand_id = hot_child_get(func, last_node, 0)
if builder.operand_seen == None &&
operand_id >= 0 &&
hot_node_is_live(func, operand_id) &&
hot_node_get(func, operand_id).op == HotOp::If &&
ssa_simple_drop_result_if_allowed(func, last_node, 0, operand_id) {
let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
builder,
region_ref,
segment.end_slot,
next_block,
)
cfg_builder_connect_result_if(
builder,
func,
operand_id,
current_block,
continuation.unwrap(),
handler_target,
)
if synthetic_exit {
exits.push(continuation.unwrap())
}
} else if cfg_dropped_branch_operand(func, last_node) is Some(branch) {
for target in cfg_builder_branch_targets(builder, func, branch) {
cfg_builder_add_edge(
builder,
current_block,
target,
CfgEdgeKind::BranchEdge,
)
}
match next_block {
Some(block_id) =>
cfg_builder_add_edge(
builder,
current_block,
block_id,
CfgEdgeKind::FallthroughEdge,
)
None => exits.push(current_block)
}
} else {
match next_block {
Some(block_id) =>
cfg_builder_add_edge(
builder,
current_block,
block_id,
CfgEdgeKind::FallthroughEdge,
)
None => exits.push(current_block)
}
}
}
HotOp::Try => {
let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
builder,
region_ref,
segment.end_slot,
next_block,
)
cfg_builder_label_target_set(
builder,
hot_control_node_label(func, last_node),
continuation.unwrap(),
)
let catch_build = match hot_region_catch_ref(func, last_node) {
Some(catch_ref) =>
Some(
cfg_builder_build_region(
builder,
func,
catch_ref,
handler_target,
force_entry=true,
),
)
None => None
}
let body_build = cfg_builder_build_region(
builder,
func,
hot_region_body_ref(func, last_node),
match catch_build {
Some(build) => build.entry_block
None => handler_target
},
force_entry=true,
)
cfg_builder_add_edge(
builder,
current_block,
body_build.entry_block.unwrap(),
CfgEdgeKind::FallthroughEdge,
)
cfg_builder_connect_exits(builder, body_build.exit_blocks, continuation)
match catch_build {
Some(build) => {
if builder.conservative_exception_entries {
cfg_builder_add_edge(
builder,
current_block,
build.entry_block.unwrap(),
CfgEdgeKind::ExceptionalEdge,
)
}
cfg_builder_connect_exits(builder, build.exit_blocks, continuation)
}
None => ()
}
if synthetic_exit {
exits.push(continuation.unwrap())
}
}
HotOp::TryTable => {
let (continuation, synthetic_exit) = cfg_builder_maybe_continuation(
builder,
region_ref,
segment.end_slot,
next_block,
)
cfg_builder_label_target_set(
builder,
hot_control_node_label(func, last_node),
continuation.unwrap(),
)
let catch_info = hot_catch_info_get(func, node.imm1)
let catch_build = cfg_builder_build_region(
builder,
func,
hot_region_catch_list_ref(func, last_node),
handler_target,
force_entry=true,
)
let body_build = cfg_builder_build_region(
builder,
func,
hot_region_body_ref(func, last_node),
catch_build.entry_block,
force_entry=true,
)
cfg_builder_add_edge(
builder,
current_block,
body_build.entry_block.unwrap(),
CfgEdgeKind::FallthroughEdge,
)
if builder.conservative_exception_entries {
cfg_builder_add_edge(
builder,
current_block,
catch_build.entry_block.unwrap(),
CfgEdgeKind::ExceptionalEdge,
)
}
cfg_builder_connect_exits(builder, body_build.exit_blocks, continuation)
let catches_exhaustive = catch_info.catches.any(fn(catch_) {
match catch_ {
HotCatchArm::CatchAllArm(_) | HotCatchArm::CatchAllRefArm(_) => true
_ => false
}
})
for catch_exit in catch_build.exit_blocks {
for catch_ in catch_info.catches {
let label_id = match catch_ {
HotCatchArm::CatchArm(_, target)
| HotCatchArm::CatchRefArm(_, target)
| HotCatchArm::CatchAllArm(target)
| HotCatchArm::CatchAllRefArm(target) => target
}
cfg_builder_add_edge(
builder,
catch_exit,
cfg_builder_exception_label_target_get(builder, label_id),
CfgEdgeKind::ExceptionalEdge,
)
}
if !catches_exhaustive {
cfg_builder_add_edge(
builder,
catch_exit,
cfg_builder_exception_target(builder, handler_target),
CfgEdgeKind::ExceptionalEdge,
)
}
}
if synthetic_exit {
exits.push(continuation.unwrap())
}
}
HotOp::Br =>
for target in cfg_builder_branch_targets(builder, func, last_node) {
cfg_builder_add_edge(
builder,
current_block,
target,
CfgEdgeKind::BranchEdge,
)
}
HotOp::BrIf
| HotOp::BrOnNull
| HotOp::BrOnNonNull
| HotOp::BrOnCast
| HotOp::BrOnCastFail => {
for target in cfg_builder_branch_targets(builder, func, last_node) {
cfg_builder_add_edge(
builder,
current_block,
target,
CfgEdgeKind::BranchEdge,
)
}
match next_block {
Some(block_id) =>
cfg_builder_add_edge(
builder,
current_block,
block_id,
CfgEdgeKind::FallthroughEdge,
)
None => exits.push(current_block)
}
}
HotOp::BrTable =>
for target in cfg_builder_branch_targets(builder, func, last_node) {
cfg_builder_add_edge(
builder,
current_block,
target,
CfgEdgeKind::BranchEdge,
)
}
HotOp::Return
| HotOp::ReturnCall
| HotOp::ReturnCallIndirect
| HotOp::ReturnCallRef =>
cfg_builder_add_edge(
builder,
current_block,
builder.cfg.exit_block,
CfgEdgeKind::ReturnEdge,
)
HotOp::Throw | HotOp::ThrowRef | HotOp::Rethrow =>
cfg_builder_add_edge(
builder,
current_block,
cfg_builder_exception_target(builder, handler_target),
CfgEdgeKind::ExceptionalEdge,
)
HotOp::Call | HotOp::CallIndirect | HotOp::CallRef => {
// Argument writes precede this edge; result consumers belong to the
// normal continuation. Traps and tail calls are not caught calls.
if handler_target is Some(_) {
cfg_builder_add_edge(
builder,
current_block,
cfg_builder_exception_target(builder, handler_target),
CfgEdgeKind::ExceptionalEdge,
)
}
match next_block {
Some(block_id) =>
cfg_builder_add_edge(
builder,
current_block,
block_id,
CfgEdgeKind::FallthroughEdge,
)
None => exits.push(current_block)
}
}
HotOp::Delegate =>
for
target in cfg_builder_exception_branch_targets(builder, func, last_node) {
cfg_builder_add_edge(
builder,
current_block,
target,
CfgEdgeKind::ExceptionalEdge,
)
}
HotOp::Unreachable =>
cfg_builder_add_edge(
builder,
current_block,
builder.cfg.exit_block,
CfgEdgeKind::UnreachableExitEdge,
)
_ =>
match next_block {
Some(block_id) =>
cfg_builder_add_edge(
builder,
current_block,
block_id,
CfgEdgeKind::FallthroughEdge,
)
None => exits.push(current_block)
}
}
}
///|
fn cfg_builder_build_region(
builder : CfgBuilder,
func : HotFunc,
region_ref : HotRegionRef,
handler_target : BlockId?,
force_entry~ : Bool = false,
) -> CfgRegionBuild {
if hot_region_root_count(func, region_ref) == 0 {
return cfg_builder_region_build_empty(builder, region_ref, force_entry)
}
let segments = cfg_builder_region_segments(
builder,
func,
region_ref,
handler_target~,
)
if segments.length() == 0 {
return cfg_builder_region_build_empty(builder, region_ref, force_entry)
}
let exits : Array[BlockId] = []
for idx = 0; idx < segments.length(); idx = idx + 1 {
cfg_builder_process_block(
builder, func, region_ref, segments, idx, handler_target, exits,
)
}
{ entry_block: Some(segments.block_id(0)), exit_blocks: exits, }
}
///|
fn cfg_edge_kind_bit(kind : CfgEdgeKind) -> UInt32 {
match kind {
FallthroughEdge => 1U
BranchEdge => 2U
ReturnEdge => 4U
ExceptionalEdge => 8U
UnreachableExitEdge => 16U
}
}
///|
fn cfg_indexed_edge_symmetry_error(cfg : HotCfg) -> String? {
let count = cfg.blocks.length()
let heads = Array::make(count, -1)
let mut edge_count = 0
for block in cfg.blocks {
let wide_source = block.succs.length() > 8
for edge in block.succs {
if edge.block < 0 || edge.block >= count {
return Some("cfg successor target must reference a valid block id")
}
if wide_source || cfg.blocks[edge.block].preds.length() > 8 {
edge_count += 1
}
}
}
let rows = Array::make(edge_count * 3, 0)
let mut next_row = 0
for source in 0.. 8
for edge in successors {
if !wide_source && cfg.blocks[edge.block].preds.length() <= 8 {
let mut found = false
for pred in cfg.blocks[edge.block].preds {
if pred.block == source && pred.kind == edge.kind {
found = true
break
}
}
if !found {
return Some("cfg successors and predecessors must stay symmetric")
}
continue
}
let row = next_row
rows[row] = source
rows[row + 1] = cfg_edge_kind_bit(edge.kind).reinterpret_as_int()
rows[row + 2] = heads[edge.block]
next_row += 3
heads[edge.block] = row
}
}
let masks = Array::make(count, 0U)
for destination in 0.. 8
for edge in predecessors {
let source = edge.block
if source < 0 || source >= count {
return Some("cfg predecessor source must reference a valid block id")
}
if !wide_destination && cfg.blocks[source].succs.length() <= 8 {
let mut found = false
for succ in cfg.blocks[source].succs {
if succ.block == destination && succ.kind == edge.kind {
found = true
break
}
}
if !found {
return Some("cfg predecessors and successors must stay symmetric")
}
continue
}
masks[source] = masks[source] | cfg_edge_kind_bit(edge.kind)
}
let mut row = heads[destination]
while row >= 0 {
let source = rows[row]
let bit = rows[row + 1].reinterpret_as_uint()
if (masks[source] & bit) == 0U {
return Some("cfg successors and predecessors must stay symmetric")
}
masks[source] = masks[source] | (bit << 5)
row = rows[row + 2]
}
for edge in predecessors {
if (wide_destination || cfg.blocks[edge.block].succs.length() > 8) &&
(masks[edge.block] & (cfg_edge_kind_bit(edge.kind) << 5)) == 0U {
return Some("cfg predecessors and successors must stay symmetric")
}
}
// Clear after all kinds for this destination have been checked. Duplicate
// source/kind pairs share one mask; clearing during verification loses it.
for edge in predecessors {
masks[edge.block] = 0U
}
}
None
}
///|
fn cfg_verify(
cfg : HotCfg,
node_membership_visits~ : Array[Int32]? = None,
) -> Unit {
let listed = if cfg.blocks.any(fn(block) { block.nodes.length() > 8 }) {
let seen = Array::make(cfg.node_to_block.length(), false)
for block in cfg.blocks {
for node in block.nodes {
match node_membership_visits {
Some(count) => count[0] += 1
None => ()
}
if node >= 0 &&
node < seen.length() &&
cfg.node_to_block[node] == block.id {
seen[node] = true
}
}
}
Some(seen)
} else {
None
}
// Keep the allocation-free bounded scan for ordinary small-degree CFGs.
// Shared catch hubs need indexed symmetry checks rather than one scan of
// all incoming calls for each call edge.
let indexed_edges = cfg.blocks.any(fn(block) {
block.preds.length() > 8 || block.succs.length() > 8
})
if indexed_edges {
if cfg_indexed_edge_symmetry_error(cfg) is Some(error) {
abort(error)
}
}
for block_id = 0; block_id < cfg.blocks.length(); block_id = block_id + 1 {
let block = cfg.blocks[block_id]
if block.id != block_id {
abort("cfg block ids must match their storage index")
}
if !indexed_edges {
for succ in block.succs {
if succ.block < 0 || succ.block >= cfg.blocks.length() {
abort("cfg successor target must reference a valid block id")
}
let mut found = false
for pred in cfg.blocks[succ.block].preds {
if pred.block == block_id && pred.kind == succ.kind {
found = true
break
}
}
if !found {
abort("cfg successors and predecessors must stay symmetric")
}
}
for pred in block.preds {
if pred.block < 0 || pred.block >= cfg.blocks.length() {
abort("cfg predecessor source must reference a valid block id")
}
let mut found = false
for succ in cfg.blocks[pred.block].succs {
if succ.block == block_id && succ.kind == pred.kind {
found = true
break
}
}
if !found {
abort("cfg predecessors and successors must stay symmetric")
}
}
}
}
for node_id = 0; node_id < cfg.node_to_block.length(); node_id = node_id + 1 {
let block_id = cfg.node_to_block[node_id]
if block_id < 0 {
continue
}
if block_id >= cfg.blocks.length() {
abort("cfg node-to-block mapping must reference a valid block")
}
let mut found = match listed {
Some(seen) => seen[node_id]
None => false
}
if listed is None {
for mapped in cfg.blocks[block_id].nodes {
match node_membership_visits {
Some(count) => count[0] += 1
None => ()
}
if mapped == node_id {
found = true
break
}
}
}
if !found {
abort("cfg node-to-block mapping must match the owning block node list")
}
}
for entry in cfg.region_roots {
if entry.block < 0 || entry.block >= cfg.blocks.length() {
abort("cfg region-root mapping must reference a valid block")
}
}
}
///|
pub fn cfg_build(
func : HotFunc,
conservative_exception_entries~ : Bool = true,
expand_operand_control~ : Bool = false,
) -> HotCfg {
hot_verify_all_or_abort(func)
let builder = cfg_builder_new(
func, conservative_exception_entries, expand_operand_control,
)
let entry_block = cfg_builder_add_block(builder, [], true)
let exit_block = cfg_builder_add_block(builder, [], true)
builder.cfg.entry_block = entry_block
builder.cfg.exit_block = exit_block
let root_build = cfg_builder_build_region(
builder,
func,
HotRegionRef::root(),
None,
)
match root_build.entry_block {
Some(block_id) =>
cfg_builder_add_edge(
builder,
entry_block,
block_id,
CfgEdgeKind::FallthroughEdge,
)
None =>
cfg_builder_add_edge(
builder,
entry_block,
exit_block,
CfgEdgeKind::FallthroughEdge,
)
}
cfg_builder_connect_exits(builder, root_build.exit_blocks, Some(exit_block))
cfg_verify(builder.cfg)
builder.cfg
}
///|
pub fn cfg_block_count(cfg : HotCfg) -> Int32 {
cfg.blocks.length()
}
///|
pub fn cfg_entry_block(cfg : HotCfg) -> BlockId {
cfg.entry_block
}
///|
pub fn cfg_exit_block(cfg : HotCfg) -> BlockId {
cfg.exit_block
}
///|
pub fn cfg_exceptional_exit_block(cfg : HotCfg) -> BlockId? {
cfg.exceptional_exit_block
}
///|
pub fn cfg_block_get(cfg : HotCfg, block_id : BlockId) -> HotBlockInfo {
cfg.blocks[block_id]
}
///|
pub fn cfg_block_for_node(cfg : HotCfg, node_id : NodeId) -> BlockId? {
match cfg.node_to_block.get(node_id) {
Some(block_id) if block_id >= 0 => Some(block_id)
_ => None
}
}
///|
pub fn cfg_block_for_region_root(
cfg : HotCfg,
region_ref : HotRegionRef,
slot : Int32,
) -> BlockId? {
let region_holder = cfg_builder_region_holder(region_ref)
for entry in cfg.region_roots {
if entry.region_holder == region_holder && entry.slot == slot {
return Some(entry.block)
}
}
None
}
///|
pub fn cfg_successors(cfg : HotCfg, block_id : BlockId) -> Array[CfgEdge] {
cfg_successor_edges(cfg, block_id).copy()
}
///|
/// Reports whether execution beginning at one structured-region root can reach
/// the following root in the same region. This is stronger than inspecting the
/// root opcode: structured controls may complete through their own labels, and
/// exception or loop edges may still reach the continuation.
pub fn cfg_region_root_can_reach_next(
func : HotFunc,
cfg : HotCfg,
region_ref : HotRegionRef,
slot : Int32,
) -> Bool {
let fallback = fn() {
if slot < 0 || slot + 1 >= hot_region_root_count(func, region_ref) {
return false
}
let root_id = hot_region_get(func, region_ref, slot)
if root_id < 0 || !hot_node_is_live(func, root_id) {
return true
}
let edge_kinds = cfg_terminator_edge_kinds(hot_query_op(func, root_id))
if edge_kinds.length() == 0 {
return true
}
for kind in edge_kinds {
if kind == CfgEdgeKind::FallthroughEdge {
return true
}
}
false
}
let start = match cfg_block_for_region_root(cfg, region_ref, slot) {
Some(block) => block
None => return fallback()
}
let target = match cfg_block_for_region_root(cfg, region_ref, slot + 1) {
Some(block) => block
None => return fallback()
}
if start == target {
return true
}
let seen = Array::make(cfg_block_count(cfg), false)
let worklist = [start]
while worklist.length() > 0 {
let current = worklist.pop().unwrap()
if current == target {
return true
}
if current < 0 || current >= seen.length() || seen[current] {
continue
}
seen[current] = true
for edge in cfg_successor_edges(cfg, current) {
if edge.block == target {
return true
}
if edge.block >= 0 && edge.block < seen.length() && !seen[edge.block] {
worklist.push(edge.block)
}
}
}
false
}
///|
pub fn cfg_predecessors(cfg : HotCfg, block_id : BlockId) -> Array[CfgEdge] {
cfg_predecessor_edges(cfg, block_id).copy()
}
///|
pub fn cfg_successor_edges(cfg : HotCfg, block_id : BlockId) -> Array[CfgEdge] {
cfg.blocks[block_id].succs
}
///|
pub fn cfg_predecessor_edges(
cfg : HotCfg,
block_id : BlockId,
) -> Array[CfgEdge] {
cfg.blocks[block_id].preds
}