Skip to content

Commit 3fa545b

Browse files
authored
Cleanup cranelift-frontend (#4989)
* cranelift-wasm: Assume block is reachable In handling the WebAssembly "end" operator, cranelift-wasm had logic to skip generating a jump instruction if the block was both unreachable and "pristine", meaning no instructions had been added. However, `translate_operator` checks first that `state.reachable` is true, so this logic only runs when cranelift-wasm believes that the current block _is_ reachable. Therefore the condition should always be true, whether the block is pristine or not. I've left a debug_assert in case `state.reachable` ever doesn't agree with `builder.is_unreachable()`, but the assert doesn't fail in any of the tests. We'll see if fuzzing finds something. Anyway, outside of cranelift-frontend, this eliminates the only use of `is_pristine()`, and there were no uses of `is_filled()`. So I've made both of those private. They're now only used in a nearby debug assert. * cranelift-frontend: Clarify pristine/filled states There was a comment here saying "A filled block cannot be pristine." Given that the intent was for those two states to be mutually exclusive, I've replaced the two booleans with a three-state enum. I also replaced all reads of these two flags with method calls. In all but one case these are only checked in debug assertions, so I don't even care whether they get inlined. They're easier to read, and this will make it easier to replace their implementations, which I hope to do soon. Finally, I replaced all assignments to either flag with an appropriate assignment of the corresponding enum state. Keep in mind this correspondence between the new enum and the old flags: - Empty: pristine true, filled false - Partial: pristine false, filled false - Filled: pristine false, filled true Every existing update to these flags could only move to a later state. (For example, Partial couldn't go back to Empty.) In the old flags that meant that pristine could only go from true to false, and filled could only go from false to true. `fill_current_block` was a weird case because at first glance it looks like it could allow both pristine and filled to be true at the same time. However, it's only called from `FuncInstBuilder::build`, which calls `ensure_inserted_block` before doing anything else, and _that_ cleared the pristine flag. Similarly, `handle_ssa_side_effects` looks like it could allow both pristine and filled to be true for anything in `split_blocks_created`. However, those blocks are created by SSABuilder, so their BlockData is not initialized by `create_block`, and instead uses BlockData::default. The `Default` implementation here previously set both flags false, while `create_block` would instead set pristine to true. So these split blocks were correctly set to the Filled state, and after this patch they are still set correctly. * cranelift-frontend: Separate SSA and user block params Previously there was a `user_param_count` field in BlockData, used purely to debug-assert that no user parameters are added to a block after `use_var` adds SSA parameters. Instead, this patch enforces a strict phase separation between the period after a block is created when user parameters can be added to it, and the period when `use_var` may be called and instructions may be added. I'm assuming that calls to `use_var` are _always_ followed by inserting one or more instructions into the block. (If you don't want to insert an instruction, why do you need to know where instructions in this block would get variable definitions from?) This patch has no visible effect for callers which follow that rule. However, it was previously legal to call `use_var`, then append a block parameter before adding instructions, so long as `use_var` didn't actually need to add a block parameter. That could only happen if the current block is sealed and has exactly one predecessor. So anyone who was counting on this behavior was playing a dangerous game anyway. * cranelift-frontend: Defer initializing block data Every reference to the func_ctx.status SecondaryMap will automatically create the appropriate entries on-demand, with the sole exception of `finalize`. In that function, debug assertions use SecondaryMap::keys to find out which blocks need to be checked. However, those assertions always succeed for blocks which never had any instructions added. So it's okay to skip them for blocks which aren't touched after `create_block`.
1 parent ab4be2b commit 3fa545b

File tree

2 files changed

+71
-69
lines changed

2 files changed

+71
-69
lines changed

cranelift/frontend/src/frontend.rs

+58-57
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use cranelift_codegen::packed_option::PackedOption;
2424
#[derive(Default)]
2525
pub struct FunctionBuilderContext {
2626
ssa: SSABuilder,
27-
blocks: SecondaryMap<Block, BlockData>,
27+
status: SecondaryMap<Block, BlockStatus>,
2828
types: SecondaryMap<Variable, Type>,
2929
}
3030

@@ -41,20 +41,15 @@ pub struct FunctionBuilder<'a> {
4141
position: PackedOption<Block>,
4242
}
4343

