Repository: bytecodealliance/wasmtime
Pull request #8596 (2024-05-11 11:37:59)

riscv64: Improve pattern matching rules for FMA
👋 Hey,

This PR reworks our FMA pattern matching to be slightly less verbose using the suggestions provided by @jameysharp on https://github.com/bytecodealliance/wasmtime/pull/8588#issuecomment-2103773727.

It additionally adds the `(fma x (splat y) z)` pattern for vectors, which can be proven to be equivalent to `(fma y (splat x) z)`. See https://github.com/bytecodealliance/wasmtime/pull/8588#issuecomment-2105622411. I've only added ISA tests for these patterns since it turns out they are already present in the SIMD FMA runtests.

I think all of the patterns matched here are sound although I can't formally prove all of them.  This comment https://github.com/bytecodealliance/wasmtime/pull/8588#issuecomment-2105622411 describes most of the validations that I did.


fitzgen: Redirecting to @jameysharp since it seems like he has more context here.

Patch:
diff --git a/cranelift/codegen/src/isa/riscv64/lower.isle b/cranelift/codegen/src/isa/riscv64/lower.isle
index 72890d91a..5a8f499e9 100644
--- a/cranelift/codegen/src/isa/riscv64/lower.isle
+++ b/cranelift/codegen/src/isa/riscv64/lower.isle
@@ -1509,56 +1509,55 @@
 
 ;;;; Rules for `fma` ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-;; fmadd: rs1 * rs2 + rs3
