[Mlir-commits] [mlir] [MLIR][CAPI][Python] Add C API and Python bindings for the remark engine (PR #229094)

Guray Ozen llvmlistbot at llvm.org
Tue Oct 6 01:23:12 PDT 2026


https://github.com/grypp updated https://github.com/llvm/llvm-project/pull/229094

>From 9d2b7caf1408a7bb02d3452c5047357d2a8eb7b7 Mon Sep 17 00:00:00 2001
From: Guray Ozen <gozen at nvidia.com>
Date: Mon, 5 Oct 2026 16:09:25 +0200
Subject: [PATCH 1/2] [MLIR][CAPI][Python] Add C API and Python bindings for
 the remark engine

Expose the optimization remark engine (mlir/IR/Remarks.h) through the C API
and the Python bindings, so clients outside C++ can enable it, receive the
reported remarks and emit their own.

C API (mlir-c/Remarks.h):
- mlirContextEnableOptimizationRemarks{,ToFile,WithCallback} select the
  categories (one regex per kind plus `all`), the emitting policy (all or
  final) and the sink: MLIR remark diagnostics, a YAML/bitstream file, or a
  callback invoked with an opaque MlirRemark.
- mlirContextFinalizeOptimizationRemarks flushes postponed remarks, writes
  the file and drops the engine; mlirContextHasRemarkEngine queries it.
- MlirRemark accessors (kind, remark/category/function names, location, id,
  key/value arguments, print), valid for the duration of the callback.
- mlirEmitOptimizationRemark emits a remark at a location through the
  engine of its context.

Python bindings:
- Context.enable_remarks(policy, format, output_file, filters, callback,
  print_as_emit_remarks), Context.finalize_remarks(), Context.remarks_enabled.
- ir.Remark and ir.RemarkKind, wrapping a remark for the duration of the
  callback and invalidated afterwards, like ir.Diagnostic.
- Location.emit_remark(kind, remark_name, category=, sub_category=,
  function_name=, message=, args=).

Tests: mlir/test/CAPI/remarks.c (mlir-capi-remarks-test) and
mlir/test/python/ir/remarks.py.

Co-Authored-By: Claude Fable 5.1 <noreply at anthropic.com>
---
 mlir/include/mlir-c/Remarks.h              | 186 +++++++++++
 mlir/include/mlir/Bindings/Python/IRCore.h |  55 ++++
 mlir/include/mlir/CAPI/Remarks.h           |  23 ++
 mlir/lib/Bindings/Python/IRCore.cpp        | 340 +++++++++++++++++++++
 mlir/lib/CAPI/IR/CMakeLists.txt            |   2 +
 mlir/lib/CAPI/IR/Remarks.cpp               | 235 ++++++++++++++
 mlir/test/CAPI/CMakeLists.txt              |   6 +
 mlir/test/CAPI/remarks.c                   | 252 +++++++++++++++
 mlir/test/CMakeLists.txt                   |   1 +
 mlir/test/lit.cfg.py                       |   1 +
 mlir/test/python/ir/remarks.py             | 303 ++++++++++++++++++
 11 files changed, 1404 insertions(+)
 create mode 100644 mlir/include/mlir-c/Remarks.h
 create mode 100644 mlir/include/mlir/CAPI/Remarks.h
 create mode 100644 mlir/lib/CAPI/IR/Remarks.cpp
 create mode 100644 mlir/test/CAPI/remarks.c
 create mode 100644 mlir/test/python/ir/remarks.py

diff --git a/mlir/include/mlir-c/Remarks.h b/mlir/include/mlir-c/Remarks.h
new file mode 100644
index 0000000000000..1bd93ba395af4
--- /dev/null
+++ b/mlir/include/mlir-c/Remarks.h
@@ -0,0 +1,186 @@
+//===-- mlir-c/Remarks.h - MLIR Remark Engine C API ---------------*- C -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This header declares the C interface to the MLIR optimization remark engine:
+// enabling the engine on a context with category filters, an emitting policy
+// and a sink (MLIR diagnostics, a YAML/bitstream file, or a callback),
+// emitting remarks, and inspecting a remark from inside a callback.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef MLIR_C_REMARKS_H
+#define MLIR_C_REMARKS_H
+
+#include "mlir-c/IR.h"
+#include "mlir-c/Support.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//===----------------------------------------------------------------------===//
+// Opaque types and enums.
+//===----------------------------------------------------------------------===//
+
+/// An opaque reference to a remark. A remark is only valid for the duration of
+/// the callback it is passed to; it must not be stored.
+struct MlirRemark {
+  const void *ptr;
+};
+typedef struct MlirRemark MlirRemark;
+
+/// The outcome a remark describes (see mlir::remark::RemarkKind).
+enum MlirRemarkKind {
+  MlirRemarkKindUnknown,
+  /// An optimization was applied.
+  MlirRemarkKindPassed,
+  /// A profitable optimization opportunity was found but not applied.
+  MlirRemarkKindMissed,
+  /// The optimization was attempted but failed.
+  MlirRemarkKindFailure,
+  /// Informational context without a pass/fail outcome.
+  MlirRemarkKindAnalysis
+};
+typedef enum MlirRemarkKind MlirRemarkKind;
+
+/// The emitting policy of the remark engine.
+enum MlirRemarkPolicy {
+  /// Report every remark as it is emitted.
+  MlirRemarkPolicyAll,
+  /// Postpone remarks and report the final set when the engine is finalized,
+  /// grouping related remarks under their parents.
+  MlirRemarkPolicyFinal
+};
+typedef enum MlirRemarkPolicy MlirRemarkPolicy;
+
+/// The serialization format of a remark output file.
+enum MlirRemarkFileFormat {
+  MlirRemarkFileFormatYAML,
+  MlirRemarkFileFormatBitstream
+};
+typedef enum MlirRemarkFileFormat MlirRemarkFileFormat;
+
+/// Regular expressions (llvm::Regex syntax, anchored by the engine) selecting
+/// the remark categories to report: `all` applies to every kind, the others to
+/// one kind each. A string of length 0 means the filter is not set, so the
+/// corresponding kind is not reported unless `all` matches.
+struct MlirRemarkCategories {
+  MlirStringRef all;
+  MlirStringRef passed;
+  MlirStringRef missed;
+  MlirStringRef analysis;
+  MlirStringRef failed;
+};
+typedef struct MlirRemarkCategories MlirRemarkCategories;
+
+/// Callback receiving every remark the engine reports.
+typedef void (*MlirRemarkCallback)(MlirRemark remark, void *userData);
+
+//===----------------------------------------------------------------------===//
+// Enabling and finalizing the engine of a context.
+//===----------------------------------------------------------------------===//
+
+/// Enables the remark engine on the context without a streamer; when
+/// `printAsEmitRemarks` is set, every reported remark is emitted as an MLIR
+/// remark diagnostic at its location. Fails when the context already has a
+/// remark engine (finalize it first).
+MLIR_CAPI_EXPORTED MlirLogicalResult mlirContextEnableOptimizationRemarks(
+    MlirContext context, MlirRemarkCategories categories,
+    MlirRemarkPolicy policy, bool printAsEmitRemarks);
+
+/// Enables the remark engine on the context, streaming the reported remarks to
+/// the file at `path` in `format`. The file is written when the engine is
+/// finalized. Fails when the file cannot be created or the context already has
+/// a remark engine.
+MLIR_CAPI_EXPORTED MlirLogicalResult mlirContextEnableOptimizationRemarksToFile(
+    MlirContext context, MlirRemarkCategories categories,
+    MlirRemarkPolicy policy, MlirRemarkFileFormat format, MlirStringRef path,
+    bool printAsEmitRemarks);
+
+/// Enables the remark engine on the context, delivering every reported remark
+/// to `callback`. `userData` is passed to the callback unchanged and released
+/// through `deleteUserData` (may be null) when the engine is finalized or the
+/// context is destroyed. Fails when the context already has a remark engine.
+MLIR_CAPI_EXPORTED MlirLogicalResult
+mlirContextEnableOptimizationRemarksWithCallback(
+    MlirContext context, MlirRemarkCategories categories,
+    MlirRemarkPolicy policy, MlirRemarkCallback callback, void *userData,
+    void (*deleteUserData)(void *), bool printAsEmitRemarks);
+
+/// Finalizes and removes the remark engine of the context: postponed remarks
+/// are reported, the output file is written and the callback's user data is
+/// released. Does nothing when no engine is enabled.
+MLIR_CAPI_EXPORTED void
+mlirContextFinalizeOptimizationRemarks(MlirContext context);
+
+/// Returns whether the context has a remark engine enabled.
+MLIR_CAPI_EXPORTED bool mlirContextHasRemarkEngine(MlirContext context);
+
+//===----------------------------------------------------------------------===//
+// Remark accessors (valid only inside a remark callback).
+//===----------------------------------------------------------------------===//
+
+/// Returns the kind of the remark.
+MLIR_CAPI_EXPORTED MlirRemarkKind mlirRemarkGetKind(MlirRemark remark);
+
+/// Returns the name identifying the remark.
+MLIR_CAPI_EXPORTED MlirStringRef mlirRemarkGetRemarkName(MlirRemark remark);
+
+/// Returns the category of the remark (the subject of the filters).
+MLIR_CAPI_EXPORTED MlirStringRef mlirRemarkGetCategoryName(MlirRemark remark);
+
+/// Returns the combined `category:subcategory` name of the remark.
+MLIR_CAPI_EXPORTED MlirStringRef
+mlirRemarkGetFullCategoryName(MlirRemark remark);
+
+/// Returns the name of the function the remark refers to.
+MLIR_CAPI_EXPORTED MlirStringRef mlirRemarkGetFunctionName(MlirRemark remark);
+
+/// Returns the location of the remark.
+MLIR_CAPI_EXPORTED MlirLocation mlirRemarkGetLocation(MlirRemark remark);
+
+/// Returns the unique id of the remark within its engine (0 when unset).
+MLIR_CAPI_EXPORTED uint64_t mlirRemarkGetId(MlirRemark remark);
+
+/// Returns the number of key/value arguments attached to the remark.
+MLIR_CAPI_EXPORTED intptr_t mlirRemarkGetNumArgs(MlirRemark remark);
+
+/// Returns the key of the `pos`-th argument of the remark.
+MLIR_CAPI_EXPORTED MlirStringRef mlirRemarkGetArgKey(MlirRemark remark,
+                                                     intptr_t pos);
+
+/// Returns the value of the `pos`-th argument of the remark.
+MLIR_CAPI_EXPORTED MlirStringRef mlirRemarkGetArgValue(MlirRemark remark,
+                                                       intptr_t pos);
+
+/// Prints the remark in its textual form, `[Kind] name | Category:... |
+/// Function=... | key=value, ...`, optionally followed by its location.
+MLIR_CAPI_EXPORTED void mlirRemarkPrint(MlirRemark remark, bool printLocation,
+                                        MlirStringCallback callback,
+                                        void *userData);
+
+//===----------------------------------------------------------------------===//
+// Emission.
+//===----------------------------------------------------------------------===//
+
+/// Emits a remark of `kind` at `location` through the remark engine of the
+/// location's context. `message` (when non-empty) and the `numArgs` key/value
+/// pairs become the arguments of the remark. Returns false, emitting nothing,
+/// when the context has no remark engine, the category is filtered out, or
+/// `kind` is MlirRemarkKindUnknown.
+MLIR_CAPI_EXPORTED bool mlirEmitOptimizationRemark(
+    MlirLocation location, MlirRemarkKind kind, MlirStringRef remarkName,
+    MlirStringRef categoryName, MlirStringRef subCategoryName,
+    MlirStringRef functionName, MlirStringRef message, intptr_t numArgs,
+    const MlirStringRef *argKeys, const MlirStringRef *argValues);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // MLIR_C_REMARKS_H
diff --git a/mlir/include/mlir/Bindings/Python/IRCore.h b/mlir/include/mlir/Bindings/Python/IRCore.h
index b28e6ec606d52..027201a9d95a5 100644
--- a/mlir/include/mlir/Bindings/Python/IRCore.h
+++ b/mlir/include/mlir/Bindings/Python/IRCore.h
@@ -26,6 +26,7 @@
 #include "mlir-c/ExtensibleDialect.h"
 #include "mlir-c/IR.h"
 #include "mlir-c/IntegerSet.h"
+#include "mlir-c/Remarks.h"
 #include "mlir-c/Support.h"
 #include "mlir-c/Transforms.h"
 #include "mlir/Bindings/Python/Nanobind.h"
@@ -253,6 +254,28 @@ class MLIR_PYTHON_API_EXPORTED PyMlirContext {
   /// registration object (internally a PyDiagnosticHandler).
   nanobind::object attachDiagnosticHandler(nanobind::object callback);
 
+  /// Enables the optimization remark engine of the context. `policy` is
+  /// "all" or "final"; `format` is "emit" (remarks become MLIR remark
+  /// diagnostics, or go to `callback` when one is given), "yaml" or
+  /// "bitstream" (remarks are streamed to `outputFile`). Raises ValueError on
+  /// invalid options or when an engine is already enabled.
+  void enableRemarks(const std::string &policy, const std::string &format,
+                     const std::string &outputFile,
+                     const std::string &allFilter,
+                     const std::string &passedFilter,
+                     const std::string &missedFilter,
+                     const std::string &analysisFilter,
+                     const std::string &failedFilter, nanobind::object callback,
+                     nanobind::object printAsEmitRemarks);
+  /// Finalizes and removes the remark engine; no-op when none is enabled.
+  void finalizeRemarks();
+  /// Whether a remark engine is enabled on the context.
+  bool getRemarksEnabled();
+  /// Whether `context` still has a live Python wrapper. False while the
+  /// wrapper is being destroyed, which is when a remark engine finalizing
+  /// with the context must not call back into Python.
+  static bool isLiveContext(MlirContext context);
+
   /// Controls whether error diagnostics should be propagated to diagnostic
   /// handlers, instead of being captured by `ErrorCapture`.
   void setEmitErrorDiagnostics(bool value) { emitErrorDiagnostics = value; }
@@ -353,6 +376,14 @@ enum class PyDiagnosticSeverity : std::underlying_type_t<
   Remark = MlirDiagnosticRemark
 };
 
+enum class PyRemarkKind : std::underlying_type_t<MlirRemarkKind> {
+  Unknown = MlirRemarkKindUnknown,
+  Passed = MlirRemarkKindPassed,
+  Missed = MlirRemarkKindMissed,
+  Failure = MlirRemarkKindFailure,
+  Analysis = MlirRemarkKindAnalysis
+};
+
 enum class PyWalkResult : std::underlying_type_t<MlirWalkResult> {
   Advance = MlirWalkResultAdvance,
   Interrupt = MlirWalkResultInterrupt,
@@ -410,6 +441,30 @@ class MLIR_PYTHON_API_EXPORTED PyDiagnostic {
   bool valid = true;
 };
 
+/// Wrapper around an MlirRemark as delivered to a remark callback. Like a
+/// PyDiagnostic it is only valid for the duration of the callback; accessing
+/// an invalidated remark raises ValueError.
+class MLIR_PYTHON_API_EXPORTED PyRemark {
+public:
+  PyRemark(MlirRemark remark) : remark(remark) {}
+  void invalidate() { valid = false; }
+  bool isValid() { return valid; }
+  PyRemarkKind getKind();
+  nanobind::str getRemarkName();
+  nanobind::str getCategoryName();
+  nanobind::str getFullCategoryName();
+  nanobind::str getFunctionName();
+  nanobind::typed<nanobind::object, PyLocation> getLocation();
+  uint64_t getId();
+  nanobind::list getArgs();
+  nanobind::str getMessage();
+
+private:
+  void checkValid();
+  MlirRemark remark;
+  bool valid = true;
+};
+
 /// Represents a diagnostic handler attached to the context. The handler's
 /// callback will be invoked with PyDiagnostic instances until the detach()
 /// method is called or the context is destroyed. A diagnostic handler can be
diff --git a/mlir/include/mlir/CAPI/Remarks.h b/mlir/include/mlir/CAPI/Remarks.h
new file mode 100644
index 0000000000000..cb477ce0ca982
--- /dev/null
+++ b/mlir/include/mlir/CAPI/Remarks.h
@@ -0,0 +1,23 @@
+//===- Remarks.h - C API Utils for MLIR Remarks -----------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file declares the conversion between the C API remark type and its C++
+// counterpart.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef MLIR_CAPI_REMARKS_H
+#define MLIR_CAPI_REMARKS_H
+
+#include "mlir-c/Remarks.h"
+#include "mlir/CAPI/Wrap.h"
+#include "mlir/IR/Remarks.h"
+
+DEFINE_C_API_PTR_METHODS(MlirRemark, const mlir::remark::detail::Remark)
+
+#endif // MLIR_CAPI_REMARKS_H
diff --git a/mlir/lib/Bindings/Python/IRCore.cpp b/mlir/lib/Bindings/Python/IRCore.cpp
index b6d92dc8064a2..7caddb6156423 100644
--- a/mlir/lib/Bindings/Python/IRCore.cpp
+++ b/mlir/lib/Bindings/Python/IRCore.cpp
@@ -17,6 +17,7 @@
 #include "mlir-c/Diagnostics.h"
 #include "mlir-c/ExtensibleDialect.h"
 #include "mlir-c/IR.h"
+#include "mlir-c/Remarks.h"
 #include "mlir-c/Support.h"
 
 #include <array>
@@ -714,6 +715,217 @@ void PyThreadContextEntry::popLocation(PyLocation &location) {
 // PyDiagnostic*
 //------------------------------------------------------------------------------
 
+//------------------------------------------------------------------------------
+// PyRemark and the remark engine of a context.
+//------------------------------------------------------------------------------
+
+namespace {
+/// User data of the remark callback streamer: the Python callable and the
+/// context it was installed on.
+struct PyRemarkCallbackData {
+  MlirContext context;
+  nb::object callback;
+};
+} // namespace
+
+static nb::str remarkStr(MlirStringRef ref) {
+  return nb::str(ref.data, ref.length);
+}
+
+static void pyRemarkCallback(MlirRemark remark, void *userData) {
+  auto *data = static_cast<PyRemarkCallbackData *>(userData);
+  // Since this can be called from arbitrary C++ contexts, always get the gil.
+  nb::gil_scoped_acquire gil;
+  // Postponed remarks are reported when the engine is destroyed. When that
+  // happens because the context itself is being destroyed, its Python wrapper
+  // is already gone and the remark cannot be delivered.
+  if (!PyMlirContext::isLiveContext(data->context))
+    return;
+  PyRemark *pyRemark = new PyRemark(remark);
+  nb::object pyRemarkObject = nb::cast(pyRemark, nb::rv_policy::take_ownership);
+  try {
+    data->callback(pyRemarkObject);
+  } catch (std::exception &e) {
+    fprintf(stderr, "MLIR Python remark callback raised exception: %s\n",
+            e.what());
+  }
+  pyRemark->invalidate();
+}
+
+static void pyRemarkCallbackDelete(void *userData) {
+  nb::gil_scoped_acquire gil;
+  delete static_cast<PyRemarkCallbackData *>(userData);
+}
+
+bool PyMlirContext::isLiveContext(MlirContext context) {
+  nb::ft_lock_guard lock(live_contexts_mutex);
+  return getLiveContexts().count(context.ptr) != 0;
+}
+
+void PyMlirContext::enableRemarks(
+    const std::string &policy, const std::string &format,
+    const std::string &outputFile, const std::string &allFilter,
+    const std::string &passedFilter, const std::string &missedFilter,
+    const std::string &analysisFilter, const std::string &failedFilter,
+    nb::object callback, nb::object printAsEmitRemarks) {
+  MlirRemarkPolicy remarkPolicy;
+  if (policy == "all") {
+    remarkPolicy = MlirRemarkPolicyAll;
+  } else if (policy == "final") {
+    remarkPolicy = MlirRemarkPolicyFinal;
+  } else {
+    throw nb::value_error(
+        ("unknown remark policy '" + policy + "'; expected 'all' or 'final'")
+            .c_str());
+  }
+  bool toFile = false;
+  MlirRemarkFileFormat fileFormat = MlirRemarkFileFormatYAML;
+  if (format == "yaml") {
+    toFile = true;
+  } else if (format == "bitstream") {
+    toFile = true;
+    fileFormat = MlirRemarkFileFormatBitstream;
+  } else if (format != "emit") {
+    throw nb::value_error(("unknown remark format '" + format +
+                           "'; expected 'emit', 'yaml' or 'bitstream'")
+                              .c_str());
+  }
+  bool hasCallback = !callback.is_none();
+  if (toFile && hasCallback)
+    throw nb::value_error(
+        ("a remark callback cannot be combined with format='" + format +
+         "'; use format='emit'")
+            .c_str());
+  if (mlirContextHasRemarkEngine(get()))
+    throw nb::value_error("remarks are already enabled on this context; call "
+                          "finalize_remarks() first");
+
+  MlirRemarkCategories categories{
+      toMlirStringRef(allFilter), toMlirStringRef(passedFilter),
+      toMlirStringRef(missedFilter), toMlirStringRef(analysisFilter),
+      toMlirStringRef(failedFilter)};
+  // Without a sink of its own, the engine prints as MLIR remark diagnostics.
+  bool emit = printAsEmitRemarks.is_none() ? (!toFile && !hasCallback)
+                                           : nb::cast<bool>(printAsEmitRemarks);
+
+  MlirLogicalResult result;
+  if (toFile) {
+    std::string path = outputFile;
+    if (path.empty())
+      path = fileFormat == MlirRemarkFileFormatYAML ? "mlir-remarks.yaml"
+                                                    : "mlir-remarks.bitstream";
+    result = mlirContextEnableOptimizationRemarksToFile(
+        get(), categories, remarkPolicy, fileFormat, toMlirStringRef(path),
+        emit);
+    if (mlirLogicalResultIsFailure(result))
+      throw nb::value_error(
+          ("failed to enable remarks: cannot write '" + path + "'").c_str());
+  } else if (hasCallback) {
+    auto *data = new PyRemarkCallbackData{get(), std::move(callback)};
+    result = mlirContextEnableOptimizationRemarksWithCallback(
+        get(), categories, remarkPolicy, pyRemarkCallback, data,
+        pyRemarkCallbackDelete, emit);
+  } else {
+    result = mlirContextEnableOptimizationRemarks(get(), categories,
+                                                  remarkPolicy, emit);
+  }
+  if (mlirLogicalResultIsFailure(result))
+    throw nb::value_error("failed to enable remarks");
+}
+
+void PyMlirContext::finalizeRemarks() {
+  mlirContextFinalizeOptimizationRemarks(get());
+}
+
+bool PyMlirContext::getRemarksEnabled() {
+  return mlirContextHasRemarkEngine(get());
+}
+
+void PyRemark::checkValid() {
+  if (!valid)
+    throw std::invalid_argument("Remark is invalid (used outside of callback)");
+}
+
+PyRemarkKind PyRemark::getKind() {
+  checkValid();
+  return static_cast<PyRemarkKind>(mlirRemarkGetKind(remark));
+}
+
+nb::str PyRemark::getRemarkName() {
+  checkValid();
+  return remarkStr(mlirRemarkGetRemarkName(remark));
+}
+
+nb::str PyRemark::getCategoryName() {
+  checkValid();
+  return remarkStr(mlirRemarkGetCategoryName(remark));
+}
+
+nb::str PyRemark::getFullCategoryName() {
+  checkValid();
+  return remarkStr(mlirRemarkGetFullCategoryName(remark));
+}
+
+nb::str PyRemark::getFunctionName() {
+  checkValid();
+  return remarkStr(mlirRemarkGetFunctionName(remark));
+}
+
+nb::typed<nb::object, PyLocation> PyRemark::getLocation() {
+  checkValid();
+  MlirLocation loc = mlirRemarkGetLocation(remark);
+  MlirContext context = mlirLocationGetContext(loc);
+  return PyLocation(PyMlirContext::forContext(context), loc).maybeDownCast();
+}
+
+uint64_t PyRemark::getId() {
+  checkValid();
+  return mlirRemarkGetId(remark);
+}
+
+nb::list PyRemark::getArgs() {
+  checkValid();
+  nb::list args;
+  intptr_t numArgs = mlirRemarkGetNumArgs(remark);
+  for (intptr_t i = 0; i < numArgs; ++i)
+    args.append(nb::make_tuple(remarkStr(mlirRemarkGetArgKey(remark, i)),
+                               remarkStr(mlirRemarkGetArgValue(remark, i))));
+  return args;
+}
+
+nb::str PyRemark::getMessage() {
+  checkValid();
+  std::string message;
+  auto callback = +[](MlirStringRef ref, void *userData) {
+    static_cast<std::string *>(userData)->append(ref.data, ref.length);
+  };
+  mlirRemarkPrint(remark, /*printLocation=*/false, callback, &message);
+  return nb::str(message.c_str(), message.size());
+}
+
+/// Converts the `kind` argument of `Location.emit_remark`, a RemarkKind or one
+/// of the strings "passed", "missed", "failure"/"failed" and "analysis".
+static MlirRemarkKind parseRemarkKind(nb::handle kind) {
+  if (nb::isinstance<PyRemarkKind>(kind))
+    return static_cast<MlirRemarkKind>(nb::cast<PyRemarkKind>(kind));
+  std::string name;
+  if (nb::try_cast<std::string>(kind, name)) {
+    if (name == "passed")
+      return MlirRemarkKindPassed;
+    if (name == "missed")
+      return MlirRemarkKindMissed;
+    if (name == "failure" || name == "failed")
+      return MlirRemarkKindFailure;
+    if (name == "analysis")
+      return MlirRemarkKindAnalysis;
+    throw nb::value_error(("unknown remark kind '" + name +
+                           "'; expected 'passed', 'missed', 'failure' or "
+                           "'analysis'")
+                              .c_str());
+  }
+  throw nb::type_error("remark kind must be a RemarkKind or a string");
+}
+
 void PyDiagnostic::invalidate() {
   valid = false;
   if (materializedNotes) {
@@ -3326,6 +3538,13 @@ void populateIRCore(nb::module_ &m) {
       .value("NOTE", PyDiagnosticSeverity::Note)
       .value("REMARK", PyDiagnosticSeverity::Remark);
 
+  nb::enum_<PyRemarkKind>(m, "RemarkKind")
+      .value("UNKNOWN", PyRemarkKind::Unknown)
+      .value("PASSED", PyRemarkKind::Passed)
+      .value("MISSED", PyRemarkKind::Missed)
+      .value("FAILURE", PyRemarkKind::Failure)
+      .value("ANALYSIS", PyRemarkKind::Analysis);
+
   nb::enum_<PyWalkOrder>(m, "WalkOrder")
       .value("PRE_ORDER", PyWalkOrder::PreOrder)
       .value("POST_ORDER", PyWalkOrder::PostOrder);
@@ -3401,6 +3620,37 @@ void populateIRCore(nb::module_ &m) {
            "exc_value"_a.none(), "traceback"_a.none(),
            "Exits the diagnostic handler context manager.");
 
+  nb::class_<PyRemark>(m, "Remark")
+      .def_prop_ro("kind", &PyRemark::getKind,
+                   "Returns the kind of the remark.")
+      .def_prop_ro("remark_name", &PyRemark::getRemarkName,
+                   "Returns the name identifying the remark.")
+      .def_prop_ro("category_name", &PyRemark::getCategoryName,
+                   "Returns the category of the remark (what the filters "
+                   "match).")
+      .def_prop_ro("full_category_name", &PyRemark::getFullCategoryName,
+                   "Returns the combined `category:subcategory` name.")
+      .def_prop_ro("function_name", &PyRemark::getFunctionName,
+                   "Returns the name of the function the remark refers to.")
+      .def_prop_ro("location", &PyRemark::getLocation,
+                   "Returns the location associated with the remark.")
+      .def_prop_ro("remark_id", &PyRemark::getId,
+                   "Returns the id of the remark within its engine (0 when "
+                   "unset).")
+      .def_prop_ro("args", &PyRemark::getArgs,
+                   "Returns the key/value arguments as a list of tuples.")
+      .def_prop_ro("message", &PyRemark::getMessage,
+                   "Returns the textual form of the remark, `[Kind] name | "
+                   "Category:... | key=value, ...`.")
+      .def(
+          "__str__",
+          [](PyRemark &self) -> nb::str {
+            if (!self.isValid())
+              return nb::str("<Invalid Remark>");
+            return self.getMessage();
+          },
+          "Returns the remark message as a string.");
+
   // Expose DefaultThreadPool to python
   nb::class_<PyThreadPool>(m, "ThreadPool")
       .def(
@@ -3499,6 +3749,55 @@ void populateIRCore(nb::module_ &m) {
       .def("attach_diagnostic_handler", &PyMlirContext::attachDiagnosticHandler,
            "callback"_a,
            "Attaches a diagnostic handler that will receive callbacks.")
+      .def("enable_remarks", &PyMlirContext::enableRemarks, nb::kw_only(),
+           "policy"_a = "all", "format"_a = "emit", "output_file"_a = "",
+           "all_filter"_a = "", "passed_filter"_a = "", "missed_filter"_a = "",
+           "analysis_filter"_a = "", "failed_filter"_a = "",
+           "callback"_a.none() = nb::none(),
+           "print_as_emit_remarks"_a.none() = nb::none(),
+           R"(
+            Enables the optimization remark engine on this context.
+
+            Remarks are reported by passes (and by `Location.emit_remark`) and
+            selected by the category filters, regular expressions anchored by
+            the engine: `all_filter` applies to every kind, the other filters to
+            one kind each; a kind without a matching filter is not reported.
+
+            Args:
+              policy: "all" reports remarks as they are emitted; "final"
+                postpones them until `finalize_remarks()` and reports the final
+                set, grouping related remarks under their parents.
+              format: "emit" delivers remarks as MLIR remark diagnostics, or to
+                `callback` when one is given; "yaml" and "bitstream" stream
+                them to `output_file`, written on `finalize_remarks()`.
+              output_file: The output path for the file formats; defaults to
+                "mlir-remarks.yaml" / "mlir-remarks.bitstream".
+              all_filter: Category regex applied to every remark kind.
+              passed_filter: Category regex for passed remarks.
+              missed_filter: Category regex for missed remarks.
+              analysis_filter: Category regex for analysis remarks.
+              failed_filter: Category regex for failed remarks.
+              callback: A callable receiving one `Remark` per reported remark;
+                the remark is only valid during the call. Requires
+                format="emit".
+              print_as_emit_remarks: Also emit every remark as an MLIR remark
+                diagnostic; defaults to True only for format="emit" without a
+                callback.
+
+            Raises:
+              ValueError: On an unknown policy, format or kind combination,
+                when remarks are already enabled, or when the output file
+                cannot be written.)")
+      .def("finalize_remarks", &PyMlirContext::finalizeRemarks,
+           R"(
+            Finalizes and removes the remark engine of this context.
+
+            Postponed remarks (policy "final") are reported, the output file is
+            written and the callback is released. Does nothing when no engine
+            is enabled; `enable_remarks` may be called again afterwards.)")
+      .def_prop_ro(
+          "remarks_enabled", &PyMlirContext::getRemarksEnabled,
+          "Returns True if a remark engine is enabled on this context.")
       .def(
           "enable_multithreading",
           [](PyMlirContext &self, bool enable) {
@@ -3853,6 +4152,47 @@ void populateIRCore(nb::module_ &m) {
 
             Args:
               message: The error message to emit.)")
+      .def(
+          "emit_remark",
+          [](PyLocation &self, nb::object kind, const std::string &remarkName,
+             const std::string &category, const std::string &subCategory,
+             const std::string &functionName, const std::string &message,
+             const std::vector<std::pair<std::string, std::string>> &args)
+              -> bool {
+            MlirRemarkKind remarkKind = parseRemarkKind(kind);
+            std::vector<MlirStringRef> keys, values;
+            keys.reserve(args.size());
+            values.reserve(args.size());
+            for (const auto &[key, value] : args) {
+              keys.push_back(toMlirStringRef(key));
+              values.push_back(toMlirStringRef(value));
+            }
+            return mlirEmitOptimizationRemark(
+                self, remarkKind, toMlirStringRef(remarkName),
+                toMlirStringRef(category), toMlirStringRef(subCategory),
+                toMlirStringRef(functionName), toMlirStringRef(message),
+                static_cast<intptr_t>(keys.size()), keys.data(), values.data());
+          },
+          "kind"_a, "remark_name"_a, nb::kw_only(), "category"_a = "",
+          "sub_category"_a = "", "function_name"_a = "", "message"_a = "",
+          "args"_a = std::vector<std::pair<std::string, std::string>>(),
+          R"(
+            Emits an optimization remark at this location through the remark
+            engine of the context (see `Context.enable_remarks`).
+
+            Args:
+              kind: A `RemarkKind`, or one of "passed", "missed", "failure"
+                and "analysis".
+              remark_name: The name identifying the remark.
+              category: The category the filters are matched against.
+              sub_category: An optional sub-category.
+              function_name: The function the remark refers to.
+              message: Free text attached to the remark.
+              args: Key/value pairs attached to the remark.
+
+            Returns:
+              True if the remark was reported; False when no remark engine is
+              enabled or the category is filtered out.)")
       .def(
           "__str__",
           [](PyLocation &self) {
diff --git a/mlir/lib/CAPI/IR/CMakeLists.txt b/mlir/lib/CAPI/IR/CMakeLists.txt
index 8bc76677d2bfd..32b8fa409e105 100644
--- a/mlir/lib/CAPI/IR/CMakeLists.txt
+++ b/mlir/lib/CAPI/IR/CMakeLists.txt
@@ -11,12 +11,14 @@ add_mlir_upstream_c_api_library(MLIRCAPIIR
   IntegerSet.cpp
   IR.cpp
   Pass.cpp
+  Remarks.cpp
   Support.cpp
 
   LINK_LIBS PUBLIC
   MLIRBytecodeWriter
   MLIRIR
   MLIRParser
+  MLIRRemarkStreamer
   MLIRSupport
   MLIRPass
 )
diff --git a/mlir/lib/CAPI/IR/Remarks.cpp b/mlir/lib/CAPI/IR/Remarks.cpp
new file mode 100644
index 0000000000000..5a54e5b38895c
--- /dev/null
+++ b/mlir/lib/CAPI/IR/Remarks.cpp
@@ -0,0 +1,235 @@
+//===- Remarks.cpp - C Interface for the MLIR Remark Engine ---------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir-c/Remarks.h"
+#include "mlir/CAPI/IR.h"
+#include "mlir/CAPI/Remarks.h"
+#include "mlir/CAPI/Support.h"
+#include "mlir/CAPI/Utils.h"
+#include "mlir/IR/MLIRContext.h"
+#include "mlir/IR/Remarks.h"
+#include "mlir/Remark/RemarkStreamer.h"
+#include "llvm/Remarks/RemarkFormat.h"
+
+using namespace mlir;
+using mlir::remark::detail::Remark;
+
+//===----------------------------------------------------------------------===//
+// Helpers
+//===----------------------------------------------------------------------===//
+
+namespace {
+/// A streamer forwarding every reported remark to a C callback.
+class CallbackRemarkStreamer : public remark::detail::MLIRRemarkStreamerBase {
+public:
+  CallbackRemarkStreamer(MlirRemarkCallback callback, void *userData,
+                         void (*deleteUserData)(void *))
+      : callback(callback), userData(userData), deleteUserData(deleteUserData) {
+  }
+
+  ~CallbackRemarkStreamer() override {
+    if (deleteUserData)
+      deleteUserData(userData);
+  }
+
+  void streamOptimizationRemark(const Remark &remark) override {
+    callback(wrap(&remark), userData);
+  }
+
+private:
+  MlirRemarkCallback callback;
+  void *userData;
+  void (*deleteUserData)(void *);
+};
+} // namespace
+
+/// Like mlir-opt, every filter is handed to the engine as a (possibly empty)
+/// string: the engine then reports a kind when its own filter or `all`
+/// matches, and reports nothing for a kind whose filters are both empty.
+static remark::RemarkCategories unwrap(MlirRemarkCategories categories) {
+  return remark::RemarkCategories{
+      unwrap(categories.all).str(), unwrap(categories.passed).str(),
+      unwrap(categories.missed).str(), unwrap(categories.analysis).str(),
+      unwrap(categories.failed).str()};
+}
+
+static std::unique_ptr<remark::detail::RemarkEmittingPolicyBase>
+createPolicy(MlirRemarkPolicy policy) {
+  switch (policy) {
+  case MlirRemarkPolicyAll:
+    return std::make_unique<remark::RemarkEmittingPolicyAll>();
+  case MlirRemarkPolicyFinal:
+    return std::make_unique<remark::RemarkEmittingPolicyFinal>();
+  }
+  llvm_unreachable("unknown remark policy");
+}
+
+static MlirRemarkKind wrap(remark::RemarkKind kind) {
+  switch (kind) {
+  case remark::RemarkKind::RemarkUnknown:
+    return MlirRemarkKindUnknown;
+  case remark::RemarkKind::RemarkPassed:
+    return MlirRemarkKindPassed;
+  case remark::RemarkKind::RemarkMissed:
+    return MlirRemarkKindMissed;
+  case remark::RemarkKind::RemarkFailure:
+    return MlirRemarkKindFailure;
+  case remark::RemarkKind::RemarkAnalysis:
+    return MlirRemarkKindAnalysis;
+  }
+  llvm_unreachable("unknown remark kind");
+}
+
+//===----------------------------------------------------------------------===//
+// Enabling and finalizing
+//===----------------------------------------------------------------------===//
+
+MlirLogicalResult mlirContextEnableOptimizationRemarks(
+    MlirContext context, MlirRemarkCategories categories,
+    MlirRemarkPolicy policy, bool printAsEmitRemarks) {
+  MLIRContext *ctx = unwrap(context);
+  if (ctx->getRemarkEngine())
+    return mlirLogicalResultFailure();
+  return wrap(remark::enableOptimizationRemarks(
+      *ctx, /*streamer=*/nullptr, createPolicy(policy), unwrap(categories),
+      printAsEmitRemarks));
+}
+
+MlirLogicalResult mlirContextEnableOptimizationRemarksToFile(
+    MlirContext context, MlirRemarkCategories categories,
+    MlirRemarkPolicy policy, MlirRemarkFileFormat format, MlirStringRef path,
+    bool printAsEmitRemarks) {
+  MLIRContext *ctx = unwrap(context);
+  if (ctx->getRemarkEngine())
+    return mlirLogicalResultFailure();
+  llvm::remarks::Format llvmFormat = format == MlirRemarkFileFormatBitstream
+                                         ? llvm::remarks::Format::Bitstream
+                                         : llvm::remarks::Format::YAML;
+  return wrap(remark::enableOptimizationRemarksWithLLVMStreamer(
+      *ctx, unwrap(path), llvmFormat, createPolicy(policy), unwrap(categories),
+      printAsEmitRemarks));
+}
+
+MlirLogicalResult mlirContextEnableOptimizationRemarksWithCallback(
+    MlirContext context, MlirRemarkCategories categories,
+    MlirRemarkPolicy policy, MlirRemarkCallback callback, void *userData,
+    void (*deleteUserData)(void *), bool printAsEmitRemarks) {
+  assert(callback && "unexpected null remark callback");
+  MLIRContext *ctx = unwrap(context);
+  if (ctx->getRemarkEngine()) {
+    if (deleteUserData)
+      deleteUserData(userData);
+    return mlirLogicalResultFailure();
+  }
+  return wrap(remark::enableOptimizationRemarks(
+      *ctx,
+      std::make_unique<CallbackRemarkStreamer>(callback, userData,
+                                               deleteUserData),
+      createPolicy(policy), unwrap(categories), printAsEmitRemarks));
+}
+
+void mlirContextFinalizeOptimizationRemarks(MlirContext context) {
+  // The engine destructor finalizes the policy (reporting postponed remarks)
+  // and then the streamer (writing the file / releasing the user data).
+  unwrap(context)->setRemarkEngine(nullptr);
+}
+
+bool mlirContextHasRemarkEngine(MlirContext context) {
+  return unwrap(context)->getRemarkEngine() != nullptr;
+}
+
+//===----------------------------------------------------------------------===//
+// Remark accessors
+//===----------------------------------------------------------------------===//
+
+MlirRemarkKind mlirRemarkGetKind(MlirRemark remark) {
+  return wrap(unwrap(remark)->getRemarkKind());
+}
+
+MlirStringRef mlirRemarkGetRemarkName(MlirRemark remark) {
+  return wrap(unwrap(remark)->getRemarkName());
+}
+
+MlirStringRef mlirRemarkGetCategoryName(MlirRemark remark) {
+  return wrap(unwrap(remark)->getCategoryName());
+}
+
+MlirStringRef mlirRemarkGetFullCategoryName(MlirRemark remark) {
+  return wrap(unwrap(remark)->getCombinedCategoryName());
+}
+
+MlirStringRef mlirRemarkGetFunctionName(MlirRemark remark) {
+  return wrap(unwrap(remark)->getFunction());
+}
+
+MlirLocation mlirRemarkGetLocation(MlirRemark remark) {
+  return wrap(unwrap(remark)->getLocation());
+}
+
+uint64_t mlirRemarkGetId(MlirRemark remark) {
+  return unwrap(remark)->getId().getValue();
+}
+
+intptr_t mlirRemarkGetNumArgs(MlirRemark remark) {
+  return static_cast<intptr_t>(unwrap(remark)->getArgs().size());
+}
+
+MlirStringRef mlirRemarkGetArgKey(MlirRemark remark, intptr_t pos) {
+  return wrap(llvm::StringRef(unwrap(remark)->getArgs()[pos].key));
+}
+
+MlirStringRef mlirRemarkGetArgValue(MlirRemark remark, intptr_t pos) {
+  return wrap(llvm::StringRef(unwrap(remark)->getArgs()[pos].val));
+}
+
+void mlirRemarkPrint(MlirRemark remark, bool printLocation,
+                     MlirStringCallback callback, void *userData) {
+  detail::CallbackOstream stream(callback, userData);
+  unwrap(remark)->print(stream, printLocation);
+}
+
+//===----------------------------------------------------------------------===//
+// Emission
+//===----------------------------------------------------------------------===//
+
+bool mlirEmitOptimizationRemark(
+    MlirLocation location, MlirRemarkKind kind, MlirStringRef remarkName,
+    MlirStringRef categoryName, MlirStringRef subCategoryName,
+    MlirStringRef functionName, MlirStringRef message, intptr_t numArgs,
+    const MlirStringRef *argKeys, const MlirStringRef *argValues) {
+  Location loc = unwrap(location);
+  remark::RemarkOpts opts = remark::RemarkOpts::name(unwrap(remarkName))
+                                .category(unwrap(categoryName))
+                                .subCategory(unwrap(subCategoryName))
+                                .function(unwrap(functionName));
+  remark::detail::InFlightRemark inFlight;
+  switch (kind) {
+  case MlirRemarkKindUnknown:
+    return false;
+  case MlirRemarkKindPassed:
+    inFlight = remark::passed(loc, opts);
+    break;
+  case MlirRemarkKindMissed:
+    inFlight = remark::missed(loc, opts);
+    break;
+  case MlirRemarkKindFailure:
+    inFlight = remark::failed(loc, opts);
+    break;
+  case MlirRemarkKindAnalysis:
+    inFlight = remark::analysis(loc, opts);
+    break;
+  }
+  if (!inFlight)
+    return false;
+  if (message.length != 0)
+    inFlight << unwrap(message);
+  for (intptr_t i = 0; i < numArgs; ++i)
+    inFlight << Remark::Arg(unwrap(argKeys[i]), unwrap(argValues[i]));
+  // The remark is reported when `inFlight` goes out of scope.
+  return true;
+}
diff --git a/mlir/test/CAPI/CMakeLists.txt b/mlir/test/CAPI/CMakeLists.txt
index d29e7744527ef..bd8b58eb442bf 100644
--- a/mlir/test/CAPI/CMakeLists.txt
+++ b/mlir/test/CAPI/CMakeLists.txt
@@ -64,6 +64,12 @@ _add_capi_test_executable(mlir-capi-llvm-test
     MLIRCAPIRegisterEverything
 )
 
+_add_capi_test_executable(mlir-capi-remarks-test
+  remarks.c
+  LINK_LIBS PRIVATE
+    MLIRCAPIIR
+)
+
 _add_capi_test_executable(mlir-capi-pass-test
   pass.c
   LINK_LIBS PRIVATE
diff --git a/mlir/test/CAPI/remarks.c b/mlir/test/CAPI/remarks.c
new file mode 100644
index 0000000000000..1b74ac624c0f2
--- /dev/null
+++ b/mlir/test/CAPI/remarks.c
@@ -0,0 +1,252 @@
+//===- remarks.c - Test of the remark engine C API ------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM
+// Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+/* RUN: mlir-capi-remarks-test 2>&1 | FileCheck %s
+ */
+
+#include "mlir-c/Remarks.h"
+#include "mlir-c/Diagnostics.h"
+#include "mlir-c/IR.h"
+#include "mlir-c/Support.h"
+
+#include <assert.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void printToStderr(MlirStringRef str, void *userData) {
+  (void)userData;
+  fwrite(str.data, 1, str.length, stderr);
+}
+
+static MlirStringRef str(const char *s) {
+  return mlirStringRefCreateFromCString(s);
+}
+
+static MlirRemarkCategories allCategories(const char *filter) {
+  MlirRemarkCategories cats;
+  memset(&cats, 0, sizeof(cats));
+  cats.all = str(filter);
+  return cats;
+}
+
+/// The callback streamer: prints every field of the remark it receives and
+/// counts the deliveries in userData.
+static void remarkCallback(MlirRemark remark, void *userData) {
+  int *count = (int *)userData;
+  ++*count;
+  const char *kind = "?";
+  switch (mlirRemarkGetKind(remark)) {
+  case MlirRemarkKindPassed:
+    kind = "passed";
+    break;
+  case MlirRemarkKindMissed:
+    kind = "missed";
+    break;
+  case MlirRemarkKindFailure:
+    kind = "failure";
+    break;
+  case MlirRemarkKindAnalysis:
+    kind = "analysis";
+    break;
+  case MlirRemarkKindUnknown:
+    kind = "unknown";
+    break;
+  }
+  MlirStringRef name = mlirRemarkGetRemarkName(remark);
+  MlirStringRef category = mlirRemarkGetCategoryName(remark);
+  MlirStringRef fullCategory = mlirRemarkGetFullCategoryName(remark);
+  MlirStringRef function = mlirRemarkGetFunctionName(remark);
+  fprintf(stderr,
+          "remark #%d kind=%s name=%.*s category=%.*s full=%.*s "
+          "function=%.*s id=%" PRIu64 "\n",
+          *count, kind, (int)name.length, name.data, (int)category.length,
+          category.data, (int)fullCategory.length, fullCategory.data,
+          (int)function.length, function.data, mlirRemarkGetId(remark));
+  intptr_t numArgs = mlirRemarkGetNumArgs(remark);
+  for (intptr_t i = 0; i < numArgs; ++i) {
+    MlirStringRef key = mlirRemarkGetArgKey(remark, i);
+    MlirStringRef value = mlirRemarkGetArgValue(remark, i);
+    fprintf(stderr, "  arg %.*s=%.*s\n", (int)key.length, key.data,
+            (int)value.length, value.data);
+  }
+  fprintf(stderr, "  print: ");
+  mlirRemarkPrint(remark, /*printLocation=*/true, printToStderr, NULL);
+  fprintf(stderr, "\n  location: ");
+  mlirLocationPrint(mlirRemarkGetLocation(remark), printToStderr, NULL);
+  fprintf(stderr, "\n");
+}
+
+static void deleteUserData(void *userData) {
+  fprintf(stderr, "deleteUserData called (count=%d)\n", *(int *)userData);
+}
+
+static bool emit(MlirLocation loc, MlirRemarkKind kind, const char *name,
+                 const char *category, const char *message) {
+  MlirStringRef keys[1] = {str("factor")};
+  MlirStringRef values[1] = {str("4")};
+  return mlirEmitOptimizationRemark(loc, kind, str(name), str(category),
+                                    str("inner"), str("main"), str(message), 1,
+                                    keys, values);
+}
+
+// CHECK-LABEL: @testCallbackStreamer
+static void testCallbackStreamer(void) {
+  fprintf(stderr, "@testCallbackStreamer\n");
+  MlirContext ctx = mlirContextCreate();
+  MlirLocation loc = mlirLocationFileLineColGet(ctx, str("remarks.c"), 7, 3);
+
+  // Without an engine nothing is emitted.
+  // CHECK: no engine: enabled=0 emitted=0
+  bool emitted = emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "dropped");
+  fprintf(stderr, "no engine: enabled=%d emitted=%d\n",
+          mlirContextHasRemarkEngine(ctx), emitted);
+
+  int count = 0;
+  MlirLogicalResult res = mlirContextEnableOptimizationRemarksWithCallback(
+      ctx, allCategories("Loop"), MlirRemarkPolicyAll, remarkCallback, &count,
+      deleteUserData, /*printAsEmitRemarks=*/false);
+  // CHECK: enabled=1 ok=1
+  fprintf(stderr, "enabled=%d ok=%d\n", mlirContextHasRemarkEngine(ctx),
+          mlirLogicalResultIsSuccess(res));
+
+  // Enabling twice fails and leaves the first engine in place.
+  // CHECK: second enable ok=0
+  res = mlirContextEnableOptimizationRemarks(ctx, allCategories(".*"),
+                                             MlirRemarkPolicyAll, true);
+  fprintf(stderr, "second enable ok=%d\n", mlirLogicalResultIsSuccess(res));
+
+  // clang-format off
+  // CHECK: remark #1 kind=passed name=Unroll category=Loop full=Loop:inner function=main id=1
+  // CHECK:   arg RemarkId=1
+  // CHECK:   arg Remark=unrolled
+  // CHECK:   arg factor=4
+  // CHECK:   print: [Passed] Unroll | Category:Loop:inner | Function=main |  @"remarks.c":7:3{{ ?}}Remark=unrolled, RemarkId=1, factor=4
+  // CHECK:   location: loc("remarks.c":7:3)
+  // CHECK: emitted=1
+  // clang-format on
+  emitted = emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "unrolled");
+  fprintf(stderr, "emitted=%d\n", emitted);
+
+  // A category the filter does not match is dropped before the streamer.
+  // CHECK-NOT: name=Vectorize
+  // CHECK: count after filtered emit=1
+  emit(loc, MlirRemarkKindMissed, "Vectorize", "Vector", "not matched");
+  fprintf(stderr, "count after filtered emit=%d\n", count);
+
+  // CHECK: remark #2 kind=missed name=Interchange
+  // CHECK: remark #3 kind=failure name=Fuse
+  // CHECK: remark #4 kind=analysis name=TripCount
+  emit(loc, MlirRemarkKindMissed, "Interchange", "Loop", "no");
+  emit(loc, MlirRemarkKindFailure, "Fuse", "Loop", "no");
+  emit(loc, MlirRemarkKindAnalysis, "TripCount", "Loop", "4");
+
+  // Finalize drops the engine and releases the user data.
+  // CHECK: deleteUserData called (count=4)
+  // CHECK: after finalize: enabled=0 count=4
+  mlirContextFinalizeOptimizationRemarks(ctx);
+  fprintf(stderr, "after finalize: enabled=%d count=%d\n",
+          mlirContextHasRemarkEngine(ctx), count);
+  // Finalizing without an engine is a no-op.
+  mlirContextFinalizeOptimizationRemarks(ctx);
+  mlirContextDestroy(ctx);
+}
+
+// CHECK-LABEL: @testFinalPolicy
+static void testFinalPolicy(void) {
+  fprintf(stderr, "@testFinalPolicy\n");
+  MlirContext ctx = mlirContextCreate();
+  MlirLocation loc = mlirLocationUnknownGet(ctx);
+  int count = 0;
+  MlirRemarkCategories cats;
+  memset(&cats, 0, sizeof(cats));
+  cats.passed = str("Loop");
+  MlirLogicalResult res = mlirContextEnableOptimizationRemarksWithCallback(
+      ctx, cats, MlirRemarkPolicyFinal, remarkCallback, &count, NULL, false);
+  assert(mlirLogicalResultIsSuccess(res));
+  emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "postponed");
+  // Only the passed filter is set: a missed remark is not reported.
+  emit(loc, MlirRemarkKindMissed, "Interchange", "Loop", "dropped");
+  // CHECK: before finalize count=0
+  fprintf(stderr, "before finalize count=%d\n", count);
+  // CHECK: remark #1 kind=passed name=Unroll
+  // CHECK: after finalize count=1
+  mlirContextFinalizeOptimizationRemarks(ctx);
+  fprintf(stderr, "after finalize count=%d\n", count);
+  mlirContextDestroy(ctx);
+}
+
+static MlirLogicalResult diagnosticHandler(MlirDiagnostic diagnostic,
+                                           void *userData) {
+  (void)userData;
+  const char *severity = "other";
+  if (mlirDiagnosticGetSeverity(diagnostic) == MlirDiagnosticRemark)
+    severity = "remark";
+  fprintf(stderr, "diagnostic %s: ", severity);
+  mlirDiagnosticPrint(diagnostic, printToStderr, NULL);
+  fprintf(stderr, "\n");
+  return mlirLogicalResultSuccess();
+}
+
+// CHECK-LABEL: @testEmitRemarkForm
+static void testEmitRemarkForm(void) {
+  fprintf(stderr, "@testEmitRemarkForm\n");
+  MlirContext ctx = mlirContextCreate();
+  MlirLocation loc = mlirLocationUnknownGet(ctx);
+  MlirDiagnosticHandlerID id =
+      mlirContextAttachDiagnosticHandler(ctx, diagnosticHandler, NULL, NULL);
+  MlirLogicalResult res = mlirContextEnableOptimizationRemarks(
+      ctx, allCategories(".*"), MlirRemarkPolicyAll,
+      /*printAsEmitRemarks=*/true);
+  assert(mlirLogicalResultIsSuccess(res));
+  // clang-format off
+  // CHECK: diagnostic remark: [Analysis] TripCount | Category:Loop:inner | Function=main | Remark=4, RemarkId=1, factor=4
+  // clang-format on
+  emit(loc, MlirRemarkKindAnalysis, "TripCount", "Loop", "4");
+  mlirContextFinalizeOptimizationRemarks(ctx);
+  mlirContextDetachDiagnosticHandler(ctx, id);
+  mlirContextDestroy(ctx);
+}
+
+// CHECK-LABEL: @testFileStreamer
+static void testFileStreamer(void) {
+  fprintf(stderr, "@testFileStreamer\n");
+  MlirContext ctx = mlirContextCreate();
+  MlirLocation loc = mlirLocationUnknownGet(ctx);
+  char path[] = "mlir-capi-remarks-test.yaml";
+  MlirLogicalResult res = mlirContextEnableOptimizationRemarksToFile(
+      ctx, allCategories(".*"), MlirRemarkPolicyAll, MlirRemarkFileFormatYAML,
+      str(path), false);
+  // CHECK: file enable ok=1
+  fprintf(stderr, "file enable ok=%d\n", mlirLogicalResultIsSuccess(res));
+  emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "to file");
+  mlirContextFinalizeOptimizationRemarks(ctx);
+  FILE *f = fopen(path, "r");
+  bool found = false;
+  if (f) {
+    char line[256];
+    while (fgets(line, sizeof(line), f))
+      if (strstr(line, "Unroll"))
+        found = true;
+    fclose(f);
+    remove(path);
+  }
+  // CHECK: yaml has remark=1
+  fprintf(stderr, "yaml has remark=%d\n", found);
+  mlirContextDestroy(ctx);
+}
+
+int main(void) {
+  testCallbackStreamer();
+  testFinalPolicy();
+  testEmitRemarkForm();
+  testFileStreamer();
+  return 0;
+}
diff --git a/mlir/test/CMakeLists.txt b/mlir/test/CMakeLists.txt
index c79949737d64b..747d0cf68116b 100644
--- a/mlir/test/CMakeLists.txt
+++ b/mlir/test/CMakeLists.txt
@@ -126,6 +126,7 @@ set(MLIR_TEST_DEPENDS
   mlir-capi-llvm-test
   mlir-capi-pass-test
   mlir-capi-quant-test
+  mlir-capi-remarks-test
   mlir-capi-rewrite-test
   mlir-capi-smt-test
   mlir-capi-sparse-tensor-test
diff --git a/mlir/test/lit.cfg.py b/mlir/test/lit.cfg.py
index 3622fc0823fdf..dbd18f8e8b0f2 100644
--- a/mlir/test/lit.cfg.py
+++ b/mlir/test/lit.cfg.py
@@ -189,6 +189,7 @@ def find_real_python_interpreter():
     "mlir-capi-pass-test",
     "mlir-capi-pdl-test",
     "mlir-capi-quant-test",
+    "mlir-capi-remarks-test",
     "mlir-capi-rewrite-test",
     "mlir-capi-sparse-tensor-test",
     "mlir-capi-transform-test",
diff --git a/mlir/test/python/ir/remarks.py b/mlir/test/python/ir/remarks.py
new file mode 100644
index 0000000000000..dcf8e6329c9ba
--- /dev/null
+++ b/mlir/test/python/ir/remarks.py
@@ -0,0 +1,303 @@
+# RUN: %PYTHON %s | FileCheck %s
+
+import gc
+import os
+import tempfile
+from mlir.ir import *
+
+
+def run(f):
+    print("\nTEST:", f.__name__)
+    f()
+    gc.collect()
+    assert Context._get_live_count() == 0
+    return f
+
+
+def emit(loc, kind="passed", name="Unroll", category="Loop", **kwargs):
+    return loc.emit_remark(kind, name, category=category, **kwargs)
+
+
+# CHECK-LABEL: TEST: testNoEngine
+ at run
+def testNoEngine():
+    ctx = Context()
+    with ctx:
+        loc = Location.file("test.mlir", 1, 2)
+        # CHECK: enabled: False
+        print("enabled:", ctx.remarks_enabled)
+        # CHECK: emitted: False
+        print("emitted:", emit(loc))
+        # Finalizing without an engine is a no-op.
+        ctx.finalize_remarks()
+
+
+# CHECK-LABEL: TEST: testCallback
+ at run
+def testCallback():
+    ctx = Context()
+    collected = []
+
+    def callback(remark):
+        collected.append(remark)
+        print(
+            "kind:",
+            remark.kind,
+            "name:",
+            remark.remark_name,
+            "category:",
+            remark.category_name,
+            "full:",
+            remark.full_category_name,
+            "function:",
+            remark.function_name,
+        )
+        print("args:", remark.args)
+        print("message:", remark.message)
+        print("str:", str(remark))
+        print("location:", remark.location)
+        print("id:", remark.remark_id)
+
+    ctx.enable_remarks(all_filter=".*", callback=callback)
+    # CHECK: enabled: True
+    print("enabled:", ctx.remarks_enabled)
+    with ctx:
+        loc = Location.file("test.mlir", 3, 4)
+        # CHECK: kind: RemarkKind.PASSED name: Unroll category: Loop full: Loop:Inner function: main
+        # CHECK: args: [('RemarkId', '1'), ('Remark', 'unrolled by 4'), ('factor', '4')]
+        # CHECK: message: [Passed] Unroll | Category:Loop:Inner | Function=main | Remark="unrolled by 4", RemarkId=1, factor=4
+        # CHECK: str: [Passed] Unroll | Category:Loop:Inner | Function=main | Remark="unrolled by 4", RemarkId=1, factor=4
+        # CHECK: location: loc("test.mlir":3:4)
+        # CHECK: id: 1
+        # CHECK: emitted: True
+        emitted = emit(
+            loc,
+            RemarkKind.PASSED,
+            sub_category="Inner",
+            function_name="main",
+            message="unrolled by 4",
+            args=[("factor", "4")],
+        )
+        print("emitted:", emitted)
+    # A remark is invalid once the callback has returned.
+    try:
+        collected[0].remark_name
+    except ValueError as e:
+        # CHECK: invalid: Remark is invalid (used outside of callback)
+        print("invalid:", e)
+    # CHECK: invalid str: <Invalid Remark>
+    print("invalid str:", str(collected[0]))
+    collected.clear()
+    ctx.finalize_remarks()
+    # CHECK: enabled after finalize: False
+    print("enabled after finalize:", ctx.remarks_enabled)
+
+
+# CHECK-LABEL: TEST: testFilters
+ at run
+def testFilters():
+    ctx = Context()
+    names = []
+    ctx.enable_remarks(
+        all_filter="Loop",
+        passed_filter="Vector",
+        callback=lambda r: names.append(r.remark_name),
+    )
+    with ctx:
+        loc = Location.unknown()
+        # The all-filter admits every kind of the Loop category.
+        # CHECK: loop passed: True
+        print("loop passed:", emit(loc, "passed", "LoopPassed", "Loop"))
+        # CHECK: loop missed: True
+        print("loop missed:", emit(loc, "missed", "LoopMissed", "Loop"))
+        # The passed-filter admits only passed remarks of the Vector category.
+        # CHECK: vector passed: True
+        print("vector passed:", emit(loc, "passed", "VectorPassed", "Vector"))
+        # CHECK: vector missed: False
+        print("vector missed:", emit(loc, "missed", "VectorMissed", "Vector"))
+        # CHECK: other: False
+        print("other:", emit(loc, "analysis", "Other", "Memory"))
+        # Unknown kinds are never emitted.
+        # CHECK: unknown: False
+        print("unknown:", emit(loc, RemarkKind.UNKNOWN, "Unknown", "Loop"))
+    ctx.finalize_remarks()
+    # CHECK: names: ['LoopPassed', 'LoopMissed', 'VectorPassed']
+    print("names:", names)
+
+
+# CHECK-LABEL: TEST: testFinalPolicy
+ at run
+def testFinalPolicy():
+    ctx = Context()
+    names = []
+    ctx.enable_remarks(
+        policy="final", all_filter=".*", callback=lambda r: names.append(r.remark_name)
+    )
+    with ctx:
+        loc = Location.unknown()
+        emit(loc, "passed", "First")
+        emit(loc, "missed", "Second")
+        emit(loc, "failed", "Third")
+        emit(loc, "analysis", "Fourth")
+    # Nothing is delivered until finalize.
+    # CHECK: before finalize: []
+    print("before finalize:", names)
+    ctx.finalize_remarks()
+    # CHECK: after finalize: 4
+    print("after finalize:", len(names))
+    assert sorted(names) == ["First", "Fourth", "Second", "Third"]
+    # The engine can be enabled again after finalize.
+    ctx.enable_remarks(all_filter=".*", callback=lambda r: names.append(r.remark_name))
+    with ctx:
+        emit(Location.unknown(), "passed", "Fifth")
+    # CHECK: re-enabled: Fifth
+    print("re-enabled:", names[-1])
+    ctx.finalize_remarks()
+
+
+# CHECK-LABEL: TEST: testYamlFile
+ at run
+def testYamlFile():
+    ctx = Context()
+    directory = tempfile.mkdtemp()
+    path = os.path.join(directory, "remarks.yaml")
+    ctx.enable_remarks(format="yaml", output_file=path, all_filter=".*")
+    with ctx:
+        loc = Location.file("test.mlir", 5, 6)
+        # CHECK: emitted: True
+        print("emitted:", emit(loc, "missed", "NotUnrolled", "Loop", function_name="f"))
+    ctx.finalize_remarks()
+    with open(path) as f:
+        content = f.read()
+    # CHECK: has file: True
+    print("has file:", os.path.exists(path))
+    # CHECK: has name: True
+    print("has name:", "NotUnrolled" in content)
+    # CHECK: has pass: True
+    print("has pass:", "Loop" in content)
+    os.remove(path)
+    os.rmdir(directory)
+
+
+# CHECK-LABEL: TEST: testEmitFormat
+ at run
+def testEmitFormat():
+    ctx = Context()
+    messages = []
+
+    def handler(d):
+        messages.append((d.severity, str(d.message)))
+        return True
+
+    handler_handle = ctx.attach_diagnostic_handler(handler)
+    ctx.enable_remarks(all_filter=".*")
+    with ctx:
+        emit(
+            Location.unknown(),
+            "analysis",
+            "TripCount",
+            "Loop",
+            message="trip count is 4",
+        )
+    ctx.finalize_remarks()
+    handler_handle.detach()
+    # CHECK: diagnostics: [(DiagnosticSeverity.REMARK, '[Analysis] TripCount | Category:Loop | Remark="trip count is 4", RemarkId=1')]
+    print("diagnostics:", messages)
+
+
+# CHECK-LABEL: TEST: testCallbackAndEmit
+ at run
+def testCallbackAndEmit():
+    ctx = Context()
+    seen = []
+
+    def handler(d):
+        seen.append("diagnostic")
+        return True
+
+    handler_handle = ctx.attach_diagnostic_handler(handler)
+    ctx.enable_remarks(
+        all_filter=".*",
+        callback=lambda r: seen.append("callback"),
+        print_as_emit_remarks=True,
+    )
+    with ctx:
+        emit(Location.unknown())
+    ctx.finalize_remarks()
+    handler_handle.detach()
+    # CHECK: seen: ['callback', 'diagnostic']
+    print("seen:", seen)
+
+
+# CHECK-LABEL: TEST: testErrors
+ at run
+def testErrors():
+    ctx = Context()
+    for kwargs in (
+        dict(policy="sometimes"),
+        dict(format="xml"),
+        dict(format="yaml", callback=lambda r: None),
+        dict(format="yaml", output_file="/nonexistent-dir/remarks.yaml"),
+    ):
+        try:
+            ctx.enable_remarks(all_filter=".*", **kwargs)
+            print("no error (unexpected)")
+        except ValueError as e:
+            print("ValueError:", e)
+    # CHECK: ValueError: unknown remark policy 'sometimes'; expected 'all' or 'final'
+    # CHECK: ValueError: unknown remark format 'xml'; expected 'emit', 'yaml' or 'bitstream'
+    # CHECK: ValueError: a remark callback cannot be combined with format='yaml'; use format='emit'
+    # CHECK: ValueError: failed to enable remarks: cannot write '/nonexistent-dir/remarks.yaml'
+    ctx.enable_remarks(all_filter=".*")
+    try:
+        ctx.enable_remarks(all_filter=".*")
+    except ValueError as e:
+        # CHECK: ValueError: remarks are already enabled on this context; call finalize_remarks() first
+        print("ValueError:", e)
+    with ctx:
+        loc = Location.unknown()
+        try:
+            loc.emit_remark("sideways", "Name")
+        except ValueError as e:
+            # CHECK: ValueError: unknown remark kind 'sideways'; expected 'passed', 'missed', 'failure' or 'analysis'
+            print("ValueError:", e)
+        try:
+            loc.emit_remark(3, "Name")
+        except TypeError as e:
+            # CHECK: TypeError: remark kind must be a RemarkKind or a string
+            print("TypeError:", e)
+    ctx.finalize_remarks()
+
+
+# CHECK-LABEL: TEST: testCallbackException
+ at run
+def testCallbackException():
+    ctx = Context()
+
+    def callback(remark):
+        raise RuntimeError("boom")
+
+    ctx.enable_remarks(all_filter=".*", callback=callback)
+    with ctx:
+        # The exception is printed to stderr and dropped; emission still succeeds.
+        # CHECK: emitted: True
+        print("emitted:", emit(Location.unknown()))
+    ctx.finalize_remarks()
+
+
+# CHECK-LABEL: TEST: testContextDestroyedWithEngine
+ at run
+def testContextDestroyedWithEngine():
+    ctx = Context()
+    names = []
+    ctx.enable_remarks(
+        policy="final", all_filter=".*", callback=lambda r: names.append(r.remark_name)
+    )
+    with ctx:
+        emit(Location.unknown(), "passed", "Pending")
+    # Destroying the context finalizes the engine; a postponed remark cannot
+    # reach Python anymore and is dropped, without crashing.
+    ctx = None
+    gc.collect()
+    # CHECK: dropped: []
+    print("dropped:", names)

>From 6ea2a31b5cfeac72fca9f5acb5071e1abaca2d17 Mon Sep 17 00:00:00 2001
From: Guray Ozen <gozen at nvidia.com>
Date: Tue, 6 Oct 2026 10:22:26 +0200
Subject: [PATCH 2/2] fixes, address issues

---
 mlir/include/mlir-c/Remarks.h              |   7 +-
 mlir/include/mlir/Bindings/Python/IRCore.h |  77 ++--
 mlir/lib/Bindings/Python/IRCore.cpp        | 294 +-------------
 mlir/lib/Bindings/Python/IRRemarks.cpp     | 232 +++++++++++
 mlir/lib/Bindings/Python/MainModule.cpp    |   3 +
 mlir/lib/CAPI/IR/Remarks.cpp               |  49 ++-
 mlir/python/CMakeLists.txt                 |   1 +
 mlir/test/CAPI/remarks.c                   | 141 +++++--
 mlir/test/python/ir/remarks.py             | 426 +++++++++++++--------
 9 files changed, 726 insertions(+), 504 deletions(-)
 create mode 100644 mlir/lib/Bindings/Python/IRRemarks.cpp

diff --git a/mlir/include/mlir-c/Remarks.h b/mlir/include/mlir-c/Remarks.h
index 1bd93ba395af4..f60051d422a96 100644
--- a/mlir/include/mlir-c/Remarks.h
+++ b/mlir/include/mlir-c/Remarks.h
@@ -159,10 +159,9 @@ MLIR_CAPI_EXPORTED MlirStringRef mlirRemarkGetArgValue(MlirRemark remark,
                                                        intptr_t pos);
 
 /// Prints the remark in its textual form, `[Kind] name | Category:... |
-/// Function=... | key=value, ...`, optionally followed by its location.
-MLIR_CAPI_EXPORTED void mlirRemarkPrint(MlirRemark remark, bool printLocation,
-                                        MlirStringCallback callback,
-                                        void *userData);
+/// Function=... | key=value, ...` (without its location).
+MLIR_CAPI_EXPORTED void
+mlirRemarkPrint(MlirRemark remark, MlirStringCallback callback, void *userData);
 
 //===----------------------------------------------------------------------===//
 // Emission.
diff --git a/mlir/include/mlir/Bindings/Python/IRCore.h b/mlir/include/mlir/Bindings/Python/IRCore.h
index 027201a9d95a5..3edf12fbafee1 100644
--- a/mlir/include/mlir/Bindings/Python/IRCore.h
+++ b/mlir/include/mlir/Bindings/Python/IRCore.h
@@ -207,6 +207,26 @@ class MLIR_PYTHON_API_EXPORTED PyThreadPool {
   MlirLlvmThreadPool threadPool;
 };
 
+/// The kinds, emitting policies and file formats of the remark engine
+/// (mlir-c/Remarks.h).
+enum class PyRemarkKind : std::underlying_type_t<MlirRemarkKind> {
+  Unknown = MlirRemarkKindUnknown,
+  Passed = MlirRemarkKindPassed,
+  Missed = MlirRemarkKindMissed,
+  Failure = MlirRemarkKindFailure,
+  Analysis = MlirRemarkKindAnalysis
+};
+
+enum class PyRemarkPolicy : std::underlying_type_t<MlirRemarkPolicy> {
+  All = MlirRemarkPolicyAll,
+  Final = MlirRemarkPolicyFinal
+};
+
+enum class PyRemarkFormat : std::underlying_type_t<MlirRemarkFileFormat> {
+  YAML = MlirRemarkFileFormatYAML,
+  Bitstream = MlirRemarkFileFormatBitstream
+};
+
 /// Wrapper around MlirContext.
 using PyMlirContextRef = PyObjectRef<PyMlirContext>;
 class MLIR_PYTHON_API_EXPORTED PyMlirContext {
@@ -254,26 +274,24 @@ class MLIR_PYTHON_API_EXPORTED PyMlirContext {
   /// registration object (internally a PyDiagnosticHandler).
   nanobind::object attachDiagnosticHandler(nanobind::object callback);
 
-  /// Enables the optimization remark engine of the context. `policy` is
-  /// "all" or "final"; `format` is "emit" (remarks become MLIR remark
-  /// diagnostics, or go to `callback` when one is given), "yaml" or
-  /// "bitstream" (remarks are streamed to `outputFile`). Raises ValueError on
-  /// invalid options or when an engine is already enabled.
-  void enableRemarks(const std::string &policy, const std::string &format,
-                     const std::string &outputFile,
-                     const std::string &allFilter,
+  /// Enables the optimization remark engine of the context, streaming to
+  /// `outputFile`, to `callback`, or (without either) to MLIR remark
+  /// diagnostics. Raises ValueError on an invalid combination, when an engine
+  /// is already enabled or when the file cannot be written.
+  void enableRemarks(PyRemarkPolicy policy,
+                     const std::optional<std::string> &outputFile,
+                     PyRemarkFormat format, const std::string &allFilter,
                      const std::string &passedFilter,
                      const std::string &missedFilter,
                      const std::string &analysisFilter,
                      const std::string &failedFilter, nanobind::object callback,
-                     nanobind::object printAsEmitRemarks);
+                     std::optional<bool> printAsEmitRemarks);
   /// Finalizes and removes the remark engine; no-op when none is enabled.
   void finalizeRemarks();
   /// Whether a remark engine is enabled on the context.
   bool getRemarksEnabled();
-  /// Whether `context` still has a live Python wrapper. False while the
-  /// wrapper is being destroyed, which is when a remark engine finalizing
-  /// with the context must not call back into Python.
+  /// Whether `context` still has a live Python wrapper (false while it is
+  /// being destroyed).
   static bool isLiveContext(MlirContext context);
 
   /// Controls whether error diagnostics should be propagated to diagnostic
@@ -376,14 +394,6 @@ enum class PyDiagnosticSeverity : std::underlying_type_t<
   Remark = MlirDiagnosticRemark
 };
 
-enum class PyRemarkKind : std::underlying_type_t<MlirRemarkKind> {
-  Unknown = MlirRemarkKindUnknown,
-  Passed = MlirRemarkKindPassed,
-  Missed = MlirRemarkKindMissed,
-  Failure = MlirRemarkKindFailure,
-  Analysis = MlirRemarkKindAnalysis
-};
-
 enum class PyWalkResult : std::underlying_type_t<MlirWalkResult> {
   Advance = MlirWalkResultAdvance,
   Interrupt = MlirWalkResultInterrupt,
@@ -446,21 +456,23 @@ class MLIR_PYTHON_API_EXPORTED PyDiagnostic {
 /// an invalidated remark raises ValueError.
 class MLIR_PYTHON_API_EXPORTED PyRemark {
 public:
-  PyRemark(MlirRemark remark) : remark(remark) {}
+  explicit PyRemark(MlirRemark remark) : remark(remark) {}
   void invalidate() { valid = false; }
-  bool isValid() { return valid; }
-  PyRemarkKind getKind();
-  nanobind::str getRemarkName();
-  nanobind::str getCategoryName();
-  nanobind::str getFullCategoryName();
-  nanobind::str getFunctionName();
-  nanobind::typed<nanobind::object, PyLocation> getLocation();
-  uint64_t getId();
-  nanobind::list getArgs();
-  nanobind::str getMessage();
+  bool isValid() const { return valid; }
+  PyRemarkKind getKind() const;
+  nanobind::str getRemarkName() const;
+  nanobind::str getCategoryName() const;
+  nanobind::str getFullCategoryName() const;
+  nanobind::str getFunctionName() const;
+  nanobind::typed<nanobind::object, PyLocation> getLocation() const;
+  uint64_t getId() const;
+  /// The key/value arguments as a list of `(key, value)` tuples.
+  nanobind::list getArgs() const;
+  /// The textual form of the remark, without its location.
+  nanobind::str getMessage() const;
 
 private:
-  void checkValid();
+  void checkValid() const;
   MlirRemark remark;
   bool valid = true;
 };
@@ -2073,6 +2085,7 @@ class MLIR_PYTHON_API_EXPORTED RecursiveMemoryEffects
 
 MLIR_PYTHON_API_EXPORTED MlirValue getUniqueResult(MlirOperation operation);
 MLIR_PYTHON_API_EXPORTED void populateIRCore(nanobind::module_ &m);
+MLIR_PYTHON_API_EXPORTED void populateIRRemarks(nanobind::module_ &m);
 MLIR_PYTHON_API_EXPORTED void populateRoot(nanobind::module_ &m);
 
 /// Helper for creating an @classmethod.
diff --git a/mlir/lib/Bindings/Python/IRCore.cpp b/mlir/lib/Bindings/Python/IRCore.cpp
index 7caddb6156423..bb0a49ba93ce2 100644
--- a/mlir/lib/Bindings/Python/IRCore.cpp
+++ b/mlir/lib/Bindings/Python/IRCore.cpp
@@ -77,7 +77,6 @@ createCustomDialectWrapper(const std::string &dialectNamespace,
 namespace mlir {
 namespace python {
 namespace MLIR_BINDINGS_PYTHON_DOMAIN {
-
 MlirBlock createBlock(
     const nb::typed<nb::sequence, PyType> &pyArgTypes,
     const std::optional<nb::typed<nb::sequence, PyLocation>> &pyArgLocs) {
@@ -715,217 +714,6 @@ void PyThreadContextEntry::popLocation(PyLocation &location) {
 // PyDiagnostic*
 //------------------------------------------------------------------------------
 
-//------------------------------------------------------------------------------
-// PyRemark and the remark engine of a context.
-//------------------------------------------------------------------------------
-
-namespace {
-/// User data of the remark callback streamer: the Python callable and the
-/// context it was installed on.
-struct PyRemarkCallbackData {
-  MlirContext context;
-  nb::object callback;
-};
-} // namespace
-
-static nb::str remarkStr(MlirStringRef ref) {
-  return nb::str(ref.data, ref.length);
-}
-
-static void pyRemarkCallback(MlirRemark remark, void *userData) {
-  auto *data = static_cast<PyRemarkCallbackData *>(userData);
-  // Since this can be called from arbitrary C++ contexts, always get the gil.
-  nb::gil_scoped_acquire gil;
-  // Postponed remarks are reported when the engine is destroyed. When that
-  // happens because the context itself is being destroyed, its Python wrapper
-  // is already gone and the remark cannot be delivered.
-  if (!PyMlirContext::isLiveContext(data->context))
-    return;
-  PyRemark *pyRemark = new PyRemark(remark);
-  nb::object pyRemarkObject = nb::cast(pyRemark, nb::rv_policy::take_ownership);
-  try {
-    data->callback(pyRemarkObject);
-  } catch (std::exception &e) {
-    fprintf(stderr, "MLIR Python remark callback raised exception: %s\n",
-            e.what());
-  }
-  pyRemark->invalidate();
-}
-
-static void pyRemarkCallbackDelete(void *userData) {
-  nb::gil_scoped_acquire gil;
-  delete static_cast<PyRemarkCallbackData *>(userData);
-}
-
-bool PyMlirContext::isLiveContext(MlirContext context) {
-  nb::ft_lock_guard lock(live_contexts_mutex);
-  return getLiveContexts().count(context.ptr) != 0;
-}
-
-void PyMlirContext::enableRemarks(
-    const std::string &policy, const std::string &format,
-    const std::string &outputFile, const std::string &allFilter,
-    const std::string &passedFilter, const std::string &missedFilter,
-    const std::string &analysisFilter, const std::string &failedFilter,
-    nb::object callback, nb::object printAsEmitRemarks) {
-  MlirRemarkPolicy remarkPolicy;
-  if (policy == "all") {
-    remarkPolicy = MlirRemarkPolicyAll;
-  } else if (policy == "final") {
-    remarkPolicy = MlirRemarkPolicyFinal;
-  } else {
-    throw nb::value_error(
-        ("unknown remark policy '" + policy + "'; expected 'all' or 'final'")
-            .c_str());
-  }
-  bool toFile = false;
-  MlirRemarkFileFormat fileFormat = MlirRemarkFileFormatYAML;
-  if (format == "yaml") {
-    toFile = true;
-  } else if (format == "bitstream") {
-    toFile = true;
-    fileFormat = MlirRemarkFileFormatBitstream;
-  } else if (format != "emit") {
-    throw nb::value_error(("unknown remark format '" + format +
-                           "'; expected 'emit', 'yaml' or 'bitstream'")
-                              .c_str());
-  }
-  bool hasCallback = !callback.is_none();
-  if (toFile && hasCallback)
-    throw nb::value_error(
-        ("a remark callback cannot be combined with format='" + format +
-         "'; use format='emit'")
-            .c_str());
-  if (mlirContextHasRemarkEngine(get()))
-    throw nb::value_error("remarks are already enabled on this context; call "
-                          "finalize_remarks() first");
-
-  MlirRemarkCategories categories{
-      toMlirStringRef(allFilter), toMlirStringRef(passedFilter),
-      toMlirStringRef(missedFilter), toMlirStringRef(analysisFilter),
-      toMlirStringRef(failedFilter)};
-  // Without a sink of its own, the engine prints as MLIR remark diagnostics.
-  bool emit = printAsEmitRemarks.is_none() ? (!toFile && !hasCallback)
-                                           : nb::cast<bool>(printAsEmitRemarks);
-
-  MlirLogicalResult result;
-  if (toFile) {
-    std::string path = outputFile;
-    if (path.empty())
-      path = fileFormat == MlirRemarkFileFormatYAML ? "mlir-remarks.yaml"
-                                                    : "mlir-remarks.bitstream";
-    result = mlirContextEnableOptimizationRemarksToFile(
-        get(), categories, remarkPolicy, fileFormat, toMlirStringRef(path),
-        emit);
-    if (mlirLogicalResultIsFailure(result))
-      throw nb::value_error(
-          ("failed to enable remarks: cannot write '" + path + "'").c_str());
-  } else if (hasCallback) {
-    auto *data = new PyRemarkCallbackData{get(), std::move(callback)};
-    result = mlirContextEnableOptimizationRemarksWithCallback(
-        get(), categories, remarkPolicy, pyRemarkCallback, data,
-        pyRemarkCallbackDelete, emit);
-  } else {
-    result = mlirContextEnableOptimizationRemarks(get(), categories,
-                                                  remarkPolicy, emit);
-  }
-  if (mlirLogicalResultIsFailure(result))
-    throw nb::value_error("failed to enable remarks");
-}
-
-void PyMlirContext::finalizeRemarks() {
-  mlirContextFinalizeOptimizationRemarks(get());
-}
-
-bool PyMlirContext::getRemarksEnabled() {
-  return mlirContextHasRemarkEngine(get());
-}
-
-void PyRemark::checkValid() {
-  if (!valid)
-    throw std::invalid_argument("Remark is invalid (used outside of callback)");
-}
-
-PyRemarkKind PyRemark::getKind() {
-  checkValid();
-  return static_cast<PyRemarkKind>(mlirRemarkGetKind(remark));
-}
-
-nb::str PyRemark::getRemarkName() {
-  checkValid();
-  return remarkStr(mlirRemarkGetRemarkName(remark));
-}
-
-nb::str PyRemark::getCategoryName() {
-  checkValid();
-  return remarkStr(mlirRemarkGetCategoryName(remark));
-}
-
-nb::str PyRemark::getFullCategoryName() {
-  checkValid();
-  return remarkStr(mlirRemarkGetFullCategoryName(remark));
-}
-
-nb::str PyRemark::getFunctionName() {
-  checkValid();
-  return remarkStr(mlirRemarkGetFunctionName(remark));
-}
-
-nb::typed<nb::object, PyLocation> PyRemark::getLocation() {
-  checkValid();
-  MlirLocation loc = mlirRemarkGetLocation(remark);
-  MlirContext context = mlirLocationGetContext(loc);
-  return PyLocation(PyMlirContext::forContext(context), loc).maybeDownCast();
-}
-
-uint64_t PyRemark::getId() {
-  checkValid();
-  return mlirRemarkGetId(remark);
-}
-
-nb::list PyRemark::getArgs() {
-  checkValid();
-  nb::list args;
-  intptr_t numArgs = mlirRemarkGetNumArgs(remark);
-  for (intptr_t i = 0; i < numArgs; ++i)
-    args.append(nb::make_tuple(remarkStr(mlirRemarkGetArgKey(remark, i)),
-                               remarkStr(mlirRemarkGetArgValue(remark, i))));
-  return args;
-}
-
-nb::str PyRemark::getMessage() {
-  checkValid();
-  std::string message;
-  auto callback = +[](MlirStringRef ref, void *userData) {
-    static_cast<std::string *>(userData)->append(ref.data, ref.length);
-  };
-  mlirRemarkPrint(remark, /*printLocation=*/false, callback, &message);
-  return nb::str(message.c_str(), message.size());
-}
-
-/// Converts the `kind` argument of `Location.emit_remark`, a RemarkKind or one
-/// of the strings "passed", "missed", "failure"/"failed" and "analysis".
-static MlirRemarkKind parseRemarkKind(nb::handle kind) {
-  if (nb::isinstance<PyRemarkKind>(kind))
-    return static_cast<MlirRemarkKind>(nb::cast<PyRemarkKind>(kind));
-  std::string name;
-  if (nb::try_cast<std::string>(kind, name)) {
-    if (name == "passed")
-      return MlirRemarkKindPassed;
-    if (name == "missed")
-      return MlirRemarkKindMissed;
-    if (name == "failure" || name == "failed")
-      return MlirRemarkKindFailure;
-    if (name == "analysis")
-      return MlirRemarkKindAnalysis;
-    throw nb::value_error(("unknown remark kind '" + name +
-                           "'; expected 'passed', 'missed', 'failure' or "
-                           "'analysis'")
-                              .c_str());
-  }
-  throw nb::type_error("remark kind must be a RemarkKind or a string");
-}
-
 void PyDiagnostic::invalidate() {
   valid = false;
   if (materializedNotes) {
@@ -1144,7 +932,6 @@ PyOperation::~PyOperation() {
 }
 
 namespace {
-
 // Constructs a new object of type T in-place on the Python heap, returning a
 // PyObjectRef to it, loosely analogous to std::make_shared<T>().
 template <typename T, class... Args>
@@ -1156,7 +943,6 @@ PyObjectRef<T> makeObjectRef(Args &&...args) {
   nb::inst_mark_ready(instance);
   return PyObjectRef<T>(ptr, std::move(instance));
 }
-
 } // namespace
 
 PyOperationRef PyOperation::createInstance(PyMlirContextRef contextRef,
@@ -2934,13 +2720,11 @@ void PyDynamicOpTraits::RecursiveMemoryEffects::bind(nb::module_ &m) {
       nb::arg("cls"), nb::arg("op_name"),
       nb::arg("context").none() = nb::none());
 }
-
 } // namespace MLIR_BINDINGS_PYTHON_DOMAIN
 } // namespace python
 } // namespace mlir
 
 namespace {
-
 using namespace mlir::python::MLIR_BINDINGS_PYTHON_DOMAIN;
 
 MlirLocation tracebackToLocation(MlirContext ctx) {
@@ -3095,7 +2879,6 @@ maybeGetTracebackLocation(const std::optional<PyLocation> &location) {
 namespace mlir {
 namespace python {
 namespace MLIR_BINDINGS_PYTHON_DOMAIN {
-
 static std::string formatMLIRError(const MLIRError &e) {
   auto locStr = [](const PyLocation &loc) {
     PyPrintAccumulator accum;
@@ -3538,13 +3321,6 @@ void populateIRCore(nb::module_ &m) {
       .value("NOTE", PyDiagnosticSeverity::Note)
       .value("REMARK", PyDiagnosticSeverity::Remark);
 
-  nb::enum_<PyRemarkKind>(m, "RemarkKind")
-      .value("UNKNOWN", PyRemarkKind::Unknown)
-      .value("PASSED", PyRemarkKind::Passed)
-      .value("MISSED", PyRemarkKind::Missed)
-      .value("FAILURE", PyRemarkKind::Failure)
-      .value("ANALYSIS", PyRemarkKind::Analysis);
-
   nb::enum_<PyWalkOrder>(m, "WalkOrder")
       .value("PRE_ORDER", PyWalkOrder::PreOrder)
       .value("POST_ORDER", PyWalkOrder::PostOrder);
@@ -3620,37 +3396,6 @@ void populateIRCore(nb::module_ &m) {
            "exc_value"_a.none(), "traceback"_a.none(),
            "Exits the diagnostic handler context manager.");
 
-  nb::class_<PyRemark>(m, "Remark")
-      .def_prop_ro("kind", &PyRemark::getKind,
-                   "Returns the kind of the remark.")
-      .def_prop_ro("remark_name", &PyRemark::getRemarkName,
-                   "Returns the name identifying the remark.")
-      .def_prop_ro("category_name", &PyRemark::getCategoryName,
-                   "Returns the category of the remark (what the filters "
-                   "match).")
-      .def_prop_ro("full_category_name", &PyRemark::getFullCategoryName,
-                   "Returns the combined `category:subcategory` name.")
-      .def_prop_ro("function_name", &PyRemark::getFunctionName,
-                   "Returns the name of the function the remark refers to.")
-      .def_prop_ro("location", &PyRemark::getLocation,
-                   "Returns the location associated with the remark.")
-      .def_prop_ro("remark_id", &PyRemark::getId,
-                   "Returns the id of the remark within its engine (0 when "
-                   "unset).")
-      .def_prop_ro("args", &PyRemark::getArgs,
-                   "Returns the key/value arguments as a list of tuples.")
-      .def_prop_ro("message", &PyRemark::getMessage,
-                   "Returns the textual form of the remark, `[Kind] name | "
-                   "Category:... | key=value, ...`.")
-      .def(
-          "__str__",
-          [](PyRemark &self) -> nb::str {
-            if (!self.isValid())
-              return nb::str("<Invalid Remark>");
-            return self.getMessage();
-          },
-          "Returns the remark message as a string.");
-
   // Expose DefaultThreadPool to python
   nb::class_<PyThreadPool>(m, "ThreadPool")
       .def(
@@ -3750,8 +3495,10 @@ void populateIRCore(nb::module_ &m) {
            "callback"_a,
            "Attaches a diagnostic handler that will receive callbacks.")
       .def("enable_remarks", &PyMlirContext::enableRemarks, nb::kw_only(),
-           "policy"_a = "all", "format"_a = "emit", "output_file"_a = "",
-           "all_filter"_a = "", "passed_filter"_a = "", "missed_filter"_a = "",
+           "policy"_a = PyRemarkPolicy::All,
+           "output_file"_a.none() = nb::none(),
+           "format"_a = PyRemarkFormat::YAML, "all_filter"_a = "",
+           "passed_filter"_a = "", "missed_filter"_a = "",
            "analysis_filter"_a = "", "failed_filter"_a = "",
            "callback"_a.none() = nb::none(),
            "print_as_emit_remarks"_a.none() = nb::none(),
@@ -3763,29 +3510,31 @@ void populateIRCore(nb::module_ &m) {
             the engine: `all_filter` applies to every kind, the other filters to
             one kind each; a kind without a matching filter is not reported.
 
+            The sink is selected by the arguments: `output_file` streams the
+            remarks to a file in `format`, `callback` delivers them to Python,
+            and without either they are emitted as MLIR remark diagnostics.
+
             Args:
-              policy: "all" reports remarks as they are emitted; "final"
-                postpones them until `finalize_remarks()` and reports the final
-                set, grouping related remarks under their parents.
-              format: "emit" delivers remarks as MLIR remark diagnostics, or to
-                `callback` when one is given; "yaml" and "bitstream" stream
-                them to `output_file`, written on `finalize_remarks()`.
-              output_file: The output path for the file formats; defaults to
-                "mlir-remarks.yaml" / "mlir-remarks.bitstream".
+              policy: `RemarkPolicy.ALL` reports remarks as they are emitted;
+                `RemarkPolicy.FINAL` postpones them until `finalize_remarks()`
+                and reports the final set, grouping related remarks under
+                their parents.
+              output_file: Path of the remark file, written on
+                `finalize_remarks()`.
+              format: The serialization format of `output_file`.
               all_filter: Category regex applied to every remark kind.
               passed_filter: Category regex for passed remarks.
               missed_filter: Category regex for missed remarks.
               analysis_filter: Category regex for analysis remarks.
               failed_filter: Category regex for failed remarks.
               callback: A callable receiving one `Remark` per reported remark;
-                the remark is only valid during the call. Requires
-                format="emit".
+                the remark is only valid during the call.
               print_as_emit_remarks: Also emit every remark as an MLIR remark
-                diagnostic; defaults to True only for format="emit" without a
+                diagnostic; defaults to True only without an output file or
                 callback.
 
             Raises:
-              ValueError: On an unknown policy, format or kind combination,
+              ValueError: When both an output file and a callback are given,
                 when remarks are already enabled, or when the output file
                 cannot be written.)")
       .def("finalize_remarks", &PyMlirContext::finalizeRemarks,
@@ -4154,12 +3903,12 @@ void populateIRCore(nb::module_ &m) {
               message: The error message to emit.)")
       .def(
           "emit_remark",
-          [](PyLocation &self, nb::object kind, const std::string &remarkName,
+          [](PyLocation &self, PyRemarkKind kind, const std::string &remarkName,
              const std::string &category, const std::string &subCategory,
              const std::string &functionName, const std::string &message,
              const std::vector<std::pair<std::string, std::string>> &args)
               -> bool {
-            MlirRemarkKind remarkKind = parseRemarkKind(kind);
+            auto remarkKind = static_cast<MlirRemarkKind>(kind);
             std::vector<MlirStringRef> keys, values;
             keys.reserve(args.size());
             values.reserve(args.size());
@@ -4181,8 +3930,7 @@ void populateIRCore(nb::module_ &m) {
             engine of the context (see `Context.enable_remarks`).
 
             Args:
-              kind: A `RemarkKind`, or one of "passed", "missed", "failure"
-                and "analysis".
+              kind: The `RemarkKind` of the remark.
               remark_name: The name identifying the remark.
               category: The category the filters are matched against.
               sub_category: An optional sub-category.
diff --git a/mlir/lib/Bindings/Python/IRRemarks.cpp b/mlir/lib/Bindings/Python/IRRemarks.cpp
new file mode 100644
index 0000000000000..a870ef48ec672
--- /dev/null
+++ b/mlir/lib/Bindings/Python/IRRemarks.cpp
@@ -0,0 +1,232 @@
+//===- IRRemarks.cpp - Remark engine bindings -----------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir-c/Remarks.h"
+#include "mlir/Bindings/Python/IRCore.h"
+
+#include <cstdio>
+#include <optional>
+#include <string>
+
+namespace nb = nanobind;
+using namespace nb::literals;
+
+namespace mlir {
+namespace python {
+namespace MLIR_BINDINGS_PYTHON_DOMAIN {
+namespace {
+/// User data of the remark callback installed by `Context.enable_remarks`.
+struct PyRemarkCallbackData {
+  MlirContext context;
+  nb::object callback;
+};
+} // namespace
+
+static nb::str remarkStr(MlirStringRef ref) {
+  return nb::str(ref.data, ref.length);
+}
+
+/// Forwards a reported remark to the Python callable in `userData`; the
+/// PyRemark is invalidated once the callable returns, like a PyDiagnostic.
+static void pyRemarkCallback(MlirRemark remark, void *userData) {
+  auto *data = static_cast<PyRemarkCallbackData *>(userData);
+  // Since this can be called from arbitrary C++ contexts, always get the gil.
+  nb::gil_scoped_acquire gil;
+  // A remark postponed until the context's destruction cannot be delivered:
+  // the Python wrapper of the context is already gone.
+  if (!PyMlirContext::isLiveContext(data->context))
+    return;
+  PyRemark *pyRemark = new PyRemark(remark);
+  nb::object pyRemarkObject = nb::cast(pyRemark, nb::rv_policy::take_ownership);
+  try {
+    data->callback(pyRemarkObject);
+  } catch (std::exception &e) {
+    fprintf(stderr, "MLIR Python remark callback raised exception: %s\n",
+            e.what());
+  }
+  pyRemark->invalidate();
+}
+
+static void pyRemarkCallbackDelete(void *userData) {
+  nb::gil_scoped_acquire gil;
+  delete static_cast<PyRemarkCallbackData *>(userData);
+}
+
+bool PyMlirContext::isLiveContext(MlirContext context) {
+  nb::ft_lock_guard lock(live_contexts_mutex);
+  return getLiveContexts().count(context.ptr) != 0;
+}
+
+void PyMlirContext::enableRemarks(
+    PyRemarkPolicy policy, const std::optional<std::string> &outputFile,
+    PyRemarkFormat format, const std::string &allFilter,
+    const std::string &passedFilter, const std::string &missedFilter,
+    const std::string &analysisFilter, const std::string &failedFilter,
+    nb::object callback, std::optional<bool> printAsEmitRemarks) {
+  bool hasCallback = !callback.is_none();
+  if (outputFile && hasCallback)
+    throw nb::value_error(
+        "a remark callback cannot be combined with an output file");
+  if (mlirContextHasRemarkEngine(get())) {
+    throw nb::value_error("remarks are already enabled on this context; call "
+                          "finalize_remarks() first");
+  }
+
+  MlirRemarkCategories categories{
+      toMlirStringRef(allFilter), toMlirStringRef(passedFilter),
+      toMlirStringRef(missedFilter), toMlirStringRef(analysisFilter),
+      toMlirStringRef(failedFilter)};
+  auto remarkPolicy = static_cast<MlirRemarkPolicy>(policy);
+  // Without a sink of its own, the engine prints as MLIR remark diagnostics.
+  bool printAsEmit =
+      printAsEmitRemarks.value_or(!outputFile.has_value() && !hasCallback);
+
+  MlirLogicalResult result;
+  if (outputFile) {
+    result = mlirContextEnableOptimizationRemarksToFile(
+        get(), categories, remarkPolicy,
+        static_cast<MlirRemarkFileFormat>(format), toMlirStringRef(*outputFile),
+        printAsEmit);
+    if (mlirLogicalResultIsFailure(result))
+      throw nb::value_error(
+          ("failed to enable remarks: cannot write '" + *outputFile + "'")
+              .c_str());
+  } else if (hasCallback) {
+    auto *data = new PyRemarkCallbackData{get(), std::move(callback)};
+    result = mlirContextEnableOptimizationRemarksWithCallback(
+        get(), categories, remarkPolicy, pyRemarkCallback, data,
+        pyRemarkCallbackDelete, printAsEmit);
+  } else {
+    result = mlirContextEnableOptimizationRemarks(get(), categories,
+                                                  remarkPolicy, printAsEmit);
+  }
+  if (mlirLogicalResultIsFailure(result))
+    throw nb::value_error("failed to enable remarks");
+}
+
+void PyMlirContext::finalizeRemarks() {
+  mlirContextFinalizeOptimizationRemarks(get());
+}
+
+bool PyMlirContext::getRemarksEnabled() {
+  return mlirContextHasRemarkEngine(get());
+}
+
+void PyRemark::checkValid() const {
+  if (!valid)
+    throw std::invalid_argument("Remark is invalid (used outside of callback)");
+}
+
+PyRemarkKind PyRemark::getKind() const {
+  checkValid();
+  return static_cast<PyRemarkKind>(mlirRemarkGetKind(remark));
+}
+
+nb::str PyRemark::getRemarkName() const {
+  checkValid();
+  return remarkStr(mlirRemarkGetRemarkName(remark));
+}
+
+nb::str PyRemark::getCategoryName() const {
+  checkValid();
+  return remarkStr(mlirRemarkGetCategoryName(remark));
+}
+
+nb::str PyRemark::getFullCategoryName() const {
+  checkValid();
+  return remarkStr(mlirRemarkGetFullCategoryName(remark));
+}
+
+nb::str PyRemark::getFunctionName() const {
+  checkValid();
+  return remarkStr(mlirRemarkGetFunctionName(remark));
+}
+
+nb::typed<nb::object, PyLocation> PyRemark::getLocation() const {
+  checkValid();
+  MlirLocation loc = mlirRemarkGetLocation(remark);
+  MlirContext context = mlirLocationGetContext(loc);
+  return PyLocation(PyMlirContext::forContext(context), loc).maybeDownCast();
+}
+
+uint64_t PyRemark::getId() const {
+  checkValid();
+  return mlirRemarkGetId(remark);
+}
+
+nb::list PyRemark::getArgs() const {
+  checkValid();
+  nb::list args;
+  intptr_t numArgs = mlirRemarkGetNumArgs(remark);
+  for (intptr_t i = 0; i < numArgs; ++i) {
+    args.append(nb::make_tuple(remarkStr(mlirRemarkGetArgKey(remark, i)),
+                               remarkStr(mlirRemarkGetArgValue(remark, i))));
+  }
+  return args;
+}
+
+nb::str PyRemark::getMessage() const {
+  checkValid();
+  std::string message;
+  auto callback = +[](MlirStringRef ref, void *userData) {
+    static_cast<std::string *>(userData)->append(ref.data, ref.length);
+  };
+  mlirRemarkPrint(remark, callback, &message);
+  return nb::str(message.c_str(), message.size());
+}
+
+void populateIRRemarks(nb::module_ &m) {
+  nb::enum_<PyRemarkKind>(m, "RemarkKind")
+      .value("UNKNOWN", PyRemarkKind::Unknown)
+      .value("PASSED", PyRemarkKind::Passed)
+      .value("MISSED", PyRemarkKind::Missed)
+      .value("FAILURE", PyRemarkKind::Failure)
+      .value("ANALYSIS", PyRemarkKind::Analysis);
+
+  nb::enum_<PyRemarkPolicy>(m, "RemarkPolicy")
+      .value("ALL", PyRemarkPolicy::All)
+      .value("FINAL", PyRemarkPolicy::Final);
+
+  nb::enum_<PyRemarkFormat>(m, "RemarkFormat")
+      .value("YAML", PyRemarkFormat::YAML)
+      .value("BITSTREAM", PyRemarkFormat::Bitstream);
+
+  nb::class_<PyRemark>(m, "Remark")
+      .def_prop_ro("kind", &PyRemark::getKind,
+                   "Returns the kind of the remark.")
+      .def_prop_ro("remark_name", &PyRemark::getRemarkName,
+                   "Returns the name identifying the remark.")
+      .def_prop_ro("category_name", &PyRemark::getCategoryName,
+                   "Returns the category of the remark (what the filters "
+                   "match).")
+      .def_prop_ro("full_category_name", &PyRemark::getFullCategoryName,
+                   "Returns the combined `category:subcategory` name.")
+      .def_prop_ro("function_name", &PyRemark::getFunctionName,
+                   "Returns the name of the function the remark refers to.")
+      .def_prop_ro("location", &PyRemark::getLocation,
+                   "Returns the location associated with the remark.")
+      .def_prop_ro("remark_id", &PyRemark::getId,
+                   "Returns the id of the remark within its engine (0 when "
+                   "unset).")
+      .def_prop_ro("args", &PyRemark::getArgs,
+                   "Returns the key/value arguments as a list of tuples.")
+      .def_prop_ro("message", &PyRemark::getMessage,
+                   "Returns the textual form of the remark, `[Kind] name | "
+                   "Category:... | key=value, ...`.")
+      .def(
+          "__str__",
+          [](PyRemark &self) -> nb::str {
+            if (!self.isValid())
+              return nb::str("<Invalid Remark>");
+            return self.getMessage();
+          },
+          "Returns the remark message as a string.");
+}
+} // namespace MLIR_BINDINGS_PYTHON_DOMAIN
+} // namespace python
+} // namespace mlir
diff --git a/mlir/lib/Bindings/Python/MainModule.cpp b/mlir/lib/Bindings/Python/MainModule.cpp
index 93ce34b2e89f5..f8f6a223c2e46 100644
--- a/mlir/lib/Bindings/Python/MainModule.cpp
+++ b/mlir/lib/Bindings/Python/MainModule.cpp
@@ -25,6 +25,7 @@ void populateIRAttributes(nb::module_ &m);
 void populateIRInterfaces(nb::module_ &m);
 void populateIRTypes(nb::module_ &m);
 void populateIRCore(nb::module_ &m);
+void populateIRRemarks(nb::module_ &m);
 void populateRoot(nb::module_ &m);
 } // namespace MLIR_BINDINGS_PYTHON_DOMAIN
 } // namespace python
@@ -41,6 +42,8 @@ NB_MODULE(_mlir, m) {
   populateRoot(m);
   // Define and populate IR submodule.
   auto irModule = m.def_submodule("ir", "MLIR IR Bindings");
+  // The remark enums are default arguments of the Context bindings.
+  populateIRRemarks(irModule);
   populateIRCore(irModule);
   populateIRAffine(irModule);
   populateIRAttributes(irModule);
diff --git a/mlir/lib/CAPI/IR/Remarks.cpp b/mlir/lib/CAPI/IR/Remarks.cpp
index 5a54e5b38895c..a05a9cbe74ca8 100644
--- a/mlir/lib/CAPI/IR/Remarks.cpp
+++ b/mlir/lib/CAPI/IR/Remarks.cpp
@@ -15,6 +15,10 @@
 #include "mlir/IR/Remarks.h"
 #include "mlir/Remark/RemarkStreamer.h"
 #include "llvm/Remarks/RemarkFormat.h"
+#include "llvm/Support/ErrorHandling.h"
+
+#include <cassert>
+#include <memory>
 
 using namespace mlir;
 using mlir::remark::detail::Remark;
@@ -48,9 +52,8 @@ class CallbackRemarkStreamer : public remark::detail::MLIRRemarkStreamerBase {
 };
 } // namespace
 
-/// Like mlir-opt, every filter is handed to the engine as a (possibly empty)
-/// string: the engine then reports a kind when its own filter or `all`
-/// matches, and reports nothing for a kind whose filters are both empty.
+/// Every filter reaches the engine as a (possibly empty) string, as with
+/// mlir-opt's options: a kind is reported when its own filter or `all` matches.
 static remark::RemarkCategories unwrap(MlirRemarkCategories categories) {
   return remark::RemarkCategories{
       unwrap(categories.all).str(), unwrap(categories.passed).str(),
@@ -69,6 +72,16 @@ createPolicy(MlirRemarkPolicy policy) {
   llvm_unreachable("unknown remark policy");
 }
 
+static llvm::remarks::Format unwrap(MlirRemarkFileFormat format) {
+  switch (format) {
+  case MlirRemarkFileFormatYAML:
+    return llvm::remarks::Format::YAML;
+  case MlirRemarkFileFormatBitstream:
+    return llvm::remarks::Format::Bitstream;
+  }
+  llvm_unreachable("unknown remark file format");
+}
+
 static MlirRemarkKind wrap(remark::RemarkKind kind) {
   switch (kind) {
   case remark::RemarkKind::RemarkUnknown:
@@ -107,12 +120,9 @@ MlirLogicalResult mlirContextEnableOptimizationRemarksToFile(
   MLIRContext *ctx = unwrap(context);
   if (ctx->getRemarkEngine())
     return mlirLogicalResultFailure();
-  llvm::remarks::Format llvmFormat = format == MlirRemarkFileFormatBitstream
-                                         ? llvm::remarks::Format::Bitstream
-                                         : llvm::remarks::Format::YAML;
   return wrap(remark::enableOptimizationRemarksWithLLVMStreamer(
-      *ctx, unwrap(path), llvmFormat, createPolicy(policy), unwrap(categories),
-      printAsEmitRemarks));
+      *ctx, unwrap(path), unwrap(format), createPolicy(policy),
+      unwrap(categories), printAsEmitRemarks));
 }
 
 MlirLogicalResult mlirContextEnableOptimizationRemarksWithCallback(
@@ -122,6 +132,7 @@ MlirLogicalResult mlirContextEnableOptimizationRemarksWithCallback(
   assert(callback && "unexpected null remark callback");
   MLIRContext *ctx = unwrap(context);
   if (ctx->getRemarkEngine()) {
+    // The caller handed over `userData`; release it as on success.
     if (deleteUserData)
       deleteUserData(userData);
     return mlirLogicalResultFailure();
@@ -134,8 +145,8 @@ MlirLogicalResult mlirContextEnableOptimizationRemarksWithCallback(
 }
 
 void mlirContextFinalizeOptimizationRemarks(MlirContext context) {
-  // The engine destructor finalizes the policy (reporting postponed remarks)
-  // and then the streamer (writing the file / releasing the user data).
+  // The engine destructor reports the postponed remarks, then destroys the
+  // streamer.
   unwrap(context)->setRemarkEngine(nullptr);
 }
 
@@ -180,17 +191,23 @@ intptr_t mlirRemarkGetNumArgs(MlirRemark remark) {
 }
 
 MlirStringRef mlirRemarkGetArgKey(MlirRemark remark, intptr_t pos) {
-  return wrap(llvm::StringRef(unwrap(remark)->getArgs()[pos].key));
+  ArrayRef<Remark::Arg> args = unwrap(remark)->getArgs();
+  assert(pos >= 0 && static_cast<size_t>(pos) < args.size() &&
+         "remark argument index out of range");
+  return wrap(llvm::StringRef(args[pos].key));
 }
 
 MlirStringRef mlirRemarkGetArgValue(MlirRemark remark, intptr_t pos) {
-  return wrap(llvm::StringRef(unwrap(remark)->getArgs()[pos].val));
+  ArrayRef<Remark::Arg> args = unwrap(remark)->getArgs();
+  assert(pos >= 0 && static_cast<size_t>(pos) < args.size() &&
+         "remark argument index out of range");
+  return wrap(llvm::StringRef(args[pos].val));
 }
 
-void mlirRemarkPrint(MlirRemark remark, bool printLocation,
-                     MlirStringCallback callback, void *userData) {
+void mlirRemarkPrint(MlirRemark remark, MlirStringCallback callback,
+                     void *userData) {
   detail::CallbackOstream stream(callback, userData);
-  unwrap(remark)->print(stream, printLocation);
+  unwrap(remark)->print(stream);
 }
 
 //===----------------------------------------------------------------------===//
@@ -202,6 +219,8 @@ bool mlirEmitOptimizationRemark(
     MlirStringRef categoryName, MlirStringRef subCategoryName,
     MlirStringRef functionName, MlirStringRef message, intptr_t numArgs,
     const MlirStringRef *argKeys, const MlirStringRef *argValues) {
+  assert((numArgs == 0 || (argKeys && argValues)) &&
+         "remark arguments require both keys and values");
   Location loc = unwrap(location);
   remark::RemarkOpts opts = remark::RemarkOpts::name(unwrap(remarkName))
                                 .category(unwrap(categoryName))
diff --git a/mlir/python/CMakeLists.txt b/mlir/python/CMakeLists.txt
index aa0adea66d2fe..64554f0bac14a 100644
--- a/mlir/python/CMakeLists.txt
+++ b/mlir/python/CMakeLists.txt
@@ -838,6 +838,7 @@ declare_mlir_python_extension(MLIRPythonExtension.MLIRPythonSupport
     Globals.cpp
     IRAttributes.cpp
     IRCore.cpp
+    IRRemarks.cpp
     IRTypes.cpp
 )
 
diff --git a/mlir/test/CAPI/remarks.c b/mlir/test/CAPI/remarks.c
index 1b74ac624c0f2..0e8d753c1d3b4 100644
--- a/mlir/test/CAPI/remarks.c
+++ b/mlir/test/CAPI/remarks.c
@@ -37,12 +37,11 @@ static MlirRemarkCategories allCategories(const char *filter) {
   return cats;
 }
 
-/// The callback streamer: prints every field of the remark it receives and
-/// counts the deliveries in userData.
+/// Prints every field of the remark and counts the deliveries in userData.
 static void remarkCallback(MlirRemark remark, void *userData) {
   int *count = (int *)userData;
   ++*count;
-  const char *kind = "?";
+  const char *kind = "unknown";
   switch (mlirRemarkGetKind(remark)) {
   case MlirRemarkKindPassed:
     kind = "passed";
@@ -57,7 +56,6 @@ static void remarkCallback(MlirRemark remark, void *userData) {
     kind = "analysis";
     break;
   case MlirRemarkKindUnknown:
-    kind = "unknown";
     break;
   }
   MlirStringRef name = mlirRemarkGetRemarkName(remark);
@@ -78,7 +76,7 @@ static void remarkCallback(MlirRemark remark, void *userData) {
             (int)value.length, value.data);
   }
   fprintf(stderr, "  print: ");
-  mlirRemarkPrint(remark, /*printLocation=*/true, printToStderr, NULL);
+  mlirRemarkPrint(remark, printToStderr, NULL);
   fprintf(stderr, "\n  location: ");
   mlirLocationPrint(mlirRemarkGetLocation(remark), printToStderr, NULL);
   fprintf(stderr, "\n");
@@ -88,6 +86,11 @@ static void deleteUserData(void *userData) {
   fprintf(stderr, "deleteUserData called (count=%d)\n", *(int *)userData);
 }
 
+static void countRemark(MlirRemark remark, void *userData) {
+  (void)remark;
+  ++*(int *)userData;
+}
+
 static bool emit(MlirLocation loc, MlirRemarkKind kind, const char *name,
                  const char *category, const char *message) {
   MlirStringRef keys[1] = {str("factor")};
@@ -103,7 +106,6 @@ static void testCallbackStreamer(void) {
   MlirContext ctx = mlirContextCreate();
   MlirLocation loc = mlirLocationFileLineColGet(ctx, str("remarks.c"), 7, 3);
 
-  // Without an engine nothing is emitted.
   // CHECK: no engine: enabled=0 emitted=0
   bool emitted = emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "dropped");
   fprintf(stderr, "no engine: enabled=%d emitted=%d\n",
@@ -117,25 +119,18 @@ static void testCallbackStreamer(void) {
   fprintf(stderr, "enabled=%d ok=%d\n", mlirContextHasRemarkEngine(ctx),
           mlirLogicalResultIsSuccess(res));
 
-  // Enabling twice fails and leaves the first engine in place.
-  // CHECK: second enable ok=0
-  res = mlirContextEnableOptimizationRemarks(ctx, allCategories(".*"),
-                                             MlirRemarkPolicyAll, true);
-  fprintf(stderr, "second enable ok=%d\n", mlirLogicalResultIsSuccess(res));
-
   // clang-format off
   // CHECK: remark #1 kind=passed name=Unroll category=Loop full=Loop:inner function=main id=1
   // CHECK:   arg RemarkId=1
   // CHECK:   arg Remark=unrolled
   // CHECK:   arg factor=4
-  // CHECK:   print: [Passed] Unroll | Category:Loop:inner | Function=main |  @"remarks.c":7:3{{ ?}}Remark=unrolled, RemarkId=1, factor=4
+  // CHECK:   print: [Passed] Unroll | Category:Loop:inner | Function=main | Remark=unrolled, RemarkId=1, factor=4
   // CHECK:   location: loc("remarks.c":7:3)
   // CHECK: emitted=1
   // clang-format on
   emitted = emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "unrolled");
   fprintf(stderr, "emitted=%d\n", emitted);
 
-  // A category the filter does not match is dropped before the streamer.
   // CHECK-NOT: name=Vectorize
   // CHECK: count after filtered emit=1
   emit(loc, MlirRemarkKindMissed, "Vectorize", "Vector", "not matched");
@@ -148,7 +143,6 @@ static void testCallbackStreamer(void) {
   emit(loc, MlirRemarkKindFailure, "Fuse", "Loop", "no");
   emit(loc, MlirRemarkKindAnalysis, "TripCount", "Loop", "4");
 
-  // Finalize drops the engine and releases the user data.
   // CHECK: deleteUserData called (count=4)
   // CHECK: after finalize: enabled=0 count=4
   mlirContextFinalizeOptimizationRemarks(ctx);
@@ -195,9 +189,9 @@ static MlirLogicalResult diagnosticHandler(MlirDiagnostic diagnostic,
   return mlirLogicalResultSuccess();
 }
 
-// CHECK-LABEL: @testEmitRemarkForm
-static void testEmitRemarkForm(void) {
-  fprintf(stderr, "@testEmitRemarkForm\n");
+// CHECK-LABEL: @testEmitAsDiagnostics
+static void testEmitAsDiagnostics(void) {
+  fprintf(stderr, "@testEmitAsDiagnostics\n");
   MlirContext ctx = mlirContextCreate();
   MlirLocation loc = mlirLocationUnknownGet(ctx);
   MlirDiagnosticHandlerID id =
@@ -220,15 +214,16 @@ static void testFileStreamer(void) {
   fprintf(stderr, "@testFileStreamer\n");
   MlirContext ctx = mlirContextCreate();
   MlirLocation loc = mlirLocationUnknownGet(ctx);
-  char path[] = "mlir-capi-remarks-test.yaml";
+
+  char yamlPath[] = "mlir-capi-remarks-test.yaml";
   MlirLogicalResult res = mlirContextEnableOptimizationRemarksToFile(
       ctx, allCategories(".*"), MlirRemarkPolicyAll, MlirRemarkFileFormatYAML,
-      str(path), false);
-  // CHECK: file enable ok=1
-  fprintf(stderr, "file enable ok=%d\n", mlirLogicalResultIsSuccess(res));
+      str(yamlPath), /*printAsEmitRemarks=*/false);
+  // CHECK: yaml enable ok=1
+  fprintf(stderr, "yaml enable ok=%d\n", mlirLogicalResultIsSuccess(res));
   emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "to file");
   mlirContextFinalizeOptimizationRemarks(ctx);
-  FILE *f = fopen(path, "r");
+  FILE *f = fopen(yamlPath, "r");
   bool found = false;
   if (f) {
     char line[256];
@@ -236,17 +231,113 @@ static void testFileStreamer(void) {
       if (strstr(line, "Unroll"))
         found = true;
     fclose(f);
-    remove(path);
+    remove(yamlPath);
   }
   // CHECK: yaml has remark=1
   fprintf(stderr, "yaml has remark=%d\n", found);
+
+  // The bitstream file starts with the LLVM remark bitstream magic.
+  char bitstreamPath[] = "mlir-capi-remarks-test.bitstream";
+  res = mlirContextEnableOptimizationRemarksToFile(
+      ctx, allCategories(".*"), MlirRemarkPolicyFinal,
+      MlirRemarkFileFormatBitstream, str(bitstreamPath),
+      /*printAsEmitRemarks=*/false);
+  // CHECK: bitstream enable ok=1
+  fprintf(stderr, "bitstream enable ok=%d\n", mlirLogicalResultIsSuccess(res));
+  emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "to file");
+  mlirContextFinalizeOptimizationRemarks(ctx);
+  char magic[5] = {0};
+  f = fopen(bitstreamPath, "rb");
+  if (f) {
+    if (fread(magic, 1, 4, f) != 4)
+      magic[0] = '\0';
+    fclose(f);
+    remove(bitstreamPath);
+  }
+  // CHECK: bitstream magic=RMRK
+  fprintf(stderr, "bitstream magic=%s\n", magic);
+  mlirContextDestroy(ctx);
+}
+
+// CHECK-LABEL: @testAccessorDefaults
+static void testAccessorDefaults(void) {
+  fprintf(stderr, "@testAccessorDefaults\n");
+  MlirContext ctx = mlirContextCreate();
+  MlirLocation loc = mlirLocationUnknownGet(ctx);
+  int count = 0;
+  MlirLogicalResult res = mlirContextEnableOptimizationRemarksWithCallback(
+      ctx, allCategories(".*"), MlirRemarkPolicyAll, remarkCallback, &count,
+      NULL, false);
+  assert(mlirLogicalResultIsSuccess(res));
+  // Unset names fall back to placeholders, the only argument is the id, and
+  // null argument arrays are accepted when numArgs is 0.
+  // clang-format off
+  // CHECK: remark #1 kind=passed name=<unknown remark name> category= full= function=<unknown function> id=1
+  // CHECK:   arg RemarkId=1
+  // CHECK:   print: [Passed]  | RemarkId=1
+  // CHECK:   location: loc(unknown)
+  // CHECK: emitted=1
+  // clang-format on
+  bool emitted =
+      mlirEmitOptimizationRemark(loc, MlirRemarkKindPassed, str(""), str(""),
+                                 str(""), str(""), str(""), 0, NULL, NULL);
+  fprintf(stderr, "emitted=%d\n", emitted);
+  // A sub-category alone is the full category name.
+  // clang-format off
+  // CHECK: remark #2 kind=missed name=Name category= full=Sub function=<unknown function> id=2
+  // CHECK:   print: [Missed] Name | Category:Sub | RemarkId=2
+  // clang-format on
+  mlirEmitOptimizationRemark(loc, MlirRemarkKindMissed, str("Name"), str(""),
+                             str("Sub"), str(""), str(""), 0, NULL, NULL);
+  // CHECK: unknown emitted=0
+  emitted = emit(loc, MlirRemarkKindUnknown, "Name", "Loop", "");
+  fprintf(stderr, "unknown emitted=%d\n", emitted);
+  mlirContextFinalizeOptimizationRemarks(ctx);
+  mlirContextDestroy(ctx);
+}
+
+// CHECK-LABEL: @testEnableWhileEnabled
+static void testEnableWhileEnabled(void) {
+  fprintf(stderr, "@testEnableWhileEnabled\n");
+  MlirContext ctx = mlirContextCreate();
+  MlirLocation loc = mlirLocationUnknownGet(ctx);
+  int first = 0, second = 0;
+  MlirLogicalResult res = mlirContextEnableOptimizationRemarksWithCallback(
+      ctx, allCategories(".*"), MlirRemarkPolicyAll, countRemark, &first,
+      deleteUserData, false);
+  // CHECK: first ok=1
+  fprintf(stderr, "first ok=%d\n", mlirLogicalResultIsSuccess(res));
+  // Every enable variant fails while an engine is active; the callback
+  // variant releases the user data it was handed right away.
+  // CHECK: deleteUserData called (count=0)
+  // CHECK: callback ok=0 file ok=0 plain ok=0
+  res = mlirContextEnableOptimizationRemarksWithCallback(
+      ctx, allCategories(".*"), MlirRemarkPolicyAll, countRemark, &second,
+      deleteUserData, false);
+  MlirLogicalResult fileRes = mlirContextEnableOptimizationRemarksToFile(
+      ctx, allCategories(".*"), MlirRemarkPolicyAll, MlirRemarkFileFormatYAML,
+      str("mlir-capi-remarks-test-unused.yaml"), false);
+  MlirLogicalResult plainRes = mlirContextEnableOptimizationRemarks(
+      ctx, allCategories(".*"), MlirRemarkPolicyAll, false);
+  fprintf(stderr, "callback ok=%d file ok=%d plain ok=%d\n",
+          mlirLogicalResultIsSuccess(res), mlirLogicalResultIsSuccess(fileRes),
+          mlirLogicalResultIsSuccess(plainRes));
+  emit(loc, MlirRemarkKindPassed, "Unroll", "Loop", "");
+  // CHECK: first=1 second=0
+  fprintf(stderr, "first=%d second=%d\n", first, second);
+  // Destroying the context with a live engine releases its user data.
+  // CHECK: deleteUserData called (count=1)
+  // CHECK: destroyed
   mlirContextDestroy(ctx);
+  fprintf(stderr, "destroyed\n");
 }
 
 int main(void) {
   testCallbackStreamer();
   testFinalPolicy();
-  testEmitRemarkForm();
+  testEmitAsDiagnostics();
   testFileStreamer();
+  testAccessorDefaults();
+  testEnableWhileEnabled();
   return 0;
 }
diff --git a/mlir/test/python/ir/remarks.py b/mlir/test/python/ir/remarks.py
index dcf8e6329c9ba..49fbe4ebb6767 100644
--- a/mlir/test/python/ir/remarks.py
+++ b/mlir/test/python/ir/remarks.py
@@ -14,32 +14,55 @@ def run(f):
     return f
 
 
-def emit(loc, kind="passed", name="Unroll", category="Loop", **kwargs):
+def emit(loc, kind=RemarkKind.PASSED, name="Unroll", category="Loop", **kwargs):
     return loc.emit_remark(kind, name, category=category, **kwargs)
 
 
+def collect(into, attribute="remark_name"):
+    """Returns a remark callback appending `attribute` of each remark to `into`."""
+    return lambda remark: into.append(getattr(remark, attribute))
+
+
 # CHECK-LABEL: TEST: testNoEngine
 @run
 def testNoEngine():
     ctx = Context()
-    with ctx:
-        loc = Location.file("test.mlir", 1, 2)
-        # CHECK: enabled: False
-        print("enabled:", ctx.remarks_enabled)
-        # CHECK: emitted: False
-        print("emitted:", emit(loc))
-        # Finalizing without an engine is a no-op.
-        ctx.finalize_remarks()
+    loc = Location.file("test.mlir", 1, 2, context=ctx)
+    # CHECK: enabled: False
+    print("enabled:", ctx.remarks_enabled)
+    # CHECK: emitted: False
+    print("emitted:", emit(loc))
+    # Finalizing without an engine is a no-op.
+    ctx.finalize_remarks()
+    # CHECK: still disabled: False
+    print("still disabled:", ctx.remarks_enabled)
+
+
+# CHECK-LABEL: TEST: testEnableFinalize
+ at run
+def testEnableFinalize():
+    ctx = Context()
+    states = [ctx.remarks_enabled]
+    ctx.enable_remarks(all_filter=".*")
+    states.append(ctx.remarks_enabled)
+    ctx.finalize_remarks()
+    states.append(ctx.remarks_enabled)
+    ctx.finalize_remarks()
+    states.append(ctx.remarks_enabled)
+    ctx.enable_remarks(policy=RemarkPolicy.FINAL, print_as_emit_remarks=False)
+    states.append(ctx.remarks_enabled)
+    ctx.finalize_remarks()
+    states.append(ctx.remarks_enabled)
+    # CHECK: states: [False, True, False, False, True, False]
+    print("states:", states)
 
 
 # CHECK-LABEL: TEST: testCallback
 @run
 def testCallback():
     ctx = Context()
-    collected = []
 
     def callback(remark):
-        collected.append(remark)
         print(
             "kind:",
             remark.kind,
@@ -51,75 +74,108 @@ def callback(remark):
             remark.full_category_name,
             "function:",
             remark.function_name,
+            "id:",
+            remark.remark_id,
         )
         print("args:", remark.args)
         print("message:", remark.message)
         print("str:", str(remark))
-        print("location:", remark.location)
-        print("id:", remark.remark_id)
+        print(
+            "location:",
+            remark.location,
+            type(remark.location).__name__,
+            remark.location.context is ctx,
+        )
 
     ctx.enable_remarks(all_filter=".*", callback=callback)
     # CHECK: enabled: True
     print("enabled:", ctx.remarks_enabled)
-    with ctx:
-        loc = Location.file("test.mlir", 3, 4)
-        # CHECK: kind: RemarkKind.PASSED name: Unroll category: Loop full: Loop:Inner function: main
-        # CHECK: args: [('RemarkId', '1'), ('Remark', 'unrolled by 4'), ('factor', '4')]
-        # CHECK: message: [Passed] Unroll | Category:Loop:Inner | Function=main | Remark="unrolled by 4", RemarkId=1, factor=4
-        # CHECK: str: [Passed] Unroll | Category:Loop:Inner | Function=main | Remark="unrolled by 4", RemarkId=1, factor=4
-        # CHECK: location: loc("test.mlir":3:4)
-        # CHECK: id: 1
-        # CHECK: emitted: True
-        emitted = emit(
-            loc,
-            RemarkKind.PASSED,
-            sub_category="Inner",
-            function_name="main",
-            message="unrolled by 4",
-            args=[("factor", "4")],
-        )
-        print("emitted:", emitted)
-    # A remark is invalid once the callback has returned.
-    try:
-        collected[0].remark_name
-    except ValueError as e:
-        # CHECK: invalid: Remark is invalid (used outside of callback)
-        print("invalid:", e)
-    # CHECK: invalid str: <Invalid Remark>
-    print("invalid str:", str(collected[0]))
-    collected.clear()
+    loc = Location.file("test.mlir", 3, 4, context=ctx)
+    # The arguments keep their insertion order (the engine's id, the message,
+    # the explicit pairs); the message sorts them by key.
+    # CHECK: kind: RemarkKind.PASSED name: Unroll category: Loop full: Loop:Inner function: main id: 1
+    # CHECK: args: [('RemarkId', '1'), ('Remark', 'unrolled by 4'), ('factor', '4')]
+    # CHECK: message: [Passed] Unroll | Category:Loop:Inner | Function=main | Remark="unrolled by 4", RemarkId=1, factor=4
+    # CHECK: str: [Passed] Unroll | Category:Loop:Inner | Function=main | Remark="unrolled by 4", RemarkId=1, factor=4
+    # CHECK: location: loc("test.mlir":3:4) FileLineColLoc True
+    # CHECK: emitted: True
+    emitted = emit(
+        loc,
+        sub_category="Inner",
+        function_name="main",
+        message="unrolled by 4",
+        args=[("factor", "4")],
+    )
+    print("emitted:", emitted)
+    # Unset names fall back to placeholders; the only argument is the id.
+    # CHECK: kind: RemarkKind.MISSED name: <unknown remark name> category:  full:  function: <unknown function> id: 2
+    # CHECK: args: [('RemarkId', '2')]
+    # CHECK: message: [Missed]  | RemarkId=2
+    Location.unknown(context=ctx).emit_remark(RemarkKind.MISSED, "")
     ctx.finalize_remarks()
     # CHECK: enabled after finalize: False
     print("enabled after finalize:", ctx.remarks_enabled)
 
 
+# CHECK-LABEL: TEST: testRemarkInvalidatedAfterCallback
+ at run
+def testRemarkInvalidatedAfterCallback():
+    ctx = Context()
+    kept = []
+    ctx.enable_remarks(all_filter=".*", callback=kept.append)
+    emit(Location.unknown(context=ctx))
+    ctx.finalize_remarks()
+    remark = kept[0]
+    for attribute in (
+        "kind",
+        "remark_name",
+        "category_name",
+        "full_category_name",
+        "function_name",
+        "location",
+        "remark_id",
+        "args",
+        "message",
+    ):
+        try:
+            getattr(remark, attribute)
+            print(attribute, "-> no error (unexpected)")
+        except ValueError as e:
+            print(attribute, "->", e)
+    # CHECK: kind -> Remark is invalid (used outside of callback)
+    # CHECK: remark_name -> Remark is invalid (used outside of callback)
+    # CHECK: category_name -> Remark is invalid (used outside of callback)
+    # CHECK: full_category_name -> Remark is invalid (used outside of callback)
+    # CHECK: function_name -> Remark is invalid (used outside of callback)
+    # CHECK: location -> Remark is invalid (used outside of callback)
+    # CHECK: remark_id -> Remark is invalid (used outside of callback)
+    # CHECK: args -> Remark is invalid (used outside of callback)
+    # CHECK: message -> Remark is invalid (used outside of callback)
+    # CHECK: str: <Invalid Remark>
+    print("str:", str(remark))
+
+
 # CHECK-LABEL: TEST: testFilters
 @run
 def testFilters():
     ctx = Context()
     names = []
     ctx.enable_remarks(
-        all_filter="Loop",
-        passed_filter="Vector",
-        callback=lambda r: names.append(r.remark_name),
+        all_filter="Loop", passed_filter="Vector", callback=collect(names)
     )
-    with ctx:
-        loc = Location.unknown()
-        # The all-filter admits every kind of the Loop category.
-        # CHECK: loop passed: True
-        print("loop passed:", emit(loc, "passed", "LoopPassed", "Loop"))
-        # CHECK: loop missed: True
-        print("loop missed:", emit(loc, "missed", "LoopMissed", "Loop"))
-        # The passed-filter admits only passed remarks of the Vector category.
-        # CHECK: vector passed: True
-        print("vector passed:", emit(loc, "passed", "VectorPassed", "Vector"))
-        # CHECK: vector missed: False
-        print("vector missed:", emit(loc, "missed", "VectorMissed", "Vector"))
-        # CHECK: other: False
-        print("other:", emit(loc, "analysis", "Other", "Memory"))
-        # Unknown kinds are never emitted.
-        # CHECK: unknown: False
-        print("unknown:", emit(loc, RemarkKind.UNKNOWN, "Unknown", "Loop"))
+    loc = Location.unknown(context=ctx)
+    # CHECK: loop passed: True
+    print("loop passed:", emit(loc, RemarkKind.PASSED, "LoopPassed", "Loop"))
+    # CHECK: loop missed: True
+    print("loop missed:", emit(loc, RemarkKind.MISSED, "LoopMissed", "Loop"))
+    # CHECK: vector passed: True
+    print("vector passed:", emit(loc, RemarkKind.PASSED, "VectorPassed", "Vector"))
+    # CHECK: vector missed: False
+    print("vector missed:", emit(loc, RemarkKind.MISSED, "VectorMissed", "Vector"))
+    # CHECK: other: False
+    print("other:", emit(loc, RemarkKind.ANALYSIS, "Other", "Memory"))
+    # CHECK: unknown: False
+    print("unknown:", emit(loc, RemarkKind.UNKNOWN, "Unknown", "Loop"))
     ctx.finalize_remarks()
     # CHECK: names: ['LoopPassed', 'LoopMissed', 'VectorPassed']
     print("names:", names)
@@ -131,28 +187,18 @@ def testFinalPolicy():
     ctx = Context()
     names = []
     ctx.enable_remarks(
-        policy="final", all_filter=".*", callback=lambda r: names.append(r.remark_name)
+        policy=RemarkPolicy.FINAL, all_filter=".*", callback=collect(names)
     )
-    with ctx:
-        loc = Location.unknown()
-        emit(loc, "passed", "First")
-        emit(loc, "missed", "Second")
-        emit(loc, "failed", "Third")
-        emit(loc, "analysis", "Fourth")
-    # Nothing is delivered until finalize.
+    loc = Location.unknown(context=ctx)
+    emit(loc, RemarkKind.PASSED, "First")
+    emit(loc, RemarkKind.MISSED, "Second")
+    emit(loc, RemarkKind.FAILURE, "Third")
+    emit(loc, RemarkKind.ANALYSIS, "Fourth")
     # CHECK: before finalize: []
     print("before finalize:", names)
     ctx.finalize_remarks()
-    # CHECK: after finalize: 4
-    print("after finalize:", len(names))
-    assert sorted(names) == ["First", "Fourth", "Second", "Third"]
-    # The engine can be enabled again after finalize.
-    ctx.enable_remarks(all_filter=".*", callback=lambda r: names.append(r.remark_name))
-    with ctx:
-        emit(Location.unknown(), "passed", "Fifth")
-    # CHECK: re-enabled: Fifth
-    print("re-enabled:", names[-1])
-    ctx.finalize_remarks()
+    # CHECK: after finalize: ['First', 'Fourth', 'Second', 'Third']
+    print("after finalize:", sorted(names))
 
 
 # CHECK-LABEL: TEST: testYamlFile
@@ -161,111 +207,153 @@ def testYamlFile():
     ctx = Context()
     directory = tempfile.mkdtemp()
     path = os.path.join(directory, "remarks.yaml")
-    ctx.enable_remarks(format="yaml", output_file=path, all_filter=".*")
-    with ctx:
-        loc = Location.file("test.mlir", 5, 6)
-        # CHECK: emitted: True
-        print("emitted:", emit(loc, "missed", "NotUnrolled", "Loop", function_name="f"))
+    ctx.enable_remarks(output_file=path, all_filter=".*")
+    loc = Location.file("test.mlir", 5, 6, context=ctx)
+    # CHECK: emitted: True
+    print(
+        "emitted:",
+        emit(
+            loc,
+            RemarkKind.MISSED,
+            "NotUnrolled",
+            sub_category="Inner",
+            function_name="f",
+            message="trip count too small",
+            args=[("tripCount", "4")],
+        ),
+    )
     ctx.finalize_remarks()
     with open(path) as f:
-        content = f.read()
-    # CHECK: has file: True
-    print("has file:", os.path.exists(path))
-    # CHECK: has name: True
-    print("has name:", "NotUnrolled" in content)
-    # CHECK: has pass: True
-    print("has pass:", "Loop" in content)
+        content = " ".join(f.read().split())
     os.remove(path)
     os.rmdir(directory)
+    for fragment in (
+        "--- !Missed",
+        "Pass: 'Loop:Inner'",
+        "Name: NotUnrolled",
+        "DebugLoc: { File: test.mlir, Line: 5, Column: 6 }",
+        "Function: f",
+        "- Remark: trip count too small",
+        "- tripCount: '4'",
+    ):
+        print(fragment, "->", fragment in content)
+    # CHECK: --- !Missed -> True
+    # CHECK: Pass: 'Loop:Inner' -> True
+    # CHECK: Name: NotUnrolled -> True
+    # CHECK: DebugLoc: { File: test.mlir, Line: 5, Column: 6 } -> True
+    # CHECK: Function: f -> True
+    # CHECK: - Remark: trip count too small -> True
+    # CHECK: - tripCount: '4' -> True
 
 
-# CHECK-LABEL: TEST: testEmitFormat
+# CHECK-LABEL: TEST: testBitstreamFile
 @run
-def testEmitFormat():
+def testBitstreamFile():
     ctx = Context()
-    messages = []
-
-    def handler(d):
-        messages.append((d.severity, str(d.message)))
-        return True
-
-    handler_handle = ctx.attach_diagnostic_handler(handler)
-    ctx.enable_remarks(all_filter=".*")
-    with ctx:
-        emit(
-            Location.unknown(),
-            "analysis",
-            "TripCount",
-            "Loop",
-            message="trip count is 4",
-        )
+    directory = tempfile.mkdtemp()
+    path = os.path.join(directory, "remarks.bitstream")
+    ctx.enable_remarks(
+        policy=RemarkPolicy.FINAL,
+        output_file=path,
+        format=RemarkFormat.BITSTREAM,
+        all_filter=".*",
+    )
+    # CHECK: emitted: True
+    print("emitted:", emit(Location.unknown(context=ctx)))
     ctx.finalize_remarks()
-    handler_handle.detach()
-    # CHECK: diagnostics: [(DiagnosticSeverity.REMARK, '[Analysis] TripCount | Category:Loop | Remark="trip count is 4", RemarkId=1')]
-    print("diagnostics:", messages)
+    # The file holds an LLVM remark bitstream once finalized.
+    with open(path, "rb") as f:
+        magic = f.read(4)
+    os.remove(path)
+    os.rmdir(directory)
+    # CHECK: magic: b'RMRK'
+    print("magic:", magic)
 
 
-# CHECK-LABEL: TEST: testCallbackAndEmit
+# CHECK-LABEL: TEST: testEmitAsDiagnostics
 @run
-def testCallbackAndEmit():
+def testEmitAsDiagnostics():
     ctx = Context()
     seen = []
 
     def handler(d):
-        seen.append("diagnostic")
+        seen.append((d.severity, str(d.message)))
         return True
 
-    handler_handle = ctx.attach_diagnostic_handler(handler)
+    handle = ctx.attach_diagnostic_handler(handler)
+    loc = Location.unknown(context=ctx)
+    # Without a sink of its own, the engine emits MLIR remark diagnostics.
+    ctx.enable_remarks(all_filter=".*")
+    emit(loc, RemarkKind.ANALYSIS, "TripCount", message="trip count is 4")
+    ctx.finalize_remarks()
+    # CHECK: diagnostics: [(DiagnosticSeverity.REMARK, '[Analysis] TripCount | Category:Loop | Remark="trip count is 4", RemarkId=1')]
+    print("diagnostics:", seen)
+    seen.clear()
+    ctx.enable_remarks(all_filter=".*", print_as_emit_remarks=False)
+    # CHECK: silent emitted: True []
+    print("silent emitted:", emit(loc), seen)
+    ctx.finalize_remarks()
+    # A callback is silent by default; combined with the diagnostics it runs first.
     ctx.enable_remarks(
         all_filter=".*",
         callback=lambda r: seen.append("callback"),
         print_as_emit_remarks=True,
     )
-    with ctx:
-        emit(Location.unknown())
+    emit(loc)
     ctx.finalize_remarks()
-    handler_handle.detach()
-    # CHECK: seen: ['callback', 'diagnostic']
-    print("seen:", seen)
+    handle.detach()
+    # CHECK: both: ['callback', (DiagnosticSeverity.REMARK, '[Passed] Unroll | Category:Loop | RemarkId=1')]
+    print("both:", seen)
 
 
 # CHECK-LABEL: TEST: testErrors
 @run
 def testErrors():
     ctx = Context()
-    for kwargs in (
-        dict(policy="sometimes"),
-        dict(format="xml"),
-        dict(format="yaml", callback=lambda r: None),
-        dict(format="yaml", output_file="/nonexistent-dir/remarks.yaml"),
-    ):
+    try:
+        ctx.enable_remarks(output_file="remarks.yaml", callback=lambda r: None)
+        print("no error (unexpected)")
+    except ValueError as e:
+        # CHECK: ValueError: a remark callback cannot be combined with an output file
+        print("ValueError:", e)
+    try:
+        ctx.enable_remarks(output_file="/nonexistent-dir/remarks.yaml")
+        print("no error (unexpected)")
+    except ValueError as e:
+        # CHECK: ValueError: failed to enable remarks: cannot write '/nonexistent-dir/remarks.yaml'
+        print("ValueError:", e)
+    # CHECK: enabled after errors: False
+    print("enabled after errors:", ctx.remarks_enabled)
+    # The options are keyword-only and typed.
+    for kwargs in (dict(policy="final"), dict(format="yaml")):
         try:
-            ctx.enable_remarks(all_filter=".*", **kwargs)
+            ctx.enable_remarks(**kwargs)
             print("no error (unexpected)")
-        except ValueError as e:
-            print("ValueError:", e)
-    # CHECK: ValueError: unknown remark policy 'sometimes'; expected 'all' or 'final'
-    # CHECK: ValueError: unknown remark format 'xml'; expected 'emit', 'yaml' or 'bitstream'
-    # CHECK: ValueError: a remark callback cannot be combined with format='yaml'; use format='emit'
-    # CHECK: ValueError: failed to enable remarks: cannot write '/nonexistent-dir/remarks.yaml'
+        except TypeError as e:
+            print("TypeError:", str(e).splitlines()[0])
+    # CHECK-COUNT-2: TypeError: enable_remarks(): incompatible function arguments
     ctx.enable_remarks(all_filter=".*")
     try:
         ctx.enable_remarks(all_filter=".*")
+        print("no error (unexpected)")
     except ValueError as e:
         # CHECK: ValueError: remarks are already enabled on this context; call finalize_remarks() first
         print("ValueError:", e)
-    with ctx:
-        loc = Location.unknown()
-        try:
-            loc.emit_remark("sideways", "Name")
-        except ValueError as e:
-            # CHECK: ValueError: unknown remark kind 'sideways'; expected 'passed', 'missed', 'failure' or 'analysis'
-            print("ValueError:", e)
-        try:
-            loc.emit_remark(3, "Name")
-        except TypeError as e:
-            # CHECK: TypeError: remark kind must be a RemarkKind or a string
-            print("TypeError:", e)
+    # CHECK: still enabled: True
+    print("still enabled:", ctx.remarks_enabled)
+    loc = Location.unknown(context=ctx)
+    try:
+        loc.emit_remark("passed", "Name")
+        print("no error (unexpected)")
+    except TypeError as e:
+        # CHECK: TypeError: emit_remark(): incompatible function arguments
+        print("TypeError:", str(e).splitlines()[0])
+    try:
+        Remark()
+        print("no error (unexpected)")
+    except TypeError as e:
+        # CHECK: TypeError: {{.*}}Remark: no constructor defined!
+        print("TypeError:", e)
     ctx.finalize_remarks()
 
 
@@ -273,30 +361,58 @@ def testErrors():
 @run
 def testCallbackException():
     ctx = Context()
+    names = []
 
     def callback(remark):
-        raise RuntimeError("boom")
+        names.append(remark.remark_name)
+        if remark.remark_name == "Boom":
+            raise RuntimeError("boom")
 
     ctx.enable_remarks(all_filter=".*", callback=callback)
-    with ctx:
-        # The exception is printed to stderr and dropped; emission still succeeds.
-        # CHECK: emitted: True
-        print("emitted:", emit(Location.unknown()))
+    loc = Location.unknown(context=ctx)
+    # The exception is reported on stderr and dropped.
+    # CHECK: boom emitted: True enabled: True
+    print("boom emitted:", emit(loc, name="Boom"), "enabled:", ctx.remarks_enabled)
+    # CHECK: after emitted: True
+    print("after emitted:", emit(loc, name="After"))
+    # CHECK: names: ['Boom', 'After']
+    print("names:", names)
     ctx.finalize_remarks()
 
 
+# CHECK-LABEL: TEST: testMultipleContexts
+ at run
+def testMultipleContexts():
+    ctx1, ctx2 = Context(), Context()
+    seen1, seen2 = [], []
+    ctx1.enable_remarks(all_filter=".*", callback=collect(seen1))
+    ctx2.enable_remarks(passed_filter="Loop", callback=collect(seen2))
+    loc1 = Location.unknown(context=ctx1)
+    loc2 = Location.unknown(context=ctx2)
+    emit(loc1, RemarkKind.PASSED, "A")
+    emit(loc2, RemarkKind.PASSED, "B")
+    emit(loc2, RemarkKind.MISSED, "C")
+    ctx1.finalize_remarks()
+    # CHECK: enabled: False True
+    print("enabled:", ctx1.remarks_enabled, ctx2.remarks_enabled)
+    emit(loc2, RemarkKind.PASSED, "D")
+    ctx2.finalize_remarks()
+    # CHECK: seen1: ['A']
+    # CHECK: seen2: ['B', 'D']
+    print("seen1:", seen1)
+    print("seen2:", seen2)
+
+
 # CHECK-LABEL: TEST: testContextDestroyedWithEngine
 @run
 def testContextDestroyedWithEngine():
     ctx = Context()
     names = []
     ctx.enable_remarks(
-        policy="final", all_filter=".*", callback=lambda r: names.append(r.remark_name)
+        policy=RemarkPolicy.FINAL, all_filter=".*", callback=collect(names)
     )
-    with ctx:
-        emit(Location.unknown(), "passed", "Pending")
-    # Destroying the context finalizes the engine; a postponed remark cannot
-    # reach Python anymore and is dropped, without crashing.
+    emit(Location.unknown(context=ctx), RemarkKind.PASSED, "Pending")
+    # A remark postponed until the context's destruction is dropped.
     ctx = None
     gc.collect()
     # CHECK: dropped: []



More information about the Mlir-commits mailing list