44-
#[derive(Clone, Default)]
45-
struct BlockData {
46-
/// A Block is "pristine" iff no instructions have been added since the last
47-
/// call to `switch_to_block()`.
48-
pristine: bool,
49-
50-
/// A Block is "filled" iff a terminator instruction has been inserted since
51-
/// the last call to `switch_to_block()`.
52-
///
53-
/// A filled block cannot be pristine.
54-
filled: bool,
55-
56-
/// Count of parameters not supplied implicitly by the SSABuilder.
57-
user_param_count: usize,
44+
#[derive(Clone, Default, Eq, PartialEq)]
45+
enum BlockStatus {
46+
/// No instructions have been added.
47+
#[default]
48+
Empty,
49+
/// Some instructions have been added, but no terminator.
50+
Partial,
51+
/// A terminator has been added; no further instructions may be added.
52+
Filled,
5853
}
5954

6055
impl FunctionBuilderContext {
@@ -66,12 +61,12 @@ impl FunctionBuilderContext {
6661

6762
fn clear(&mut self) {
6863
self.ssa.clear();
69-
self.blocks.clear();
64+
self.status.clear();
7065
self.types.clear();
7166
}
7267

7368
fn is_empty(&self) -> bool {
74-
self.ssa.is_empty() && self.blocks.is_empty() && self.types.is_empty()
69+
self.ssa.is_empty() && self.status.is_empty() && self.types.is_empty()
7570
}
7671
}
7772

@@ -304,11 +299,6 @@ impl<'a> FunctionBuilder<'a> {
304299
pub fn create_block(&mut self) -> Block {
305300
let block = self.func.dfg.make_block();
306301
self.func_ctx.ssa.declare_block(block);
307-
self.func_ctx.blocks[block] = BlockData {
308-
filled: false,
309-
pristine: true,
310-
user_param_count: 0,
311-
};
312302
block
313303
}
314304

@@ -337,13 +327,13 @@ impl<'a> FunctionBuilder<'a> {
337327
debug_assert!(
338328
self.position.is_none()
339329
|| self.is_unreachable()
340-
|| self.is_pristine()
341-
|| self.is_filled(),
330+
|| self.is_pristine(self.position.unwrap())
331+
|| self.is_filled(self.position.unwrap()),
342332
"you have to fill your block before switching"
343333
);
344334
// We cannot switch to a filled block
345335
debug_assert!(
346-
!self.func_ctx.blocks[block].filled,
336+
!self.is_filled(block),
347337
"you cannot switch to a block which is already filled"
348338
);
349339

@@ -393,6 +383,12 @@ impl<'a> FunctionBuilder<'a> {
393383
/// Returns the Cranelift IR necessary to use a previously defined user
394384
/// variable, returning an error if this is not possible.
395385
pub fn try_use_var(&mut self, var: Variable) -> Result<Value, UseVariableError> {
386+
// Assert that we're about to add instructions to this block using the definition of the
387+
// given variable. ssa.use_var is the only part of this crate which can add block parameters
388+
// behind the caller's back. If we disallow calling append_block_param as soon as use_var is
389+
// called, then we enforce a strict separation between user parameters and SSA parameters.
390+
self.ensure_inserted_block();
391+
396392
let (val, side_effects) = {
397393
let ty = *self
398394
.func_ctx
@@ -534,14 +530,14 @@ impl<'a> FunctionBuilder<'a> {
534530
/// Make sure that the current block is inserted in the layout.
535531
pub fn ensure_inserted_block(&mut self) {
536532
let block = self.position.unwrap();
537-
if self.func_ctx.blocks[block].pristine {
533+
if self.is_pristine(block) {
538534
if !self.func.layout.is_block_inserted(block) {
539535
self.func.layout.append_block(block);
540536
}
541-
self.func_ctx.blocks[block].pristine = false;
537+
self.func_ctx.status[block] = BlockStatus::Partial;
542538
} else {
543539
debug_assert!(
544-
!self.func_ctx.blocks[block].filled,
540+
!self.is_filled(block),
545541
"you cannot add an instruction to a block already filled"
546542
);
547543
}
@@ -569,9 +565,12 @@ impl<'a> FunctionBuilder<'a> {
569565

570566
// These parameters count as "user" parameters here because they aren't
571567
// inserted by the SSABuilder.
572-
let user_param_count = &mut self.func_ctx.blocks[block].user_param_count;
568+
debug_assert!(
569+
self.is_pristine(block),
570+
"You can't add block parameters after adding any instruction"
571+
);
572+
573573
for argtyp in &self.func.stencil.signature.params {
574-
*user_param_count += 1;
575574
self.func
576575
.stencil
577576
.dfg
@@ -585,9 +584,12 @@ impl<'a> FunctionBuilder<'a> {
585584
pub fn append_block_params_for_function_returns(&mut self, block: Block) {
586585
// These parameters count as "user" parameters here because they aren't
587586
// inserted by the SSABuilder.
588-
let user_param_count = &mut self.func_ctx.blocks[block].user_param_count;
587+
debug_assert!(
588+
self.is_pristine(block),
589+
"You can't add block parameters after adding any instruction"
590+
);
591+
589592
for argtyp in &self.func.stencil.signature.returns {
590-
*user_param_count += 1;
591593
self.func
592594
.stencil
593595
.dfg
@@ -602,25 +604,27 @@ impl<'a> FunctionBuilder<'a> {
602604
// Check that all the `Block`s are filled and sealed.
603605
#[cfg(debug_assertions)]
604606
{
605-
for (block, block_data) in self.func_ctx.blocks.iter() {
606-
assert!(
607-
block_data.pristine || self.func_ctx.ssa.is_sealed(block),
608-
"FunctionBuilder finalized, but block {} is not sealed",
609-
block,
610-
);
611-
assert!(
612-
block_data.pristine || block_data.filled,
613-
"FunctionBuilder finalized, but block {} is not filled",
614-
block,
615-
);
607+
for block in self.func_ctx.status.keys() {
608+
if !self.is_pristine(block) {
609+
assert!(
610+
self.func_ctx.ssa.is_sealed(block),
611+
"FunctionBuilder finalized, but block {} is not sealed",
612+
block,
613+
);
614+
assert!(
615+
self.is_filled(block),
616+
"FunctionBuilder finalized, but block {} is not filled",
617+
block,
618+
);
619+
}
616620
}
617621
}
618622

619623
// In debug mode, check that all blocks are valid basic blocks.
620624
#[cfg(debug_assertions)]
621625
{
622626
// Iterate manually to provide more helpful error messages.
623-
for block in self.func_ctx.blocks.keys() {
627+
for block in self.func_ctx.status.keys() {
624628
if let Err((inst, msg)) = self.func.is_block_basic(block) {
625629
let inst_str = self.func.dfg.display_inst(inst);
626630
panic!(
@@ -665,14 +669,9 @@ impl<'a> FunctionBuilder<'a> {
665669
/// instructions to it, otherwise this could interfere with SSA construction.
666670
pub fn append_block_param(&mut self, block: Block, ty: Type) -> Value {
667671
debug_assert!(
668-
self.func_ctx.blocks[block].pristine,
672+
self.is_pristine(block),
669673
"You can't add block parameters after adding any instruction"
670674
);
671-
debug_assert_eq!(
672-
self.func_ctx.blocks[block].user_param_count,
673-
self.func.dfg.num_block_params(block)
674-
);
675-
self.func_ctx.blocks[block].user_param_count += 1;
676675
self.func.dfg.append_block_param(block, ty)
677676
}
678677

@@ -714,14 +713,14 @@ impl<'a> FunctionBuilder<'a> {
714713

715714
/// Returns `true` if and only if no instructions have been added since the last call to
716715
/// `switch_to_block`.
717-
pub fn is_pristine(&self) -> bool {
718-
self.func_ctx.blocks[self.position.unwrap()].pristine
716+
fn is_pristine(&self, block: Block) -> bool {
717+
self.func_ctx.status[block] == BlockStatus::Empty
719718
}
720719

721720
/// Returns `true` if and only if a terminator instruction has been inserted since the
722721
/// last call to `switch_to_block`.
723-
pub fn is_filled(&self) -> bool {
724-
self.func_ctx.blocks[self.position.unwrap()].filled
722+
fn is_filled(&self, block: Block) -> bool {
723+
self.func_ctx.status[block] == BlockStatus::Filled
725724
}
726725
}
727726

@@ -1078,7 +1077,7 @@ fn greatest_divisible_power_of_two(size: u64) -> u64 {
10781077
impl<'a> FunctionBuilder<'a> {
10791078
/// A Block is 'filled' when a terminator instruction is present.
10801079
fn fill_current_block(&mut self) {
1081-
self.func_ctx.blocks[self.position.unwrap()].filled = true;
1080+
self.func_ctx.status[self.position.unwrap()] = BlockStatus::Filled;
10821081
}
10831082

10841083
fn declare_successor(&mut self, dest_block: Block, jump_inst: Inst) {
@@ -1089,10 +1088,12 @@ impl<'a> FunctionBuilder<'a> {
10891088

10901089
fn handle_ssa_side_effects(&mut self, side_effects: SideEffects) {
10911090
for split_block in side_effects.split_blocks_created {
1092-
self.func_ctx.blocks[split_block].filled = true
1091+
self.func_ctx.status[split_block] = BlockStatus::Filled;
10931092
}
10941093
for modified_block in side_effects.instructions_added_to_blocks {
1095-
self.func_ctx.blocks[modified_block].pristine = false
1094+
if self.is_pristine(modified_block) {
1095+
self.func_ctx.status[modified_block] = BlockStatus::Partial;
1096+
}
10961097
}
10971098
}
10981099
}

cranelift/wasm/src/code_translator.rs

+13-12
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
115115
return Ok(());
116116
}
117117

118+
// Given that we believe the current block is reachable, the FunctionBuilder ought to agree.
119+
debug_assert!(!builder.is_unreachable());
120+
118121
// This big match treats all Wasm code operators.
119122
match op {
120123
/********************************** Locals ****************************************
@@ -380,18 +383,16 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
380383
Operator::End => {
381384
let frame = state.control_stack.pop().unwrap();
382385
let next_block = frame.following_code();
383-
384-
if !builder.is_unreachable() || !builder.is_pristine() {
385-
let return_count = frame.num_return_values();
386-
let return_args = state.peekn_mut(return_count);
387-
canonicalise_then_jump(builder, frame.following_code(), return_args);
388-
// You might expect that if we just finished an `if` block that
389-
// didn't have a corresponding `else` block, then we would clean
390-
// up our duplicate set of parameters that we pushed earlier
391-
// right here. However, we don't have to explicitly do that,
392-
// since we truncate the stack back to the original height
393-
// below.
394-
}
386+
let return_count = frame.num_return_values();
387+
let return_args = state.peekn_mut(return_count);
388+
389+
canonicalise_then_jump(builder, next_block, return_args);
390+
// You might expect that if we just finished an `if` block that
391+
// didn't have a corresponding `else` block, then we would clean
392+
// up our duplicate set of parameters that we pushed earlier
393+
// right here. However, we don't have to explicitly do that,
394+
// since we truncate the stack back to the original height
395+
// below.
395396

396397
builder.switch_to_block(next_block);
397398
builder.seal_block(next_block);

0 commit comments

Comments
 (0)