-(rule 0 (lower (has_type (ty_scalar_float ty) (fma x y z)))
-  (rv_fmadd ty (FRM.RNE) x y z))
-
-;; fmsub: rs1 * rs2 - rs3
-(rule 1 (lower (has_type (ty_scalar_float ty) (fma x y (fneg z))))
-  (rv_fmsub ty (FRM.RNE) x y z))
-
-;; fnmsub: -rs1 * rs2 + rs3
-(rule 2 (lower (has_type (ty_scalar_float ty) (fma (fneg x) y z)))
-  (rv_fnmsub ty (FRM.RNE) x y z))
-
-;; fnmadd: -rs1 * rs2 - rs3
-(rule 3 (lower (has_type (ty_scalar_float ty) (fma (fneg x) y (fneg z))))
-  (rv_fnmadd ty (FRM.RNE) x y z))
-
-;; (fma x y z) computes x * y + z
-;; vfmacc computes vd[i] = +(vs1[i] * vs2[i]) + vd[i]
-;; We need to reverse the order of the arguments
-
-(rule 4 (lower (has_type (ty_vec_fits_in_register ty) (fma x y z)))
-  (rv_vfmacc_vv z y x (unmasked) ty))
-
-(rule 5 (lower (has_type (ty_vec_fits_in_register ty) (fma (splat x) y z)))
-  (rv_vfmacc_vf z y x (unmasked) ty))
-
-;; vfmsac computes vd[i] = +(vs1[i] * vs2[i]) - vd[i]
-
-(rule 6 (lower (has_type (ty_vec_fits_in_register ty) (fma x y (fneg z))))
-  (rv_vfmsac_vv z y x (unmasked) ty))
-
-(rule 9 (lower (has_type (ty_vec_fits_in_register ty) (fma (splat x) y (fneg z))))
-  (rv_vfmsac_vf z y x (unmasked) ty))
-
-;; vfnmacc computes vd[i] = -(vs1[i] * vs2[i]) - vd[i]
-
-(rule 7 (lower (has_type (ty_vec_fits_in_register ty) (fma (fneg x) y (fneg z))))
-  (rv_vfnmacc_vv z y x (unmasked) ty))
-
-(rule 9 (lower (has_type (ty_vec_fits_in_register ty) (fma (fneg (splat x)) y (fneg z))))
-  (rv_vfnmacc_vf z y x (unmasked) ty))
-
-;; vfnmsac computes vd[i] = -(vs1[i] * vs2[i]) + vd[i]
-
-(rule 5 (lower (has_type (ty_vec_fits_in_register ty) (fma (fneg x) y z)))
-  (rv_vfnmsac_vv z y x (unmasked) ty))
-
-(rule 8 (lower (has_type (ty_vec_fits_in_register ty) (fma (fneg (splat x)) y z)))
-  (rv_vfnmsac_vf z y x (unmasked) ty))
-
+;; RISC-V has 4 FMA instructions that do a slightly different computation.
+;;
+;; fmadd:   (rs1 * rs2) + rs3
+;; fmsub:   (rs1 * rs2) - rs3
+;; fnmadd: -(rs1 * rs2) - rs3
+;; fnmsub: -(rs1 * rs2) + rs3
+;;
+;; Additionally there are vector versions of these instructions with slightly different names.
+;; The vector instructions also have two variants each. `.vv` and `.vf`, where `.vv` variants
+;; take two vector operands and the `.vf` variants take a vector operand and a scalar operand.
+;;
+;; Due to this, variation they recieve the arguments in a different order. So we need to swap
+;; the arguments below.
+;;
+;; vfmacc:  vd[i] = +(vs1[i] * vs2[i]) + vd[i]
+;; vfmsac:  vd[i] = +(vs1[i] * vs2[i]) - vd[i]
+;; vfnmacc: vd[i] = -(vs1[i] * vs2[i]) - vd[i]
+;; vfnmsac: vd[i] = -(vs1[i] * vs2[i]) + vd[i]
+
+(type IsFneg (enum (Result (negate u64) (value Value))))
+
+(decl pure is_fneg (Value) IsFneg)
+(rule 1 (is_fneg (fneg x)) (IsFneg.Result 1 x))
+(rule 0 (is_fneg x) (IsFneg.Result 0 x))
+
+(rule (lower (has_type ty (fma x_src y_src z_src)))
+  (if-let (IsFneg.Result neg_x x) (is_fneg x_src))
+  (if-let (IsFneg.Result neg_y y) (is_fneg y_src))
+  (if-let (IsFneg.Result neg_z z) (is_fneg z_src))
+  (rv_fma ty (u64_xor neg_x neg_y) neg_z x y z))
+
+; parity arguments indicate whether to negate the x*y term or the z term, respectively
+(decl rv_fma (Type u64 u64 Value Value Value) InstOutput)
+(rule 0 (rv_fma (ty_scalar_float ty) 0 0 x y z) (rv_fmadd ty (FRM.RNE) x y z))
+(rule 0 (rv_fma (ty_scalar_float ty) 0 1 x y z) (rv_fmsub ty (FRM.RNE) x y z))
+(rule 0 (rv_fma (ty_scalar_float ty) 1 0 x y z) (rv_fnmsub ty (FRM.RNE) x y z))
+(rule 0 (rv_fma (ty_scalar_float ty) 1 1 x y z) (rv_fnmadd ty (FRM.RNE) x y z))
+(rule 1 (rv_fma (ty_vec_fits_in_register ty) 0 0 x y z) (rv_vfmacc_vv z y x (unmasked) ty))
+(rule 1 (rv_fma (ty_vec_fits_in_register ty) 0 1 x y z) (rv_vfmsac_vv z y x (unmasked) ty))
+(rule 1 (rv_fma (ty_vec_fits_in_register ty) 1 0 x y z) (rv_vfnmsac_vv z y x (unmasked) ty))
+(rule 1 (rv_fma (ty_vec_fits_in_register ty) 1 1 x y z) (rv_vfnmacc_vv z y x (unmasked) ty))
+(rule 2 (rv_fma (ty_vec_fits_in_register ty) 0 0 (splat x) y z) (rv_vfmacc_vf z y x (unmasked) ty))
+(rule 2 (rv_fma (ty_vec_fits_in_register ty) 0 1 (splat x) y z) (rv_vfmsac_vf z y x (unmasked) ty))
+(rule 2 (rv_fma (ty_vec_fits_in_register ty) 1 0 (splat x) y z) (rv_vfnmsac_vf z y x (unmasked) ty))
+(rule 2 (rv_fma (ty_vec_fits_in_register ty) 1 1 (splat x) y z) (rv_vfnmacc_vf z y x (unmasked) ty))
+(rule 3 (rv_fma (ty_vec_fits_in_register ty) 0 0 x (splat y) z) (rv_vfmacc_vf z x y (unmasked) ty))
+(rule 3 (rv_fma (ty_vec_fits_in_register ty) 0 1 x (splat y) z) (rv_vfmsac_vf z x y (unmasked) ty))
+(rule 3 (rv_fma (ty_vec_fits_in_register ty) 1 0 x (splat y) z) (rv_vfnmsac_vf z x y (unmasked) ty))
+(rule 3 (rv_fma (ty_vec_fits_in_register ty) 1 1 x (splat y) z) (rv_vfnmacc_vf z x y (unmasked) ty))
 
 ;;;; Rules for `sqrt` ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 (rule 0 (lower (has_type (ty_scalar_float ty) (sqrt x)))
diff --git a/cranelift/frontend/src/frontend.rs b/cranelift/frontend/src/frontend.rs
index 631f6ba8c..81d907a33 100644
--- a/cranelift/frontend/src/frontend.rs
+++ b/cranelift/frontend/src/frontend.rs
@@ -19,7 +19,7 @@ use cranelift_codegen::packed_option::PackedOption;
 /// Structure used for translating a series of functions into Cranelift IR.
 ///
 /// In order to reduce memory reallocations when compiling multiple functions,
-/// `FunctionBuilderContext` holds various data structures which are cleared between
+/// [`FunctionBuilderContext`] holds various data structures which are cleared between
 /// functions, rather than dropped, preserving the underlying allocations.
 #[derive(Default)]
 pub struct FunctionBuilderContext {
@@ -28,7 +28,7 @@ pub struct FunctionBuilderContext {
     types: SecondaryMap<Variable, Type>,
 }
 
-/// Temporary object used to build a single Cranelift IR `Function`.
+/// Temporary object used to build a single Cranelift IR [`Function`].
 pub struct FunctionBuilder<'a> {
     /// The function currently being built.
     /// This field is public so the function can be re-borrowed.
@@ -53,8 +53,8 @@ enum BlockStatus {
 }
 
 impl FunctionBuilderContext {
-    /// Creates a FunctionBuilderContext structure. The structure is automatically cleared after
-    /// each [`FunctionBuilder`](struct.FunctionBuilder.html) completes translating a function.
+    /// Creates a [`FunctionBuilderContext`] structure. The structure is automatically cleared after
+    /// each [`FunctionBuilder`] completes translating a function.
     pub fn new() -> Self {
         Self::default()
     }
@@ -70,7 +70,7 @@ impl FunctionBuilderContext {
     }
 }
 
-/// Implementation of the [`InstBuilder`](cranelift_codegen::ir::InstBuilder) that has
+/// Implementation of the [`InstBuilder`] that has
 /// one convenience method per Cranelift IR instruction.
 pub struct FuncInstBuilder<'short, 'long: 'short> {
     builder: &'short mut FunctionBuilder<'long>,
@@ -223,7 +223,7 @@ pub enum DefVariableError {
     ///
     /// note: to obtain the type of the value, you can call
     /// [`cranelift_codegen::ir::dfg::DataFlowGraph::value_type`] (using the
-    /// [`FunctionBuilder.func.dfg`] field)
+    /// `FunctionBuilder.func.dfg` field)
     TypeMismatch(Variable, Value),
     /// The value was defined (in a call to [`FunctionBuilder::def_var`]) before
     /// it was declared (in a call to [`FunctionBuilder::declare_var`]).
@@ -261,7 +261,7 @@ impl fmt::Display for DefVariableError {
 /// The module is parametrized by one type which is the representation of variables in your
 /// origin language. It offers a way to conveniently append instruction to your program flow.
 /// You are responsible to split your instruction flow into extended blocks (declared with
-/// `create_block`) whose properties are:
+/// [`create_block`](Self::create_block)) whose properties are:
 ///
 /// - branch and jump instructions can only point at the top of extended blocks;
 /// - the last instruction of each block is a terminator instruction which has no natural successor,
@@ -269,28 +269,27 @@ impl fmt::Display for DefVariableError {
 ///
 /// The parameters of Cranelift IR instructions are Cranelift IR values, which can only be created
 /// as results of other Cranelift IR instructions. To be able to create variables redefined multiple
-/// times in your program, use the `def_var` and `use_var` command, that will maintain the
-/// correspondence between your variables and Cranelift IR SSA values.
+/// times in your program, use the [`def_var`](Self::def_var) and [`use_var`](Self::use_var) command,
+/// that will maintain the correspondence between your variables and Cranelift IR SSA values.
 ///
-/// The first block for which you call `switch_to_block` will be assumed to be the beginning of
-/// the function.
+/// The first block for which you call [`switch_to_block`](Self::switch_to_block) will be assumed to
+/// be the beginning of the function.
 ///
-/// At creation, a `FunctionBuilder` instance borrows an already allocated `Function` which it
+/// At creation, a [`FunctionBuilder`] instance borrows an already allocated `Function` which it
 /// modifies with the information stored in the mutable borrowed
-/// [`FunctionBuilderContext`](struct.FunctionBuilderContext.html). The function passed in
-/// argument should be newly created with
-/// [`Function::with_name_signature()`](Function::with_name_signature), whereas the
-/// `FunctionBuilderContext` can be kept as is between two function translations.
+/// [`FunctionBuilderContext`]. The function passed in argument should be newly created with
+/// [`Function::with_name_signature()`], whereas the [`FunctionBuilderContext`] can be kept as is
+/// between two function translations.
 ///
 /// # Errors
 ///
 /// The functions below will panic in debug mode whenever you try to modify the Cranelift IR
 /// function in a way that violate the coherence of the code. For instance: switching to a new
-/// `Block` when you haven't filled the current one with a terminator instruction, inserting a
+/// [`Block`] when you haven't filled the current one with a terminator instruction, inserting a
 /// return instruction with arguments that don't match the function's signature.
 impl<'a> FunctionBuilder<'a> {
-    /// Creates a new FunctionBuilder structure that will operate on a `Function` using a
-    /// `FunctionBuilderContext`.
+    /// Creates a new [`FunctionBuilder`] structure that will operate on a [`Function`] using a
+    /// [`FunctionBuilderContext`].
     pub fn new(func: &'a mut Function, func_ctx: &'a mut FunctionBuilderContext) -> Self {
         debug_assert!(func_ctx.is_empty());
         Self {
@@ -311,7 +310,7 @@ impl<'a> FunctionBuilder<'a> {
         self.srcloc = srcloc;
     }
 
-    /// Creates a new `Block` and returns its reference.
+    /// Creates a new [`Block`] and returns its reference.
     pub fn create_block(&mut self) -> Block {
         let block = self.func.dfg.make_block();
         self.func_ctx.ssa.declare_block(block);
@@ -332,7 +331,7 @@ impl<'a> FunctionBuilder<'a> {
     }
 
     /// After the call to this function, new instructions will be inserted into the designated
-    /// block, in the order they are declared. You must declare the types of the Block arguments
+    /// block, in the order they are declared. You must declare the types of the [`Block`] arguments
     /// you will use here.
     ///
     /// When inserting the terminator instruction (which doesn't have a fallthrough to its immediate
@@ -367,9 +366,9 @@ impl<'a> FunctionBuilder<'a> {
         self.handle_ssa_side_effects(side_effects);
     }
 
-    /// Effectively calls seal_block on all unsealed blocks in the function.
+    /// Effectively calls [seal_block](Self::seal_block) on all unsealed blocks in the function.
     ///
-    /// It's more efficient to seal `Block`s as soon as possible, during
+    /// It's more efficient to seal [`Block`]s as soon as possible, during
     /// translation, but for frontends where this is impractical to do, this
     /// function can be used at the end of translating all blocks to ensure
     /// that everything is sealed.
@@ -473,9 +472,10 @@ impl<'a> FunctionBuilder<'a> {
             })
     }
 
-    /// Set label for Value
+    /// Set label for [`Value`]
     ///
-    /// This will not do anything unless `func.dfg.collect_debug_info` is called first.
+    /// This will not do anything unless
+    /// [`func.dfg.collect_debug_info`](DataFlowGraph::collect_debug_info) is called first.
     pub fn set_val_label(&mut self, val: Value, label: ValueLabel) {
         if let Some(values_labels) = self.func.stencil.dfg.values_labels.as_mut() {
             use alloc::collections::btree_map::Entry;
@@ -497,19 +497,21 @@ impl<'a> FunctionBuilder<'a> {
         }
     }
 
-    /// Creates a jump table in the function, to be used by `br_table` instructions.
+    /// Creates a jump table in the function, to be used by [`br_table`](InstBuilder::br_table) instructions.
     pub fn create_jump_table(&mut self, data: JumpTableData) -> JumpTable {
         self.func.create_jump_table(data)
     }
 
-    /// Creates a sized stack slot in the function, to be used by `stack_load`, `stack_store` and
-    /// `stack_addr` instructions.
+    /// Creates a sized stack slot in the function, to be used by [`stack_load`](InstBuilder::stack_load),
+    /// [`stack_store`](InstBuilder::stack_store) and [`stack_addr`](InstBuilder::stack_addr) instructions.
     pub fn create_sized_stack_slot(&mut self, data: StackSlotData) -> StackSlot {
         self.func.create_sized_stack_slot(data)
     }
 
-    /// Creates a dynamic stack slot in the function, to be used by `dynamic_stack_load`,
-    /// `dynamic_stack_store` and `dynamic_stack_addr` instructions.
+    /// Creates a dynamic stack slot in the function, to be used by
+    /// [`dynamic_stack_load`](InstBuilder::dynamic_stack_load),
+    /// [`dynamic_stack_store`](InstBuilder::dynamic_stack_store) and
+    /// [`dynamic_stack_addr`](InstBuilder::dynamic_stack_addr) instructions.
     pub fn create_dynamic_stack_slot(&mut self, data: DynamicStackSlotData) -> DynamicStackSlot {
         self.func.create_dynamic_stack_slot(data)
     }
@@ -529,8 +531,8 @@ impl<'a> FunctionBuilder<'a> {
         self.func.create_global_value(data)
     }
 
-    /// Returns an object with the [`InstBuilder`](cranelift_codegen::ir::InstBuilder)
-    /// trait that allows to conveniently append an instruction to the current `Block` being built.
+    /// Returns an object with the [`InstBuilder`]
+    /// trait that allows to conveniently append an instruction to the current [`Block`] being built.
     pub fn ins<'short>(&'short mut self) -> FuncInstBuilder<'short, 'a> {
         let block = self
             .position
@@ -554,10 +556,10 @@ impl<'a> FunctionBuilder<'a> {
         }
     }
 
-    /// Returns a `FuncCursor` pointed at the current position ready for inserting instructions.
+    /// Returns a [`FuncCursor`] pointed at the current position ready for inserting instructions.
     ///
     /// This can be used to insert SSA code that doesn't need to access locals and that doesn't
-    /// need to know about `FunctionBuilder` at all.
+    /// need to know about [`FunctionBuilder`] at all.
     pub fn cursor(&mut self) -> FuncCursor {
         self.ensure_inserted_block();
         FuncCursor::new(self.func)
@@ -565,7 +567,7 @@ impl<'a> FunctionBuilder<'a> {
             .at_bottom(self.position.unwrap())
     }
 
-    /// Append parameters to the given `Block` corresponding to the function
+    /// Append parameters to the given [`Block`] corresponding to the function
     /// parameters. This can be used to set up the block parameters for the
     /// entry block.
     pub fn append_block_params_for_function_params(&mut self, block: Block) {
@@ -589,7 +591,7 @@ impl<'a> FunctionBuilder<'a> {
         }
     }
 
-    /// Append parameters to the given `Block` corresponding to the function
+    /// Append parameters to the given [`Block`] corresponding to the function
     /// return values. This can be used to set up the block parameters for a
     /// function exit block.
     pub fn append_block_params_for_function_returns(&mut self, block: Block) {
@@ -610,7 +612,7 @@ impl<'a> FunctionBuilder<'a> {
 
     /// Declare that translation of the current function is complete.
     ///
-    /// This resets the state of the `FunctionBuilderContext` in preparation to
+    /// This resets the state of the [`FunctionBuilderContext`] in preparation to
     /// be used for another function.
     pub fn finalize(self) {
         // Check that all the `Block`s are filled and sealed.
@@ -659,18 +661,19 @@ impl<'a> FunctionBuilder<'a> {
 /// function. The functions below help you inspect the function you're creating and modify it
 /// in ways that can be unsafe if used incorrectly.
 impl<'a> FunctionBuilder<'a> {
-    /// Retrieves all the parameters for a `Block` currently inferred from the jump instructions
+    /// Retrieves all the parameters for a [`Block`] currently inferred from the jump instructions
     /// inserted that target it and the SSA construction.
     pub fn block_params(&self, block: Block) -> &[Value] {
         self.func.dfg.block_params(block)
     }
 
-    /// Retrieves the signature with reference `sigref` previously added with `import_signature`.
+    /// Retrieves the signature with reference `sigref` previously added with
+    /// [`import_signature`](Self::import_signature).
     pub fn signature(&self, sigref: SigRef) -> Option<&Signature> {
         self.func.dfg.signatures.get(sigref)
     }
 
-    /// Creates a parameter for a specific `Block` by appending it to the list of already existing
+    /// Creates a parameter for a specific [`Block`] by appending it to the list of already existing
     /// parameters.
     ///
     /// **Note:** this function has to be called at the creation of the `Block` before adding
@@ -703,7 +706,7 @@ impl<'a> FunctionBuilder<'a> {
         }
     }
 
-    /// Returns `true` if and only if the current `Block` is sealed and has no predecessors declared.
+    /// Returns `true` if and only if the current [`Block`] is sealed and has no predecessors declared.
     ///
     /// The entry block of a function is never unreachable.
     pub fn is_unreachable(&self) -> bool {
@@ -720,13 +723,13 @@ impl<'a> FunctionBuilder<'a> {
     }
 
     /// Returns `true` if and only if no instructions have been added since the last call to
-    /// `switch_to_block`.
+    /// [`switch_to_block`](Self::switch_to_block).
     fn is_pristine(&self, block: Block) -> bool {
         self.func_ctx.status[block] == BlockStatus::Empty
     }
 
     /// Returns `true` if and only if a terminator instruction has been inserted since the
-    /// last call to `switch_to_block`.
+    /// last call to [`switch_to_block`](Self::switch_to_block).
     fn is_filled(&self, block: Block) -> bool {
         self.func_ctx.status[block] == BlockStatus::Filled
     }
diff --git a/cranelift/frontend/src/lib.rs b/cranelift/frontend/src/lib.rs
index 68761f755..ce9057b6f 100644
--- a/cranelift/frontend/src/lib.rs
+++ b/cranelift/frontend/src/lib.rs
@@ -4,38 +4,36 @@
 //! corresponding to your source program written in another language.
 //!
 //! To get started, create an [`FunctionBuilderContext`](struct.FunctionBuilderContext.html) and
-//! pass it as an argument to a [`FunctionBuilder`](struct.FunctionBuilder.html).
+//! pass it as an argument to a [`FunctionBuilder`].
 //!
 //! # Mutable variables and Cranelift IR values
 //!
 //! The most interesting feature of this API is that it provides a single way to deal with all your
-//! variable problems. Indeed, the [`FunctionBuilder`](struct.FunctionBuilder.html) struct has a
+//! variable problems. Indeed, the [`FunctionBuilder`] struct has a
 //! type `Variable` that should be an index of your source language variables. Then, through
 //! calling the functions
-//! [`declare_var`](struct.FunctionBuilder.html#method.declare_var),
-//! [`def_var`](struct.FunctionBuilder.html#method.def_var) and
-//! [`use_var`](struct.FunctionBuilder.html#method.use_var), the
-//! [`FunctionBuilder`](struct.FunctionBuilder.html) will create for you all the Cranelift IR
-//! values corresponding to your variables.
+//! [`declare_var`](FunctionBuilder::declare_var), [`def_var`](FunctionBuilder::def_var) and
+//! [`use_var`](FunctionBuilder::use_var), the [`FunctionBuilder`] will create for you all the
+//! Cranelift IR values corresponding to your variables.
 //!
 //! This API has been designed to help you translate your mutable variables into
 //! [`SSA`](https://en.wikipedia.org/wiki/Static_single_assignment_form) form.
-//! [`use_var`](struct.FunctionBuilder.html#method.use_var) will return the Cranelift IR value
+//! [`use_var`](FunctionBuilder::use_var) will return the Cranelift IR value
 //! that corresponds to your mutable variable at a precise point in the program. However, if you know
 //! beforehand that one of your variables is defined only once, for instance if it is the result
 //! of an intermediate expression in an expression-based language, then you can translate it
 //! directly by the Cranelift IR value returned by the instruction builder. Using the
-//! [`use_var`](struct.FunctionBuilder.html#method.use_var) API for such an immutable variable
+//! [`use_var`](FunctionBuilder::use_var) API for such an immutable variable
 //! would also work but with a slight additional overhead (the SSA algorithm does not know
 //! beforehand if a variable is immutable or not).
 //!
 //! The moral is that you should use these three functions to handle all your mutable variables,
 //! even those that are not present in the source code but artifacts of the translation. It is up
-//! to you to keep a mapping between the mutable variables of your language and their `Variable`
-//! index that is used by Cranelift. Caution: as the `Variable` is used by Cranelift to index an
-//! array containing information about your mutable variables, when you create a new `Variable`
-//! with [`Variable::new(var_index)`] you should make sure that `var_index` is provided by a
-//! counter incremented by 1 each time you encounter a new mutable variable.
+//! to you to keep a mapping between the mutable variables of your language and their [`Variable`]
+//! index that is used by Cranelift. Caution: as the [`Variable`] is used by Cranelift to index an
+//! array containing information about your mutable variables, when you create a new [`Variable`]
+//! with `Variable::new(var_index)` you should make sure that `var_index`
+//! is provided by a counter incremented by 1 each time you encounter a new mutable variable.
 //!
 //! # Example
 //!
@@ -60,7 +58,7 @@
 //! }
 //! ```
 //!
-//! Here is how you build the corresponding Cranelift IR function using `FunctionBuilderContext`:
+//! Here is how you build the corresponding Cranelift IR function using [`FunctionBuilderContext`]:
 //!
 //! ```rust
 //! extern crate cranelift_codegen;
diff --git a/cranelift/frontend/src/ssa.rs b/cranelift/frontend/src/ssa.rs
index ba763fd70..ec27b5a32 100644
--- a/cranelift/frontend/src/ssa.rs
+++ b/cranelift/frontend/src/ssa.rs
@@ -19,7 +19,7 @@ use cranelift_codegen::packed_option::PackedOption;
 
 /// Structure containing the data relevant the construction of SSA for a given function.
 ///
-/// The parameter struct `Variable` corresponds to the way variables are represented in the
+/// The parameter struct [`Variable`] corresponds to the way variables are represented in the
 /// non-SSA language you're translating from.
 ///
 /// The SSA building relies on information about the variables used and defined.


Test patch:
diff --git a/cranelift/filetests/filetests/isa/riscv64/simd-fma.clif b/cranelift/filetests/filetests/isa/riscv64/simd-fma.clif
index 689763fd9..313afba1a 100644
--- a/cranelift/filetests/filetests/isa/riscv64/simd-fma.clif
+++ b/cranelift/filetests/filetests/isa/riscv64/simd-fma.clif
@@ -362,3 +362,90 @@ block0(v0: f64, v1: f64x2, v2: f64x2):
 ;   addi sp, sp, 0x10
 ;   ret
 
+
+function %fma_splat_y_f32x4(f32x4, f32, f32x4) -> f32x4 {
+block0(v0: f32x4, v1: f32, v2: f32x4):
+    v3 = splat.f32x4 v1
+    v4 = fma v0, v3, v2
+    return v4
+}
+
+; VCode:
+;   addi sp,sp,-16
+;   sd ra,8(sp)
+;   sd fp,0(sp)
+;   mv fp,sp
+; block0:
+;   vle8.v v9,-32(incoming_arg) #avl=16, #vtype=(e8, m1, ta, ma)
+;   vle8.v v14,-16(incoming_arg) #avl=16, #vtype=(e8, m1, ta, ma)
+;   vfmacc.vf v14,v9,fa0 #avl=4, #vtype=(e32, m1, ta, ma)
+;   vse8.v v14,0(a0) #avl=16, #vtype=(e8, m1, ta, ma)
+;   ld ra,8(sp)
+;   ld fp,0(sp)
+;   addi sp,sp,16
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   addi sp, sp, -0x10
+;   sd ra, 8(sp)
+;   sd s0, 0(sp)
+;   mv s0, sp
+; block1: ; offset 0x10
+;   .byte 0x57, 0x70, 0x08, 0xcc
+;   addi t6, sp, 0x10
+;   .byte 0x87, 0x84, 0x0f, 0x02
+;   addi t6, sp, 0x20
+;   .byte 0x07, 0x87, 0x0f, 0x02
+;   .byte 0x57, 0x70, 0x02, 0xcd
+;   .byte 0x57, 0x57, 0x95, 0xb2
+;   .byte 0x57, 0x70, 0x08, 0xcc
+;   .byte 0x27, 0x07, 0x05, 0x02
+;   ld ra, 8(sp)
+;   ld s0, 0(sp)
+;   addi sp, sp, 0x10
+;   ret
+
+function %fma_splat_y_f64x2(f64x2, f64, f64x2) -> f64x2 {
+block0(v0: f64x2, v1: f64, v2: f64x2):
+    v3 = splat.f64x2 v1
+    v4 = fma v0, v3, v2
+    return v4
+}
+
+; VCode:
+;   addi sp,sp,-16
+;   sd ra,8(sp)
+;   sd fp,0(sp)
+;   mv fp,sp
+; block0:
+;   vle8.v v9,-32(incoming_arg) #avl=16, #vtype=(e8, m1, ta, ma)
+;   vle8.v v14,-16(incoming_arg) #avl=16, #vtype=(e8, m1, ta, ma)
+;   vfmacc.vf v14,v9,fa0 #avl=2, #vtype=(e64, m1, ta, ma)
+;   vse8.v v14,0(a0) #avl=16, #vtype=(e8, m1, ta, ma)
+;   ld ra,8(sp)
+;   ld fp,0(sp)
+;   addi sp,sp,16
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   addi sp, sp, -0x10
+;   sd ra, 8(sp)
+;   sd s0, 0(sp)
+;   mv s0, sp
+; block1: ; offset 0x10
+;   .byte 0x57, 0x70, 0x08, 0xcc
+;   addi t6, sp, 0x10
+;   .byte 0x87, 0x84, 0x0f, 0x02
+;   addi t6, sp, 0x20
+;   .byte 0x07, 0x87, 0x0f, 0x02
+;   .byte 0x57, 0x70, 0x81, 0xcd
+;   .byte 0x57, 0x57, 0x95, 0xb2
+;   .byte 0x57, 0x70, 0x08, 0xcc
+;   .byte 0x27, 0x07, 0x05, 0x02
+;   ld ra, 8(sp)
+;   ld s0, 0(sp)
+;   addi sp, sp, 0x10
+;   ret
+