[Mlir-commits] [mlir] [mlir][wasm] Support for saturating FP truncations (PR #212709)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Wed Jul 29 02:06:30 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-mlir

Author: Luc Forget (lforg37)

<details>
<summary>Changes</summary>

Add support for saturate truncation of float to int operations.

This also requires the support of parser with sub-opcodes.
This is handled by supporting parser with extra argument for sub-opcode. 

---

Patch is 47.08 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/212709.diff


5 Files Affected:

- (modified) mlir/include/mlir/Dialect/WasmSSA/IR/WasmSSAOps.td (+30) 
- (modified) mlir/include/mlir/Target/Wasm/WasmBinaryEncoding.h (+2) 
- (modified) mlir/lib/Target/Wasm/TranslateFromWasm.cpp (+344-261) 
- (added) mlir/test/Target/Wasm/inputs/trunc_sat.yaml.wasm (+69) 
- (added) mlir/test/Target/Wasm/trunc_sat.mlir (+94) 


``````````diff
diff --git a/mlir/include/mlir/Dialect/WasmSSA/IR/WasmSSAOps.td b/mlir/include/mlir/Dialect/WasmSSA/IR/WasmSSAOps.td
index 3b3c3e8825fef..c206ea8c05583 100644
--- a/mlir/include/mlir/Dialect/WasmSSA/IR/WasmSSAOps.td
+++ b/mlir/include/mlir/Dialect/WasmSSA/IR/WasmSSAOps.td
@@ -1101,6 +1101,36 @@ def WasmSSA_TruncUIOp : WasmSSA_ConversionOp<"trunc_ui",
     [WasmSSA_FPType],
     [WasmSSA_IntegerType]>{}
 
+def WasmSSA_TruncSatSIOp : WasmSSA_ConversionOp<"trunc_sat_si",
+    [{Truncate floating point value to signed integer, saturating.
+
+    Consume a floating point value and produces an unsigned integer holding the value truncated toward zero.
+    NaN is mapped to 0, infinities are mapped to minimal and maximal representable values.
+    Normal values are clamped to the representable range and truncated.}],
+    [{Example:
+
+     ```mlir
+     %a = wasmssa.trunc_sat_si %b : f32 to i32
+     ```
+    }],
+    [WasmSSA_FPType],
+    [WasmSSA_IntegerType]>{}
+
+def WasmSSA_TruncSatUIOp : WasmSSA_ConversionOp<"trunc_sat_ui",
+    [{Truncate floating point value to unsigned integer, saturating.
+
+    Consume a floating point value and produces an unsigned integer holding the value truncated toward zero.
+    Negative infinity and NaN are mapped to 0, positive infinity is mapped to maximal representable values.
+    Normal values are clamped to the representable range and truncated.}],
+    [{Example:
+
+     ```mlir
+     %a = wasmssa.trunc_sat_ui %b : f32 to i32
+     ```
+    }],
+    [WasmSSA_FPType],
+    [WasmSSA_IntegerType]>{}
+
 def WasmSSA_DemoteOp : WasmSSA_ConversionOp<"demote",
     "Convert a f64 value to f32",
     [{Example:
diff --git a/mlir/include/mlir/Target/Wasm/WasmBinaryEncoding.h b/mlir/include/mlir/Target/Wasm/WasmBinaryEncoding.h
index fcaec612f913d..66506ed19b1a7 100644
--- a/mlir/include/mlir/Target/Wasm/WasmBinaryEncoding.h
+++ b/mlir/include/mlir/Target/Wasm/WasmBinaryEncoding.h
@@ -174,6 +174,8 @@ struct WasmBinaryEncoding {
     static constexpr std::byte extendI648S{0xC2};
     static constexpr std::byte extendI6416S{0xC3};
     static constexpr std::byte extendI6432S{0xC4};
+
+    static constexpr std::byte saturatedTruncate{0xFC};
   };
 
   /// Byte encodings of types in Wasm binaries
diff --git a/mlir/lib/Target/Wasm/TranslateFromWasm.cpp b/mlir/lib/Target/Wasm/TranslateFromWasm.cpp
index cea6bd2816dc1..a700a875e1caa 100644
--- a/mlir/lib/Target/Wasm/TranslateFromWasm.cpp
+++ b/mlir/lib/Target/Wasm/TranslateFromWasm.cpp
@@ -16,6 +16,7 @@
 #include "mlir/IR/BuiltinAttributeInterfaces.h"
 #include "mlir/IR/BuiltinAttributes.h"
 #include "mlir/IR/BuiltinTypes.h"
+#include "mlir/IR/Diagnostics.h"
 #include "mlir/IR/Location.h"
 #include "mlir/Support/LLVM.h"
 #include "mlir/Target/Wasm/WasmBinaryEncoding.h"
@@ -30,6 +31,7 @@
 #include <cassert>
 #include <cstddef>
 #include <cstdint>
+#include <type_traits>
 #include <variant>
 
 #define DEBUG_TYPE "wasm-translate"
@@ -247,6 +249,9 @@ constexpr ByteSequence<std::byte{IS}...>
 constexpr auto all8bitsBytes =
     castIndexSequenceToBytes(std::make_index_sequence<256>());
 
+template <std::byte>
+struct OpCode {};
+
 class ExpressionParser {
 public:
   using locals_t = SmallVector<local_val_t>;
@@ -255,38 +260,6 @@ class ExpressionParser {
       : parser{parser}, symbols{symbols}, locals{initLocal} {}
 
 private:
-  template <typename valueT>
-  parsed_inst_t
-  parseConstInst(OpBuilder &builder,
-                 std::enable_if_t<std::is_arithmetic_v<valueT>> * = nullptr);
-
-  /// Construct an operation with \p numOperands operands and a single result.
-  /// Each operand must have the same type. Suitable for e.g. binops, unary
-  /// ops, etc.
-  ///
-  /// \p opcode - The WASM opcode to build.
-  /// \p valueType - The operand and result type for the built instruction.
-  /// \p numOperands - The number of operands for the built operation.
-  ///
-  /// \returns The parsed instruction result, or failure.
-  template <typename opcode, typename valueType, unsigned int numOperands>
-  inline parsed_inst_t
-  buildNumericOp(OpBuilder &builder,
-                 std::enable_if_t<std::is_arithmetic_v<valueType>> * = nullptr);
-
-  /// Construct a conversion operation of type \p opType that takes a value from
-  /// type \p inputType on the stack and will produce a value of type
-  /// \p outputType.
-  ///
-  /// \p opType - The WASM dialect operation to build.
-  /// \p inputType - The operand type for the built instruction.
-  /// \p outputType - The result type for the built instruction.
-  ///
-  /// \returns The parsed instruction result, or failure.
-  template <typename opType, typename inputType, typename outputType,
-            typename... extraArgsT>
-  inline parsed_inst_t buildConvertOp(OpBuilder &builder, extraArgsT...);
-
   inline parsed_inst_t dispatchToInstParser(std::byte opCode,
                                             OpBuilder &builder);
   ///
@@ -354,39 +327,12 @@ class ExpressionParser {
     return getFuncTypeFor(builder, *parseResult);
   }
 
-  llvm::FailureOr<FunctionType> parseBlockFuncType(OpBuilder &builder);
-
   struct ParseResultWithInfo {
     SmallVector<Value> opResults;
     std::byte endingByte;
   };
 
-  template <typename FilterT = ByteSequence<WasmBinaryEncoding::endByte>>
-  /// @param blockToFill: the block which content will be populated
-  /// @param resType: the type that this block is supposed to return
-  llvm::FailureOr<std::byte>
-  parseBlockContent(OpBuilder &builder, Block *blockToFill, TypeRange resTypes,
-                    Location opLoc, LabelLevelOpInterface levelOp,
-                    FilterT parseEndBytes = {}) {
-    OpBuilder::InsertionGuard guard{builder};
-    builder.setInsertionPointToStart(blockToFill);
-    LDBG() << "parsing a block of type "
-           << builder.getFunctionType(blockToFill->getArgumentTypes(),
-                                      resTypes);
-    auto nC = addNesting(levelOp);
-
-    if (failed(pushResults(blockToFill->getArguments())))
-      return failure();
-    auto bodyParsingRes = parse(builder, parseEndBytes);
-    if (failed(bodyParsingRes))
-      return failure();
-    auto returnOperands = popOperands(resTypes);
-    if (failed(returnOperands))
-      return failure();
-    BlockReturnOp::create(builder, opLoc, *returnOperands);
-    LDBG() << "end of parsing of a block";
-    return bodyParsingRes->endingByte;
-  }
+  llvm::FailureOr<FunctionType> parseBlockFuncType(OpBuilder &builder);
 
 public:
   template <std::byte ParseEndByte = WasmBinaryEncoding::endByte>
@@ -415,98 +361,117 @@ class ExpressionParser {
   template <typename OpToCreate>
   parsed_inst_t parseSetOrTee(OpBuilder &);
 
-  /// Blocks and Loops have a similar format and differ only in how their exit
-  /// is handled which doesn´t matter at parsing time. Factorizes in one
-  /// function.
-  template <typename OpToCreate>
-  parsed_inst_t parseBlockLikeOp(OpBuilder &);
-
   Location getCurrentOpLoc() {
     assert(currentOpLoc.has_value() &&
            "expects current opcode location to be set");
     return *currentOpLoc;
   }
 
-  class TopLevelInstParserRegistry {
+  struct ExprParserProxy {
   public:
-    template <std::byte opCode>
-    static constexpr bool hasParserForOpcode = false;
+    friend ExpressionParser;
+    inline auto parseBlockFuncType(OpBuilder &builder) {
+      return exprParser.parseBlockFuncType(builder);
+    }
 
-    template <std::byte opCode>
-    static parsed_inst_t parseInstrWithOpCode(OpBuilder &,
-                                              ExpressionParser &) = delete;
-  };
+    template <typename FilterT = ByteSequence<WasmBinaryEncoding::endByte>>
+    /// @param blockToFill: the block which content will be populated
+    /// @param resType: the type that this block is supposed to return
+    llvm::FailureOr<std::byte>
+    parseBlockContent(OpBuilder &builder, Block *blockToFill,
+                      TypeRange resTypes, Location opLoc,
+                      LabelLevelOpInterface levelOp,
+                      FilterT parseEndBytes = {}) {
+      OpBuilder::InsertionGuard guard{builder};
+      builder.setInsertionPointToStart(blockToFill);
+      LDBG() << "parsing a block of type "
+             << builder.getFunctionType(blockToFill->getArgumentTypes(),
+                                        resTypes);
+      auto nC = exprParser.addNesting(levelOp);
+
+      if (failed(exprParser.pushResults(blockToFill->getArguments())))
+        return failure();
+      auto bodyParsingRes = exprParser.parse(builder, parseEndBytes);
+      if (failed(bodyParsingRes))
+        return failure();
+      auto returnOperands = exprParser.popOperands(resTypes);
+      if (failed(returnOperands))
+        return failure();
+      BlockReturnOp::create(builder, opLoc, *returnOperands);
+      LDBG() << "end of parsing of a block";
+      return bodyParsingRes->endingByte;
+    }
 
-private:
-  std::optional<Location> currentOpLoc;
-  ParserHead &parser;
-  WasmModuleSymbolTables const &symbols;
-  locals_t locals;
-  ValueStack valueStack;
-};
+    inline ParserHead &parser() { return exprParser.parser; }
 
-static inline parsed_inst_t
-unreachableHandler(OpBuilder &, ExpressionParser &expressionParser) {
-  llvm_unreachable("Failure in opcode parser dispatch logic.");
-  return mlir::failure();
-}
+    /// Blocks and Loops have a similar format and differ only in how their exit
+    /// is handled which doesn´t matter at parsing time. Factorizes in one
+    /// function.
+    template <typename OpToCreate>
+    parsed_inst_t parseBlockLikeOp(OpBuilder &);
 
-template <typename ParserRegistry>
-class InstDispatcher {
-private:
-  using dispatch_t = parsed_inst_t (*)(OpBuilder &, ExpressionParser &);
+    inline auto getCurrentOpLoc() { return exprParser.getCurrentOpLoc(); }
 
-  template <std::byte opCode>
-  static constexpr dispatch_t getHandlerForOpCode() {
-    if constexpr (ParserRegistry::template hasParserForOpcode<opCode>)
-      return ParserRegistry::template parseInstrWithOpCode<opCode>;
-    else
-      return unreachableHandler;
-  }
+    inline auto popOperands(TypeRange operandTypes) {
+      return exprParser.popOperands(operandTypes);
+    }
 
-public:
-  template <std::byte opCode>
-  static constexpr bool isValidInst =
-      ParserRegistry::template hasParserForOpcode<opCode>;
+    inline auto &symbols() { return exprParser.symbols; }
 
-private:
-  static inline parsed_inst_t
-  invalidOpcodeDiag(OpBuilder &, ExpressionParser &expressionParser,
-                    std::byte opCode) {
-    return emitError(expressionParser.getCurrentOpLoc(),
-                     "unknown instruction opcode: ")
-           << static_cast<int>(opCode);
-  }
+    inline auto &locals() { return exprParser.locals; }
 
-  template <std::byte... opCodes>
-  static inline parsed_inst_t dispatchImpl(std::byte opCode, OpBuilder &builder,
-                                           ExpressionParser &exprParser,
-                                           ByteSequence<opCodes...>) {
-    static constexpr std::array<bool, 256> opcodeValidityMap{
-        isValidInst<opCodes>...};
-    static constexpr std::array<dispatch_t, 256> dispatchTable{
-        getHandlerForOpCode<opCodes>()...};
-    if (opcodeValidityMap[static_cast<size_t>(opCode)]) {
-      return dispatchTable[static_cast<size_t>(opCode)](builder, exprParser);
+    template <typename OpToCreate>
+    parsed_inst_t parseSetOrTee(OpBuilder &builder) {
+      return exprParser.parseSetOrTee<OpToCreate>(builder);
     }
-    return invalidOpcodeDiag(builder, exprParser, opCode);
-  }
 
-public:
-  ///
-  /// @brief dispatch control flow to the sub parser registered for opCode in
-  /// `ParserRegistry`
-  ///
-  /// @param opCode opCode of the instruction to be Parsed
-  /// @param builder builder that will be passed to the parser
-  /// @param exprParser the generic parser passed to the sub parser
-  ///
-  /// @return the result of the parser or an error if there is no parser
-  /// registered for the opcode (emits a diagnostic)
-  static parsed_inst_t dispatch(std::byte opCode, OpBuilder &builder,
-                                ExpressionParser &exprParser) {
-    return dispatchImpl(opCode, builder, exprParser, all8bitsBytes);
-  }
+    template <typename valueT>
+    parsed_inst_t
+    parseConstInst(OpBuilder &builder,
+                   std::enable_if_t<std::is_arithmetic_v<valueT>> * = nullptr);
+
+    /// Construct an operation with \p numOperands operands and a single result.
+    /// Each operand must have the same type. Suitable for e.g. binops, unary
+    /// ops, etc.
+    ///
+    /// \p opcode - The WASM opcode to build.
+    /// \p valueType - The operand and result type for the built instruction.
+    /// \p numOperands - The number of operands for the built operation.
+    ///
+    /// \returns The parsed instruction result, or failure.
+    template <typename opcode, typename valueType, unsigned int numOperands>
+    inline parsed_inst_t buildNumericOp(
+        OpBuilder &builder,
+        std::enable_if_t<std::is_arithmetic_v<valueType>> * = nullptr);
+
+    /// Construct a conversion operation of type \p opType that takes a value
+    /// from
+    /// type \p inputType on the stack and will produce a value of type
+    /// \p outputType.
+    ///
+    /// \p opType - The WASM dialect operation to build.
+    /// \p inputType - The operand type for the built instruction.
+    /// \p outputType - The result type for the built instruction.
+    ///
+    /// \returns The parsed instruction result, or failure.
+    template <typename opType, typename inputType, typename outputType,
+              typename... extraArgsT>
+    inline parsed_inst_t buildConvertOp(OpBuilder &builder, extraArgsT...);
+
+  private:
+    explicit ExprParserProxy(ExpressionParser &exprParser)
+        : exprParser{exprParser} {};
+
+  private:
+    ExpressionParser &exprParser;
+  };
+
+private:
+  std::optional<Location> currentOpLoc;
+  ParserHead &parser;
+  WasmModuleSymbolTables const &symbols;
+  locals_t locals;
+  ValueStack valueStack;
 };
 
 class ParserHead {
@@ -1031,8 +996,9 @@ ExpressionParser::parseBlockFuncType(OpBuilder &builder) {
 }
 
 template <typename OpToCreate>
-parsed_inst_t ExpressionParser::parseBlockLikeOp(OpBuilder &builder) {
-  auto opLoc = currentOpLoc;
+parsed_inst_t
+ExpressionParser::ExprParserProxy::parseBlockLikeOp(OpBuilder &builder) {
+  auto opLoc = getCurrentOpLoc();
   auto funcType = parseBlockFuncType(builder);
   if (failed(funcType))
     return failure();
@@ -1046,49 +1012,35 @@ parsed_inst_t ExpressionParser::parseBlockLikeOp(OpBuilder &builder) {
   Region *curRegion = curBlock->getParent();
   auto resTypes = funcType->getResults();
   llvm::SmallVector<Location> locations{};
-  locations.resize(resTypes.size(), *currentOpLoc);
+  locations.resize(resTypes.size(), getCurrentOpLoc());
   auto *successor =
       builder.createBlock(curRegion, curRegion->end(), resTypes, locations);
   builder.setInsertionPointToEnd(curBlock);
   auto blockOp =
-      OpToCreate::create(builder, *currentOpLoc, *inputOps, successor);
+      OpToCreate::create(builder, getCurrentOpLoc(), *inputOps, successor);
   auto *blockBody = blockOp.createBlock();
-  if (failed(parseBlockContent(builder, blockBody, resTypes, *opLoc, blockOp)))
+  if (failed(parseBlockContent(builder, blockBody, resTypes, opLoc, blockOp)))
     return failure();
   builder.setInsertionPointToStart(successor);
   return {ValueRange{successor->getArguments()}};
 }
 
-// We can't use SFINAE in combination with deleted default instantiation
-// to identify which parsers are registered due to GCC < 14.1 bug, so we
-// use a constexpr variable to register them.
-// This is in order to avoid having to have only one "registration" of the
-// opcode.
-#define REGISTER_PARSER_OPCODE_PARSER(parserType, opcode, builderName,         \
-                                      parserName)                              \
-  template <>                                                                  \
-  constexpr bool parserType::hasParserForOpcode<opcode> = true;                \
-  template <>                                                                  \
-  inline parsed_inst_t parserType::parseInstrWithOpCode<opcode>(               \
-      OpBuilder & (builderName), ExpressionParser & (parserName))
-
-#define REGISTER_PRIMARY_WASM_INST_PARSER(opcode, builderName, parserName)     \
-  REGISTER_PARSER_OPCODE_PARSER(ExpressionParser::TopLevelInstParserRegistry,  \
-                                opcode, builderName, parserName)
-
-REGISTER_PRIMARY_WASM_INST_PARSER(WasmBinaryEncoding::OpCode::block, builder,
-                                  exprParser) {
+parsed_inst_t parse(OpCode<WasmBinaryEncoding::OpCode::block>,
+                    OpBuilder &builder,
+                    ExpressionParser::ExprParserProxy &exprParser) {
   return exprParser.parseBlockLikeOp<BlockOp>(builder);
 }
 
-REGISTER_PRIMARY_WASM_INST_PARSER(WasmBinaryEncoding::OpCode::loop, builder,
-                                  exprParser) {
+parsed_inst_t parse(OpCode<WasmBinaryEncoding::OpCode::loop>,
+                    OpBuilder &builder,
+                    ExpressionParser::ExprParserProxy &exprParser) {
   return exprParser.parseBlockLikeOp<LoopOp>(builder);
 }
 
-REGISTER_PRIMARY_WASM_INST_PARSER(WasmBinaryEncoding::OpCode::ifOpCode, builder,
-                                  exprParser) {
-  auto opLoc = exprParser.currentOpLoc;
+parsed_inst_t parse(OpCode<WasmBinaryEncoding::OpCode::ifOpCode>,
+                    OpBuilder &builder,
+                    ExpressionParser::ExprParserProxy &exprParser) {
+  auto opLoc = exprParser.getCurrentOpLoc();
   auto funcType = exprParser.parseBlockFuncType(builder);
   if (failed(funcType))
     return failure();
@@ -1117,14 +1069,14 @@ REGISTER_PRIMARY_WASM_INST_PARSER(WasmBinaryEncoding::OpCode::ifOpCode, builder,
       ByteSequence<WasmBinaryEncoding::endByte,
                    WasmBinaryEncoding::OpCode::elseOpCode>{};
   auto parseIfRes = exprParser.parseBlockContent(
-      builder, ifEntryBlock, resTypes, *opLoc, ifOp, ifElseFilter);
+      builder, ifEntryBlock, resTypes, opLoc, ifOp, ifElseFilter);
   if (failed(parseIfRes))
     return failure();
   if (*parseIfRes == WasmBinaryEncoding::OpCode::elseOpCode) {
     LDBG() << "  else block is present.";
     Block *elseEntryBlock = ifOp.createElseBlock();
     auto parseElseRes = exprParser.parseBlockContent(builder, elseEntryBlock,
-                                                     resTypes, *opLoc, ifOp);
+                                                     resTypes, opLoc, ifOp);
     if (failed(parseElseRes))
       return failure();
   }
@@ -1132,9 +1084,10 @@ REGISTER_PRIMARY_WASM_INST_PARSER(WasmBinaryEncoding::OpCode::ifOpCode, builder,
   return {ValueRange{successor->getArguments()}};
 }
 
-REGISTER_PRIMARY_WASM_INST_PARSER(WasmBinaryEncoding::OpCode::branchIf, builder,
-                                  exprParser) {
-  auto level = exprParser.parser.parseLiteral<uint32_t>();
+parsed_inst_t parse(OpCode<WasmBinaryEncoding::OpCode::branchIf>,
+                    OpBuilder &builder,
+                    ExpressionParser::ExprParserProxy &exprParser) {
+  auto level = exprParser.parser().parseLiteral<uint32_t>();
   if (failed(level))
     return failure();
   Block *curBlock = builder.getBlock();
@@ -1160,15 +1113,16 @@ REGISTER_PRIMARY_WASM_INST_PARSER(WasmBinaryEncoding::OpCode::branchIf, builder,
   return {*branchArgs};
 }
 
-REGISTER_PRIMARY_WASM_INST_PARSER(WasmBinaryEncoding::OpCode::call, builder,
-                                  exprParser) {
-  auto loc = *exprParser.currentOpLoc;
-  auto funcIdx = exprParser.parser.parseLiteral<uint32_t>();
+parsed_inst_t parse(OpCode<WasmBinaryEncoding::OpCode::call>,
+                    OpBuilder &builder,
+                    ExpressionParser::ExprParserProxy &exprParser) {
+  auto loc = exprParser.getCurrentOpLoc();
+  auto funcIdx = exprParser.parser().parseLiteral<uint32_t>();
   if (failed(funcIdx))
     return failure();
-  if (*funcIdx >= exprParser.symbols.funcSymbols.size())
+  if (*funcIdx >= exprParser.symbols().funcSymbols.size())
     return emitError(loc, "Invalid function index: ") << *funcIdx;
-  auto callee = exprParser.symbols.funcSymbols[*funcIdx];
+  auto callee = exprParser.symbols().funcSymbo...
[truncated]

``````````

</details>


https://github.com/llvm/llvm-project/pull/212709


More information about the Mlir-commits mailing list