Add simple_io_sample custom compiler plugin. (#12745)

Includes updates to the plugin mechanism:

* Enables building plugins at a specific path (in or out of repo)
* Re-organizes initialization sequence to separate dialect registration
from activation
* Wires into iree-opt
* Adds a mechanism for extending pipelines with additional passes and
wires that into the preprocessing pipeline

Progress on #12520
diff --git a/build_tools/cmake/iree_compiler_plugin.cmake b/build_tools/cmake/iree_compiler_plugin.cmake
index 2a56be9..5d492a8 100644
--- a/build_tools/cmake/iree_compiler_plugin.cmake
+++ b/build_tools/cmake/iree_compiler_plugin.cmake
@@ -5,6 +5,7 @@
 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 set(IREE_COMPILER_PLUGINS "" CACHE STRING "List of named in-tree plugins (under compiler/plugins) to statically compile")
+set(IREE_COMPILER_PLUGIN_PATHS "" CACHE STRING "Paths to external compiler plugins")
 
 # Ids of all plugins that have been included in the configure step. This
 # may include plugins that we do not statically link but we do build.
@@ -60,6 +61,17 @@
     set(_plugin_src_dir "${IREE_SOURCE_DIR}/compiler/plugins/${_plugin_id}")
     iree_compiler_add_plugin("${_plugin_id}" "${_plugin_src_dir}")
   endforeach()
+  unset(_plugin_id)
+  unset(_plugin_src_dir)
+
+  # Process out of tree plugins.
+  foreach(_plugin_src_dir ${IREE_COMPILER_PLUGIN_PATHS})
+    # TODO: Support some path mangling to allow overriding the plugin id
+    # if it is not literally the last path component.
+    cmake_path(ABSOLUTE_PATH _plugin_src_dir BASE_DIRECTORY "${IREE_SOURCE_DIR}" NORMALIZE)
+    cmake_path(GET _plugin_src_dir FILENAME _plugin_id)
+    iree_compiler_add_plugin("${_plugin_id}" "${_plugin_src_dir}")
+  endforeach()
 endfunction()
 
 # iree_compiler_add_plugin(src bin)
@@ -81,6 +93,17 @@
   set(IREE_COMPILER_IN_ADD_PLUGIN "${plugin_id}")
   set_property(GLOBAL APPEND PROPERTY IREE_COMPILER_INCLUDED_PLUGIN_IDS "${plugin_id}")
   set(_binary_dir "${IREE_BINARY_DIR}/compiler/plugins/${_plugin_id}")
+
+  # Force enable BUILD_SHARED_LIBS for the compiler if instructed.
+  set(_IREE_ORIG_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
+  if(IREE_COMPILER_BUILD_SHARED_LIBS)
+    set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE)
+  endif()
+
   add_subdirectory("${plugin_src_dir}" "${_binary_dir}")
+
+  # Reset BUILD_SHARED_LIBS.
+  set(BUILD_SHARED_LIBS ${_IREE_ORIG_BUILD_SHARED_LIBS} CACHE BOOL "" FORCE)
+
   unset(IREE_COMPILER_IN_ADD_PLUGIN)
-endfunction()
\ No newline at end of file
+endfunction()
diff --git a/compiler/src/iree/compiler/API/Internal/BUILD.bazel b/compiler/src/iree/compiler/API/Internal/BUILD.bazel
index 8a2addf..bf8294f 100644
--- a/compiler/src/iree/compiler/API/Internal/BUILD.bazel
+++ b/compiler/src/iree/compiler/API/Internal/BUILD.bazel
@@ -74,11 +74,14 @@
     ],
     deps = [
         "//compiler/bindings/c:headers",
+        "//compiler/src/iree/compiler/PluginAPI:PluginManager",
         "//compiler/src/iree/compiler/Tools:init_passes_and_dialects",
         "//compiler/src/iree/compiler/Tools:init_targets",
         "@llvm-project//llvm:Support",
+        "@llvm-project//mlir:Debug",
         "@llvm-project//mlir:IR",
         "@llvm-project//mlir:MlirOptLib",
+        "@llvm-project//mlir:Pass",
         "@llvm-project//mlir:Support",
     ],
 )
diff --git a/compiler/src/iree/compiler/API/Internal/CMakeLists.txt b/compiler/src/iree/compiler/API/Internal/CMakeLists.txt
index 80322a8..a9e3130 100644
--- a/compiler/src/iree/compiler/API/Internal/CMakeLists.txt
+++ b/compiler/src/iree/compiler/API/Internal/CMakeLists.txt
@@ -71,9 +71,12 @@
     "IREEOptToolEntryPoint.cpp"
   DEPS
     LLVMSupport
+    MLIRDebug
     MLIRIR
     MLIROptLib
+    MLIRPass
     MLIRSupport
+    iree::compiler::PluginAPI::PluginManager
     iree::compiler::Tools::init_passes_and_dialects
     iree::compiler::Tools::init_targets
     iree::compiler::bindings::c::headers
diff --git a/compiler/src/iree/compiler/API/Internal/Embed.cpp b/compiler/src/iree/compiler/API/Internal/Embed.cpp
index 3a43fea..aabd131 100644
--- a/compiler/src/iree/compiler/API/Internal/Embed.cpp
+++ b/compiler/src/iree/compiler/API/Internal/Embed.cpp
@@ -138,7 +138,7 @@
   }
   pluginManager.globalInitialize();
   pluginManager.registerPasses();
-  pluginManager.registerDialects(registry);
+  pluginManager.registerGlobalDialects(registry);
 }
 
 void GlobalInit::registerCommandLineOptions() {
@@ -198,7 +198,14 @@
   LogicalResult activatePluginsOnce() {
     if (!pluginsActivated) {
       pluginsActivated = true;
-      pluginActivationStatus = pluginSession.activatePlugins(&context);
+      if (failed(pluginSession.initializePlugins())) {
+        pluginActivationStatus = failure();
+      } else {
+        DialectRegistry registry;
+        pluginSession.registerDialects(registry);
+        context.appendDialectRegistry(registry);
+        pluginActivationStatus = pluginSession.activatePlugins(&context);
+      }
     }
     return pluginActivationStatus;
   }
@@ -503,17 +510,9 @@
   Error *outputVMCSource(Output &output);
   Error *outputHALExecutable(Output &output);
 
-  IREEVMPipelineHooks &getHooks() {
-    static IREEVMPipelineHooks hooks = {
-        // buildConstEvalPassPipelineCallback =
-        [](OpPassManager &pm) {
-          pm.addPass(ConstEval::createJitGlobalsPass());
-        }};
-    return hooks;
-  }
-
   Session &session;
   PassManager passManager;
+  IREEVMPipelineHooks pipelineHooks;
 
   // Diagnostic handlers are instantiated upon parsing the source (when we
   // have the SrcMgr) and held for the duration of the invocation. Each will
@@ -543,6 +542,16 @@
     mlir::applyDefaultTimingPassManagerCLOptions(passManager);
   }
   passManager.addInstrumentation(std::make_unique<PassTracing>());
+
+  // Since the jitter invokes much of the top-level compiler recursively,
+  // it must be injected at the top-level here vs in the pass pipeline
+  // (or else the circular dependency cannot be resolved).
+  pipelineHooks.buildConstEvalPassPipelineCallback = [](OpPassManager &pm) {
+    pm.addPass(ConstEval::createJitGlobalsPass());
+  };
+  // The PluginSession implements PipelineExtensions and delegates it to
+  // activated plugins.
+  pipelineHooks.pipelineExtensions = &session.pluginSession;
 }
 
 bool Invocation::parseSource(Source &source) {
@@ -610,7 +619,7 @@
           session.bindingOptions, session.inputOptions,
           session.preprocessingOptions, session.highLevelOptimizationOptions,
           session.schedulingOptions, session.halTargetOptions,
-          session.vmTargetOptions, getHooks(), passManager, *compileToPhase);
+          session.vmTargetOptions, pipelineHooks, passManager, *compileToPhase);
       break;
     }
     case IREE_COMPILER_PIPELINE_HAL_EXECUTABLE: {
diff --git a/compiler/src/iree/compiler/API/Internal/IREEOptToolEntryPoint.cpp b/compiler/src/iree/compiler/API/Internal/IREEOptToolEntryPoint.cpp
index adf0a35..6fcd2cf 100644
--- a/compiler/src/iree/compiler/API/Internal/IREEOptToolEntryPoint.cpp
+++ b/compiler/src/iree/compiler/API/Internal/IREEOptToolEntryPoint.cpp
@@ -8,20 +8,114 @@
 //
 // Based on mlir-opt but registers the passes and dialects we care about.
 
+#include "iree/compiler/PluginAPI/PluginManager.h"
 #include "iree/compiler/Tools/init_dialects.h"
 #include "iree/compiler/Tools/init_passes.h"
 #include "iree/compiler/Tools/init_targets.h"
 #include "iree/compiler/tool_entry_points_api.h"
 #include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/Process.h"
+#include "llvm/Support/SourceMgr.h"
+#include "llvm/Support/ToolOutputFile.h"
+#include "mlir/Debug/Counter.h"
+#include "mlir/IR/AsmState.h"
 #include "mlir/IR/Dialect.h"
+#include "mlir/IR/MLIRContext.h"
+#include "mlir/Pass/PassManager.h"
+#include "mlir/Support/FileUtilities.h"
 #include "mlir/Support/LogicalResult.h"
 #include "mlir/Tools/mlir-opt/MlirOptMain.h"
 
+using namespace llvm;
+using namespace mlir;
+
+static LogicalResult ireeOptMainFromCL(int argc, char **argv,
+                                       llvm::StringRef toolName,
+                                       DialectRegistry &registry) {
+  static cl::opt<std::string> inputFilename(
+      cl::Positional, cl::desc("<input file>"), cl::init("-"));
+
+  static cl::opt<std::string> outputFilename("o", cl::desc("Output filename"),
+                                             cl::value_desc("filename"),
+                                             cl::init("-"));
+
+  InitLLVM y(argc, argv);
+
+  // Register any command line options.
+  MlirOptMainConfig::registerCLOptions();
+  registerAsmPrinterCLOptions();
+  registerMLIRContextCLOptions();
+  registerPassManagerCLOptions();
+  registerDefaultTimingManagerCLOptions();
+  tracing::DebugCounter::registerCLOptions();
+  auto &pluginManagerOptions =
+      mlir::iree_compiler::PluginManagerOptions::FromFlags::get();
+
+  // Build the list of dialects as a header for the --help message.
+  std::string helpHeader = (toolName + "\nAvailable Dialects: ").str();
+  {
+    llvm::raw_string_ostream os(helpHeader);
+    interleaveComma(registry.getDialectNames(), os,
+                    [&](auto name) { os << name; });
+  }
+
+  // We support a limited form of the PluginManager, allowing it to perform
+  // global initialization and dialect registration.
+  mlir::iree_compiler::PluginManager pluginManager;
+  if (!pluginManager.loadAvailablePlugins()) {
+    llvm::errs() << "error: Failed to initialize IREE compiler plugins\n";
+    return failure();
+  }
+  pluginManager.initializeCLI();
+
+  // Parse pass names in main to ensure static initialization completed.
+  cl::ParseCommandLineOptions(argc, argv, helpHeader);
+  MlirOptMainConfig config = MlirOptMainConfig::createFromCLOptions();
+
+  // The local binder is meant for overriding session-level options, but for
+  // tools like this it is unused.
+  auto localBinder = mlir::iree_compiler::OptionsBinder::local();
+  pluginManager.globalInitialize();
+  pluginManager.registerPasses();
+  pluginManager.registerGlobalDialects(registry);
+  mlir::iree_compiler::PluginManagerSession pluginSession(
+      pluginManager, localBinder, pluginManagerOptions);
+  if (failed(pluginSession.initializePlugins())) return failure();
+  pluginSession.registerDialects(registry);
+
+  // When reading from stdin and the input is a tty, it is often a user mistake
+  // and the process "appears to be stuck". Print a message to let the user know
+  // about it!
+  if (inputFilename == "-" &&
+      sys::Process::FileDescriptorIsDisplayed(fileno(stdin)))
+    llvm::errs() << "(processing input from stdin now, hit ctrl-c/ctrl-d to "
+                    "interrupt)\n";
+
+  // Set up the input file.
+  std::string errorMessage;
+  auto file = openInputFile(inputFilename, &errorMessage);
+  if (!file) {
+    llvm::errs() << errorMessage << "\n";
+    return failure();
+  }
+
+  auto output = openOutputFile(outputFilename, &errorMessage);
+  if (!output) {
+    llvm::errs() << errorMessage << "\n";
+    return failure();
+  }
+  if (failed(MlirOptMain(output->os(), std::move(file), registry, config)))
+    return failure();
+
+  // Keep the output file if the invocation of MlirOptMain was successful.
+  output->keep();
+  return success();
+}
+
 int ireeOptRunMain(int argc, char **argv) {
   llvm::setBugReportMsg(
       "Please report issues to https://github.com/openxla/iree/issues and "
       "include the crash backtrace.\n");
-  llvm::InitLLVM y(argc, argv);
 
   mlir::DialectRegistry registry;
   mlir::iree_compiler::registerAllDialects(registry);
@@ -32,9 +126,8 @@
   // TODO: this should be upstreamed.
   mlir::linalg::transform::registerDropSchedulePass();
 
-  if (failed(MlirOptMain(argc, argv, "IREE modular optimizer driver\n",
-                         registry,
-                         /*preloadDialectsInContext=*/false))) {
+  if (failed(ireeOptMainFromCL(argc, argv, "IREE modular optimizer driver\n",
+                               registry))) {
     return 1;
   }
   return 0;
diff --git a/compiler/src/iree/compiler/Pipelines/Pipelines.cpp b/compiler/src/iree/compiler/Pipelines/Pipelines.cpp
index cb1c611..e7f0736 100644
--- a/compiler/src/iree/compiler/Pipelines/Pipelines.cpp
+++ b/compiler/src/iree/compiler/Pipelines/Pipelines.cpp
@@ -127,7 +127,8 @@
       break;
     default:
       IREE_TRACE_ADD_BEGIN_FRAME_PASS(passManager, "Preprocessing");
-      IREE::buildPreprocessingPassPipeline(passManager, preprocessingOptions);
+      IREE::buildPreprocessingPassPipeline(passManager, preprocessingOptions,
+                                           hooks.pipelineExtensions);
       IREE_TRACE_ADD_END_FRAME_PASS(passManager, "Preprocessing");
       if (compileTo == IREEVMPipelinePhase::Preprocessing)
         return;  // early-exit
diff --git a/compiler/src/iree/compiler/Pipelines/Pipelines.h b/compiler/src/iree/compiler/Pipelines/Pipelines.h
index c135f47..acf7776 100644
--- a/compiler/src/iree/compiler/Pipelines/Pipelines.h
+++ b/compiler/src/iree/compiler/Pipelines/Pipelines.h
@@ -16,6 +16,8 @@
 namespace mlir {
 namespace iree_compiler {
 
+class PipelineExtensions;
+
 // Hooks for injecting behavior into the IREEVM pipeline. Since these are not
 // derived from CLI options, we maintain them as a separate struct.
 struct IREEVMPipelineHooks {
@@ -26,6 +28,9 @@
   // the constant evaluator, which needs to recursively invoke these
   // pipelines.
   std::function<void(OpPassManager &)> buildConstEvalPassPipelineCallback;
+
+  // Applies pipeline extensions to the built pipeline if not nullptr.
+  PipelineExtensions *pipelineExtensions = nullptr;
 };
 
 enum class IREEVMPipelinePhase {
diff --git a/compiler/src/iree/compiler/PluginAPI/Client.cpp b/compiler/src/iree/compiler/PluginAPI/Client.cpp
index 1162a4b..341fac8 100644
--- a/compiler/src/iree/compiler/PluginAPI/Client.cpp
+++ b/compiler/src/iree/compiler/PluginAPI/Client.cpp
@@ -14,6 +14,8 @@
 
 namespace mlir::iree_compiler {
 
+PipelineExtensions::~PipelineExtensions() = default;
+
 AbstractPluginRegistration::~AbstractPluginRegistration() = default;
 AbstractPluginSession::~AbstractPluginSession() = default;
 
@@ -31,7 +33,9 @@
 
 void PluginRegistrar::registerPlugin(
     std::unique_ptr<AbstractPluginRegistration> registration) {
-  std::string_view id = registration->getPluginId();
+  // Need to copy the id since in the error case, the registration will be
+  // deleted before reporting the error message.
+  std::string id = std::string(registration->getPluginId());
   auto foundIt = registrations.insert(
       std::make_pair(llvm::StringRef(id), std::move(registration)));
   if (!foundIt.second) {
diff --git a/compiler/src/iree/compiler/PluginAPI/Client.h b/compiler/src/iree/compiler/PluginAPI/Client.h
index 94ede00..b22f473 100644
--- a/compiler/src/iree/compiler/PluginAPI/Client.h
+++ b/compiler/src/iree/compiler/PluginAPI/Client.h
@@ -4,6 +4,9 @@
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
+#ifndef IREE_COMPILER_PLUGINAPI_CLIENT_H_
+#define IREE_COMPILER_PLUGINAPI_CLIENT_H_
+
 #include <optional>
 #include <string_view>
 
@@ -13,6 +16,7 @@
 namespace mlir {
 class DialectRegistry;
 class MLIRContext;
+class OpPassManager;
 }  // namespace mlir
 
 namespace mlir::iree_compiler {
@@ -29,6 +33,16 @@
   static void bindOptions(OptionsBinder &binder) {}
 };
 
+// Entrypoints for extending IREE's pass pipelines at various stages.
+// Override what is needed.
+class PipelineExtensions {
+ public:
+  virtual ~PipelineExtensions();
+
+  // Adds passes to the |buildPreprocessingPassPipeline| pipeline at the end.
+  virtual void extendPreprocessingPassPipeline(OpPassManager &passManager) {}
+};
+
 // Abstract class representing a plugin registration. It is responsible for
 // various global initialization and creation of plugin sessions that mirror
 // the lifetime of and |iree_compiler_session_t| for when the plugin is
@@ -72,7 +86,7 @@
   // change behavior. It is safer to customize the context on a per-session
   // basis in a plugin session's activate() method (i.e. if registering
   // interfaces or behavior changes extensions).
-  virtual void registerDialects(DialectRegistry &registry) {}
+  virtual void registerGlobalDialects(DialectRegistry &registry) {}
 
   // Creates an uninitialized session. If the CLI was initialized, then this
   // should also ensure that any command line options were managed properly into
@@ -96,15 +110,24 @@
 // Most users will inherit from this class via the PluginSession CRTP helper,
 // which adds some niceties and support for global command line option
 // registration.
-class AbstractPluginSession {
+class AbstractPluginSession : public PipelineExtensions {
  public:
   virtual ~AbstractPluginSession();
 
+  // Called prior to context initialization in order to register dialects.
+  void registerDialects(DialectRegistry &registry) {
+    onRegisterDialects(registry);
+  }
+
   // Called after the session has been fully constructed. If it fails, then
   // it should emit an appropriate diagnostic.
   LogicalResult activate(MLIRContext *context);
 
  protected:
+  // Called from registerDialects() prior to initializing the context and
+  // prior to onActivate().
+  virtual void onRegisterDialects(DialectRegistry &registry) {}
+
   // Called from the activate() method once pre-conditions are verified and the
   // context is set.
   virtual LogicalResult onActivate() { return success(); };
@@ -122,7 +145,7 @@
   // AbstractPluginRegistration.
   static void globalInitialize() {}
   static void registerPasses() {}
-  static void registerDialects(DialectRegistry &registry) {}
+  static void registerGlobalDialects(DialectRegistry &registry) {}
 
   struct Registration : public AbstractPluginRegistration {
     using AbstractPluginRegistration::AbstractPluginRegistration;
@@ -135,9 +158,9 @@
       // Actually need to capture the reference, not a copy. So get a pointer.
       globalCLIOptions = &OptionsFromFlags<OptionsTy>::get();
     }
-    void registerDialects(DialectRegistry &registry) override {
+    void registerGlobalDialects(DialectRegistry &registry) override {
       // Forward to the CRTP derived type.
-      DerivedTy::registerDialects(registry);
+      DerivedTy::registerGlobalDialects(registry);
     }
     std::unique_ptr<AbstractPluginSession> createUninitializedSession(
         OptionsBinder &localOptionsBinder) override {
@@ -181,3 +204,5 @@
 };
 
 }  // namespace mlir::iree_compiler
+
+#endif  // IREE_COMPILER_PLUGINAPI_CLIENT_H_
diff --git a/compiler/src/iree/compiler/PluginAPI/PluginManager.cpp b/compiler/src/iree/compiler/PluginAPI/PluginManager.cpp
index a920181..3c9fda9 100644
--- a/compiler/src/iree/compiler/PluginAPI/PluginManager.cpp
+++ b/compiler/src/iree/compiler/PluginAPI/PluginManager.cpp
@@ -66,9 +66,9 @@
   }
 }
 
-void PluginManager::registerDialects(DialectRegistry &registry) {
+void PluginManager::registerGlobalDialects(DialectRegistry &registry) {
   for (auto &kv : registrations) {
-    kv.second->registerDialects(registry);
+    kv.second->registerGlobalDialects(registry);
   }
 }
 
@@ -82,7 +82,7 @@
   }
 }
 
-LogicalResult PluginManagerSession::activatePlugins(MLIRContext *context) {
+LogicalResult PluginManagerSession::initializePlugins() {
   auto getAvailableIds = [&]() -> llvm::SmallVector<llvm::StringRef> {
     llvm::SmallVector<llvm::StringRef> availableIds;
     for (auto &kv : allPluginSessions) {
@@ -105,25 +105,36 @@
   // sorting accordingly. For now, what you say is what you get.
   for (auto &pluginId : options.plugins) {
     if (options.printPluginInfo) {
-      llvm::errs() << "[IREE plugins]: Activating plugin '" << pluginId
+      llvm::errs() << "[IREE plugins]: Initializing plugin '" << pluginId
                    << "'\n";
     }
     auto foundIt = allPluginSessions.find(pluginId);
     if (foundIt == allPluginSessions.end()) {
-      auto diag = mlir::emitError(mlir::UnknownLoc::get(context))
-                  << "could not activate requested IREE plugin '" << pluginId
-                  << "' because it is not registered (available plugins: ";
-      llvm::interleaveComma(getAvailableIds(), diag);
-      diag << ")";
+      llvm::errs()
+          << "[IREE plugins error]: could not activate requested IREE plugin '"
+          << pluginId << "' because it is not registered (available plugins: ";
+      llvm::interleaveComma(getAvailableIds(), llvm::errs());
+      llvm::errs() << ")\n";
       return failure();
     }
 
-    AbstractPluginSession *instance = foundIt->second.get();
-    if (failed(instance->activate(context))) return failure();
-    activatedSessions.push_back(instance);
+    initializedSessions.push_back(foundIt->second.get());
   }
 
   return success();
 }
 
+void PluginManagerSession::registerDialects(DialectRegistry &registry) {
+  for (auto *s : initializedSessions) {
+    s->registerDialects(registry);
+  }
+}
+
+LogicalResult PluginManagerSession::activatePlugins(MLIRContext *context) {
+  for (auto *s : initializedSessions) {
+    if (failed(s->activate(context))) return failure();
+  }
+  return success();
+}
+
 }  // namespace mlir::iree_compiler
diff --git a/compiler/src/iree/compiler/PluginAPI/PluginManager.h b/compiler/src/iree/compiler/PluginAPI/PluginManager.h
index b39a802..c76e5f7 100644
--- a/compiler/src/iree/compiler/PluginAPI/PluginManager.h
+++ b/compiler/src/iree/compiler/PluginAPI/PluginManager.h
@@ -4,6 +4,9 @@
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
+#ifndef IREE_COMPILER_PLUGINAPI_PLUGINMANAGER_H_
+#define IREE_COMPILER_PLUGINAPI_PLUGINMANAGER_H_
+
 #include <optional>
 #include <string_view>
 #include <vector>
@@ -66,29 +69,44 @@
 
   // Calls through to AbstractPluginRegistration::registerDialects for all
   // available plugins.
-  void registerDialects(DialectRegistry &registry);
+  void registerGlobalDialects(DialectRegistry &registry);
 
  private:
   friend class PluginManagerSession;
 };
 
 // Holds activated plugins for an |iree_compiler_session_t|.
-class PluginManagerSession {
+class PluginManagerSession : public PipelineExtensions {
  public:
   PluginManagerSession(PluginManager &pluginManager, OptionsBinder &binder,
                        PluginManagerOptions &options);
 
+  // Initializes all plugins that should be activated by default.
+  LogicalResult initializePlugins();
+
+  // Invokes registerDialects() on all initialized plugins.
+  void registerDialects(DialectRegistry &registry);
+
   // Activates plugins as configured.
   LogicalResult activatePlugins(MLIRContext *context);
 
+  // Forward pipeline extensions.
+  void extendPreprocessingPassPipeline(OpPassManager &passManager) override {
+    for (auto *s : initializedSessions) {
+      s->extendPreprocessingPassPipeline(passManager);
+    }
+  }
+
  private:
   PluginManagerOptions &options;
   // At construction, uninitialized plugin sessions are created for all
   // registered plugins so that CLI options can be set properly.
   llvm::StringMap<std::unique_ptr<AbstractPluginSession>> allPluginSessions;
 
-  // Activation state.
-  llvm::SmallVector<AbstractPluginSession *> activatedSessions;
+  // Initialized list of plugins.
+  llvm::SmallVector<AbstractPluginSession *> initializedSessions;
 };
 
 }  // namespace mlir::iree_compiler
+
+#endif  // IREE_COMPILER_PLUGINAPI_PLUGINMANAGER_H_
diff --git a/compiler/src/iree/compiler/Preprocessing/BUILD.bazel b/compiler/src/iree/compiler/Preprocessing/BUILD.bazel
index 11993e9..13d3feb 100644
--- a/compiler/src/iree/compiler/Preprocessing/BUILD.bazel
+++ b/compiler/src/iree/compiler/Preprocessing/BUILD.bazel
@@ -22,6 +22,7 @@
     ],
     deps = [
         "//compiler/src/iree/compiler/Pipelines:Options",
+        "//compiler/src/iree/compiler/PluginAPI",
         "//compiler/src/iree/compiler/Preprocessing/Common:Transforms",
         "@llvm-project//llvm:Support",
         "@llvm-project//mlir:ArithDialect",
diff --git a/compiler/src/iree/compiler/Preprocessing/CMakeLists.txt b/compiler/src/iree/compiler/Preprocessing/CMakeLists.txt
index 67a6ed2..dcef2c2 100644
--- a/compiler/src/iree/compiler/Preprocessing/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Preprocessing/CMakeLists.txt
@@ -22,6 +22,7 @@
     MLIRArithDialect
     MLIRPass
     iree::compiler::Pipelines::Options
+    iree::compiler::PluginAPI
     iree::compiler::Preprocessing::Common::Transforms
   PUBLIC
 )
diff --git a/compiler/src/iree/compiler/Preprocessing/Passes.cpp b/compiler/src/iree/compiler/Preprocessing/Passes.cpp
index f30688e..27e0132 100644
--- a/compiler/src/iree/compiler/Preprocessing/Passes.cpp
+++ b/compiler/src/iree/compiler/Preprocessing/Passes.cpp
@@ -15,41 +15,37 @@
 namespace iree_compiler {
 namespace IREE {
 
-void buildPreprocessingPassPipeline(
-    OpPassManager &passManager,
-    const PreprocessingOptions &preprocessingOptions) {
-  auto pipelineStr = preprocessingOptions.preprocessingPassPipeline;
-  if (pipelineStr.empty()) {
-    return;
-  }
+namespace {
 
+void extendWithTextPipeline(OpPassManager &passManager,
+                            StringRef textPipeline) {
+  StringRef orig = textPipeline;
   // Strip the `builtin.module(...)` that surrounds the pass pipeline
   // description. On failure an assertion is triggered, but in release builds
   // it just will silently return and not raise an error. There is no
   // way to handle the error in caller currently.
-  StringRef text(pipelineStr);
-  size_t pos = text.find_first_of("(");
+  size_t pos = textPipeline.find_first_of("(");
   if (pos == StringRef::npos) {
     llvm::errs() << "ERROR: expected preprocessing pass pipeline string to be "
                     "nested within `builtin.module(..)`; got `"
-                 << pipelineStr << "`\n";
+                 << orig << "`\n";
     return;
   }
-  if (text.substr(0, pos) != "builtin.module") {
+  if (textPipeline.substr(0, pos) != "builtin.module") {
     llvm::errs() << "ERROR: expected preprocessing pass pipeline string to be "
                     "nested within `builtin.module(..)`; got `"
-                 << pipelineStr << "`\n";
+                 << orig << "`\n";
     return;
   }
-  if (text.back() != ')') {
-    llvm::errs() << "ERROR: mismatched parenthesis in pass pipeline `"
-                 << pipelineStr << "`\n";
+  if (textPipeline.back() != ')') {
+    llvm::errs() << "ERROR: mismatched parenthesis in pass pipeline `" << orig
+                 << "`\n";
     return;
   }
-  text = text.substr(pos + 1);
-  if (failed(parsePassPipeline(text.drop_back(), passManager))) {
-    llvm::errs() << "ERROR: mismatched parenthesis in pass pipeline `"
-                 << pipelineStr << "`\n";
+  textPipeline = textPipeline.substr(pos + 1);
+  if (failed(parsePassPipeline(textPipeline.drop_back(), passManager))) {
+    llvm::errs() << "ERROR: mismatched parenthesis in pass pipeline `" << orig
+                 << "`\n";
     return;
   }
   LLVM_DEBUG({
@@ -58,6 +54,23 @@
   });
 }
 
+}  // namespace
+
+void buildPreprocessingPassPipeline(
+    OpPassManager &passManager,
+    const PreprocessingOptions &preprocessingOptions,
+    PipelineExtensions *pipelineExtensions) {
+  auto pipelineStr = preprocessingOptions.preprocessingPassPipeline;
+  if (!preprocessingOptions.preprocessingPassPipeline.empty()) {
+    extendWithTextPipeline(passManager,
+                           preprocessingOptions.preprocessingPassPipeline);
+  }
+
+  if (pipelineExtensions) {
+    pipelineExtensions->extendPreprocessingPassPipeline(passManager);
+  }
+}
+
 void registerPreprocessingPasses() { registerCommonPreprocessingPasses(); }
 
 }  // namespace IREE
diff --git a/compiler/src/iree/compiler/Preprocessing/Passes.h b/compiler/src/iree/compiler/Preprocessing/Passes.h
index 0eae95c..7768e82 100644
--- a/compiler/src/iree/compiler/Preprocessing/Passes.h
+++ b/compiler/src/iree/compiler/Preprocessing/Passes.h
@@ -10,6 +10,7 @@
 #include <functional>
 
 #include "iree/compiler/Pipelines/Options.h"
+#include "iree/compiler/PluginAPI/Client.h"
 #include "mlir/Pass/PassManager.h"
 
 namespace mlir {
@@ -22,8 +23,9 @@
 /// on the sequence of preprocessing passes to run after conversion from input
 /// dialects like `mhlo`/`tosa` before running the core IREE compilation
 /// pipelines (starting with the flow pipeline).
-void buildPreprocessingPassPipeline(OpPassManager &passManager,
-                                    const PreprocessingOptions &options);
+void buildPreprocessingPassPipeline(
+    OpPassManager &passManager, const PreprocessingOptions &options,
+    PipelineExtensions *pipelineExtensions = nullptr);
 
 void registerPreprocessingPasses();
 
@@ -31,4 +33,4 @@
 }  // namespace iree_compiler
 }  // namespace mlir
 
-#endif  // IREE_COMPILER_PREPROCESSING_PASSES_H_
\ No newline at end of file
+#endif  // IREE_COMPILER_PREPROCESSING_PASSES_H_
diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt
index 1e70c9d..057c48d 100644
--- a/samples/CMakeLists.txt
+++ b/samples/CMakeLists.txt
@@ -4,4 +4,19 @@
 # See https://llvm.org/LICENSE.txt for license information.
 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
-iree_add_all_subdirs()
+# Not CMake directories:
+#   colab
+#   models
+#   vision_inference
+# Samples that are actually external projects do not get directly
+# included:
+#   compiler_plugins
+
+add_subdirectory(custom_dispatch)
+add_subdirectory(custom_module)
+add_subdirectory(dynamic_shapes)
+add_subdirectory(emitc_modules)
+add_subdirectory(py_custom_module)
+add_subdirectory(simple_embedding)
+add_subdirectory(static_library)
+add_subdirectory(variables_and_state)
diff --git a/samples/compiler_plugins/simple_io_sample/BUILD.bazel b/samples/compiler_plugins/simple_io_sample/BUILD.bazel
new file mode 100644
index 0000000..32556d8
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/BUILD.bazel
@@ -0,0 +1,152 @@
+# Copyright 2023 The IREE Authors
+#
+# Licensed 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
+
+load("//build_tools/bazel:build_defs.oss.bzl", "iree_compiler_register_plugin", "iree_gentbl_cc_library", "iree_tablegen_doc", "iree_td_library")
+
+package(
+    default_visibility = ["//visibility:public"],
+    features = ["layering_check"],
+    licenses = ["notice"],  # Apache 2.0
+)
+
+cc_library(
+    name = "defs",
+    includes = ["src"],
+)
+
+cc_library(
+    name = "registration",
+    srcs = [
+        "src/PluginRegistration.cpp",
+    ],
+    deps = [
+        ":IR",
+        ":Transforms",
+        ":defs",
+        "//compiler/src/iree/compiler/PluginAPI",
+        "@llvm-project//mlir:IR",
+        "@llvm-project//mlir:Pass",
+    ],
+)
+
+iree_compiler_register_plugin(
+    plugin_id = "simple_io_sample",
+    target = ":registration",
+)
+
+iree_td_library(
+    name = "td_files",
+    srcs = [
+        "src/simple_io_sample/IR/SimpleIOOps.td",
+        "src/simple_io_sample/Transforms/Passes.td",
+    ],
+    deps = [
+        "@llvm-project//mlir:ControlFlowInterfacesTdFiles",
+        "@llvm-project//mlir:FuncTdFiles",
+        "@llvm-project//mlir:InferTypeOpInterfaceTdFiles",
+        "@llvm-project//mlir:OpBaseTdFiles",
+        "@llvm-project//mlir:SideEffectInterfacesTdFiles",
+        "@llvm-project//mlir:ViewLikeInterfaceTdFiles",
+    ],
+)
+
+cc_library(
+    name = "IR",
+    srcs = [
+        "src/simple_io_sample/IR/SimpleIODialect.cpp",
+        "src/simple_io_sample/IR/SimpleIOOps.cpp",
+        "src/simple_io_sample/IR/SimpleIOOps.cpp.inc",
+    ],
+    hdrs = [
+        "src/simple_io_sample/IR/SimpleIODialect.h",
+        "src/simple_io_sample/IR/SimpleIOOps.h",
+        "src/simple_io_sample/IR/SimpleIOOps.h.inc",
+    ],
+    deps = [
+        ":SimpleIOOpsGen",
+        ":defs",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//mlir:FuncDialect",
+        "@llvm-project//mlir:IR",
+        "@llvm-project//mlir:Support",
+    ],
+)
+
+iree_gentbl_cc_library(
+    name = "SimpleIOOpsGen",
+    tbl_outs = [
+        (
+            ["--gen-dialect-decls"],
+            "src/simple_io_sample/IR/SimpleIODialect.h.inc",
+        ),
+        (
+            ["--gen-dialect-defs"],
+            "src/simple_io_sample/IR/SimpleIODialect.cpp.inc",
+        ),
+        (
+            ["--gen-op-decls"],
+            "src/simple_io_sample/IR/SimpleIOOps.h.inc",
+        ),
+        (
+            ["--gen-op-defs"],
+            "src/simple_io_sample/IR/SimpleIOOps.cpp.inc",
+        ),
+    ],
+    tblgen = "@llvm-project//mlir:mlir-tblgen",
+    td_file = "src/simple_io_sample/IR/SimpleIOOps.td",
+    deps = [":td_files"],
+)
+
+cc_library(
+    name = "Transforms",
+    srcs = [
+        "src/simple_io_sample/Transforms/LegalizeSimpleIO.cpp",
+    ],
+    hdrs = [
+        "src/simple_io_sample/Transforms/Passes.h",
+        "src/simple_io_sample/Transforms/Passes.h.inc",
+    ],
+    deps = [
+        ":IR",
+        ":PassesIncGen",
+        ":defs",
+        "@llvm-project//mlir:FuncDialect",
+        "@llvm-project//mlir:IR",
+        "@llvm-project//mlir:Pass",
+    ],
+)
+
+iree_gentbl_cc_library(
+    name = "PassesIncGen",
+    tbl_outs = [
+        (
+            ["--gen-pass-decls"],
+            "src/simple_io_sample/Transforms/Passes.h.inc",
+        ),
+    ],
+    tblgen = "@llvm-project//mlir:mlir-tblgen",
+    td_file = "src/simple_io_sample/Transforms/Passes.td",
+    deps = [
+        ":td_files",
+        "@llvm-project//mlir:PassBaseTdFiles",
+    ],
+)
+
+iree_tablegen_doc(
+    name = "SimpleIODialectDocGen",
+    tbl_outs = [
+        (
+            [
+                "--gen-dialect-doc",
+                "-dialect=simple_io",
+            ],
+            "src/simple_io_sample/IR/SimpleIODialect.md",
+        ),
+    ],
+    tblgen = "@llvm-project//mlir:mlir-tblgen",
+    td_file = "src/simple_io_sample/IR/SimpleIOOps.td",
+    deps = [":td_files"],
+)
diff --git a/samples/compiler_plugins/simple_io_sample/CMakeLists.txt b/samples/compiler_plugins/simple_io_sample/CMakeLists.txt
new file mode 100644
index 0000000..12ba8a4
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/CMakeLists.txt
@@ -0,0 +1,113 @@
+################################################################################
+# Autogenerated by build_tools/bazel_to_cmake/bazel_to_cmake.py from           #
+# samples/compiler_plugins/simple_io_sample/BUILD.bazel                        #
+#                                                                              #
+# Use iree_cmake_extra_content from iree/build_defs.oss.bzl to add arbitrary   #
+# CMake-only content.                                                          #
+#                                                                              #
+# To disable autogeneration for this file entirely, delete this header.        #
+################################################################################
+
+iree_add_all_subdirs()
+
+iree_cc_library(
+  NAME
+    defs
+  INCLUDES
+    "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>"
+    "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src>"
+  PUBLIC
+)
+
+iree_cc_library(
+  NAME
+    registration
+  SRCS
+    "src/PluginRegistration.cpp"
+  DEPS
+    ::IR
+    ::Transforms
+    ::defs
+    MLIRIR
+    MLIRPass
+    iree::compiler::PluginAPI
+  PUBLIC
+)
+
+iree_compiler_register_plugin(
+  PLUGIN_ID
+    simple_io_sample
+  TARGET
+    ::registration
+)
+
+iree_cc_library(
+  NAME
+    IR
+  HDRS
+    "src/simple_io_sample/IR/SimpleIODialect.h"
+    "src/simple_io_sample/IR/SimpleIOOps.h"
+    "src/simple_io_sample/IR/SimpleIOOps.h.inc"
+  SRCS
+    "src/simple_io_sample/IR/SimpleIODialect.cpp"
+    "src/simple_io_sample/IR/SimpleIOOps.cpp"
+    "src/simple_io_sample/IR/SimpleIOOps.cpp.inc"
+  DEPS
+    ::SimpleIOOpsGen
+    ::defs
+    LLVMSupport
+    MLIRFuncDialect
+    MLIRIR
+    MLIRSupport
+  PUBLIC
+)
+
+iree_tablegen_library(
+  NAME
+    SimpleIOOpsGen
+  TD_FILE
+    "src/simple_io_sample/IR/SimpleIOOps.td"
+  OUTS
+    --gen-dialect-decls src/simple_io_sample/IR/SimpleIODialect.h.inc
+    --gen-dialect-defs src/simple_io_sample/IR/SimpleIODialect.cpp.inc
+    --gen-op-decls src/simple_io_sample/IR/SimpleIOOps.h.inc
+    --gen-op-defs src/simple_io_sample/IR/SimpleIOOps.cpp.inc
+)
+
+iree_cc_library(
+  NAME
+    Transforms
+  HDRS
+    "src/simple_io_sample/Transforms/Passes.h"
+    "src/simple_io_sample/Transforms/Passes.h.inc"
+  SRCS
+    "src/simple_io_sample/Transforms/LegalizeSimpleIO.cpp"
+  DEPS
+    ::IR
+    ::PassesIncGen
+    ::defs
+    MLIRFuncDialect
+    MLIRIR
+    MLIRPass
+  PUBLIC
+)
+
+iree_tablegen_library(
+  NAME
+    PassesIncGen
+  TD_FILE
+    "src/simple_io_sample/Transforms/Passes.td"
+  OUTS
+    --gen-pass-decls src/simple_io_sample/Transforms/Passes.h.inc
+)
+
+iree_tablegen_doc(
+  NAME
+    SimpleIODialectDocGen
+  TD_FILE
+    "src/simple_io_sample/IR/SimpleIOOps.td"
+  OUTS
+    --gen-dialect-doc -dialect=simple_io src/simple_io_sample/IR/SimpleIODialect.md
+)
+
+### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_BELOW_THIS_LINE ###
diff --git a/samples/compiler_plugins/simple_io_sample/README.md b/samples/compiler_plugins/simple_io_sample/README.md
new file mode 100644
index 0000000..d123902
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/README.md
@@ -0,0 +1,37 @@
+# SimpleIO Compiler Plugin Sample
+
+WARNING: This sample is under construction.
+
+This sample demonstrates a compiler plugin which:
+
+* Adds a new dialect to IREE
+* Implements pre-processor lowerings to transform ops to internal
+  implementations (TODO)
+* Has a python-based runner that implements the IO ops in pure python (TODO)
+* Illustrates some advanced features of the way such things can be
+  constructed (custom types, async, etc) (TODO)
+* Show how to test such a plugin (TODO)
+
+To use this, the plugin must be built into the compiler via:
+
+```
+-DIREE_COMPILER_PLUGIN_PATHS=samples/compiler_plugins/simple_io_sample
+```
+
+It can then be activated in either `iree-opt` or `iree-compile` via the
+option `--iree-plugin=simple_io_sample`.
+
+To compile a sample:
+
+```
+iree-compile --iree-plugin=simple_io_sample test/print.mlir -o /tmp/print.vmfb
+python run_mock.py /tmp/print.vmfb
+```
+
+Should print:
+
+```
+--- Loading /tmp/print.vmfb
+--- Running main()
++++ HELLO FROM SIMPLE_IO
+```
diff --git a/samples/compiler_plugins/simple_io_sample/src/PluginRegistration.cpp b/samples/compiler_plugins/simple_io_sample/src/PluginRegistration.cpp
new file mode 100644
index 0000000..5ddd8b7
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/PluginRegistration.cpp
@@ -0,0 +1,55 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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 "iree/compiler/PluginAPI/Client.h"
+#include "mlir/IR/Diagnostics.h"
+#include "mlir/IR/Location.h"
+#include "mlir/IR/MLIRContext.h"
+#include "mlir/Pass/Pass.h"
+#include "simple_io_sample/IR/SimpleIODialect.h"
+#include "simple_io_sample/Transforms/Passes.h"
+
+using namespace mlir;
+using namespace mlir::iree_compiler;
+
+namespace detail {
+namespace {
+
+#define GEN_PASS_REGISTRATION
+#include "simple_io_sample/Transforms/Passes.h.inc"
+
+}  // namespace
+}  // namespace detail
+
+namespace {
+
+struct MyOptions {
+  void bindOptions(OptionsBinder &binder) {}
+};
+
+struct MySession : public PluginSession<MySession, MyOptions> {
+  static void registerPasses() { ::detail::registerPasses(); }
+
+  void onRegisterDialects(DialectRegistry &registry) override {
+    registry.insert<IREE::SimpleIO::SimpleIODialect>();
+  }
+
+  LogicalResult onActivate() override { return success(); }
+
+  void extendPreprocessingPassPipeline(OpPassManager &pm) override {
+    pm.addPass(IREE::SimpleIO::createLegalizeSimpleIOPass());
+  }
+};
+
+}  // namespace
+
+IREE_DEFINE_COMPILER_OPTION_FLAGS(MyOptions);
+
+extern "C" bool iree_register_compiler_plugin_simple_io_sample(
+    mlir::iree_compiler::PluginRegistrar *registrar) {
+  registrar->registerPlugin<MySession>("simple_io_sample");
+  return true;
+}
diff --git a/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIODialect.cpp b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIODialect.cpp
new file mode 100644
index 0000000..313a015
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIODialect.cpp
@@ -0,0 +1,22 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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 "simple_io_sample/IR/SimpleIODialect.h"
+
+#include "simple_io_sample/IR/SimpleIOOps.h"
+
+namespace mlir::iree_compiler::IREE::SimpleIO {
+
+void SimpleIODialect::initialize() {
+  addOperations<
+#define GET_OP_LIST
+#include "simple_io_sample/IR/SimpleIOOps.cpp.inc"
+      >();
+}
+
+}  // namespace mlir::iree_compiler::IREE::SimpleIO
+
+#include "simple_io_sample/IR/SimpleIODialect.cpp.inc"
diff --git a/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIODialect.h b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIODialect.h
new file mode 100644
index 0000000..06fc3bb
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIODialect.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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
+
+#ifndef IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_IR_SIMPLEIODIALECT_H_
+#define IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_IR_SIMPLEIODIALECT_H_
+
+#include "mlir/IR/Dialect.h"
+#include "mlir/IR/OpDefinition.h"
+
+// Include generated.
+#include "simple_io_sample/IR/SimpleIODialect.h.inc"  // IWYU pragma: keep
+
+#endif  // IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_IR_SIMPLEIODIALECT_H_
diff --git a/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.cpp b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.cpp
new file mode 100644
index 0000000..1acebd4
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.cpp
@@ -0,0 +1,14 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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 "simple_io_sample/IR/SimpleIOOps.h"
+
+#include "mlir/IR/OpImplementation.h"
+
+// clang-format off
+#define GET_OP_CLASSES
+#include "simple_io_sample/IR/SimpleIOOps.cpp.inc" // IWYU pragma: keep
+// clang-format on
diff --git a/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.h b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.h
new file mode 100644
index 0000000..f9602db
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.h
@@ -0,0 +1,17 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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
+
+#ifndef IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_IR_SIMPLEIOOPS_H_
+#define IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_IR_SIMPLEIOOPS_H_
+
+#include "mlir/IR/Builders.h"
+#include "mlir/IR/Operation.h"
+
+// Include generated.
+#define GET_OP_CLASSES
+#include "simple_io_sample/IR/SimpleIOOps.h.inc"  // IWYU pragma: keep
+
+#endif  // IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_IR_SIMPLEIOOPS_H_
diff --git a/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.td b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.td
new file mode 100644
index 0000000..b5324f0
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/IR/SimpleIOOps.td
@@ -0,0 +1,31 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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
+
+#ifndef SIMPLE_IO_SAMPLE
+#define SIMPLE_IO_SAMPLE
+
+include "mlir/IR/OpBase.td"
+
+def SimpleIO_Dialect : Dialect {
+  let name = "simple_io";
+  let cppNamespace = "::mlir::iree_compiler::IREE::SimpleIO";
+}
+
+class SimpleIO_Op<string mnemonic, list<Trait> traits = []> :
+    Op<SimpleIO_Dialect, mnemonic, traits> {
+}
+
+def SimpleIO_PrintOp : SimpleIO_Op<"print", []> {
+  let summary = [{Print}];
+  let arguments = (ins);
+  let results = (outs);
+
+  let assemblyFormat = [{
+    attr-dict
+  }];
+}
+
+#endif
diff --git a/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/LegalizeSimpleIO.cpp b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/LegalizeSimpleIO.cpp
new file mode 100644
index 0000000..4e60da2
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/LegalizeSimpleIO.cpp
@@ -0,0 +1,54 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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/Dialect/Func/IR/FuncOps.h"
+#include "mlir/IR/Builders.h"
+#include "mlir/IR/BuiltinAttributes.h"
+#include "mlir/IR/BuiltinTypes.h"
+#include "simple_io_sample/IR/SimpleIOOps.h"
+#include "simple_io_sample/Transforms/Passes.h"
+
+#define GEN_PASS_DEF_LEGALIZESIMPLEIO
+#include "simple_io_sample/Transforms/Passes.h.inc"
+
+namespace mlir::iree_compiler::IREE::SimpleIO {
+namespace {
+
+class LegalizeSimpleIOPass
+    : public ::impl::LegalizeSimpleIOBase<LegalizeSimpleIOPass> {
+ public:
+  void runOnOperation() override {
+    auto *context = &getContext();
+    // TODO: This is all just a placeholder. To make it real, we should be
+    // checking if the import already exists and likely doing some more fancy
+    // lowering.
+    // Add imports.
+    auto m = getOperation();
+    auto importBuilder = OpBuilder::atBlockBegin(m.getBody());
+    importBuilder
+        .create<func::FuncOp>(m.getLoc(), "simple_io.print",
+                              FunctionType::get(context, {}, {}))
+        .setPrivate();
+
+    // Legalize operations.
+    m.walk([&](Operation *op) {
+      if (auto printOp = dyn_cast<IREE::SimpleIO::PrintOp>(op)) {
+        OpBuilder b(op);
+        b.create<func::CallOp>(printOp.getLoc(), "simple_io.print",
+                               TypeRange{});
+        printOp.erase();
+      }
+    });
+  }
+};
+
+}  // namespace
+
+std::unique_ptr<OperationPass<ModuleOp>> createLegalizeSimpleIOPass() {
+  return std::make_unique<LegalizeSimpleIOPass>();
+}
+
+}  // namespace mlir::iree_compiler::IREE::SimpleIO
diff --git a/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/Passes.h b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/Passes.h
new file mode 100644
index 0000000..58c01d8
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/Passes.h
@@ -0,0 +1,20 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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
+
+#ifndef IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_TRANSFORMS_PASSES_H_
+#define IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_TRANSFORMS_PASSES_H_
+
+#include "mlir/IR/BuiltinOps.h"
+#include "mlir/Pass/Pass.h"
+#include "mlir/Pass/PassManager.h"
+
+namespace mlir::iree_compiler::IREE::SimpleIO {
+
+std::unique_ptr<OperationPass<ModuleOp>> createLegalizeSimpleIOPass();
+
+}  // namespace mlir::iree_compiler::IREE::SimpleIO
+
+#endif  // IREE_SAMPLES_COMPILER_PLUGINS_SIMPLE_IO_SAMPLE_TRANSFORMS_PASSES_H_
diff --git a/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/Passes.td b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/Passes.td
new file mode 100644
index 0000000..38f7f20
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/src/simple_io_sample/Transforms/Passes.td
@@ -0,0 +1,19 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed 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/Pass/PassBase.td"
+
+#ifndef IREE_SIMPLEIO_PASSES
+#define IREE_SIMPLEIO_PASSES
+
+def LegalizeSimpleIO : Pass<"iree-simpleio-legalize", "mlir::ModuleOp"> {
+  let summary = "Legalizes the simpleio sample ops";
+  let constructor = [{
+    ::mlir::iree_compiler::IREE::SimpleIO::createLegalizeSimpleIOPass()
+  }];
+}
+
+#endif // IREE_SIMPLEIO_PASSES
diff --git a/samples/compiler_plugins/simple_io_sample/test/print.mlir b/samples/compiler_plugins/simple_io_sample/test/print.mlir
new file mode 100644
index 0000000..b27b7b8
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/test/print.mlir
@@ -0,0 +1,7 @@
+// RUN: iree-opt --iree-plugin=simple_io_sample --iree-print-plugin-info %s
+
+func.func @main() {
+  // CHECK: call @simple_io.print
+  simple_io.print
+  func.return
+}
diff --git a/samples/compiler_plugins/simple_io_sample/test/run_mock.py b/samples/compiler_plugins/simple_io_sample/test/run_mock.py
new file mode 100644
index 0000000..db9454a
--- /dev/null
+++ b/samples/compiler_plugins/simple_io_sample/test/run_mock.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# Copyright 2023 The IREE Authors
+#
+# Licensed 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
+
+# TODO: Turn this into a real test and wire it up.
+# Usage:
+#   iree-compile --iree-plugin=simple_io_sample print.mlir -o print.vmfb
+#   run_mock.py print.vmfb
+
+import iree.runtime as rt
+import sys
+
+input_file = sys.argv[1]
+print(f"--- Loading {input_file}")
+
+with open(input_file, "rb") as f:
+  vmfb_contents = f.read()
+
+
+def create_simple_io_module():
+
+  class SimpleIO:
+
+    def __init__(self, iface):
+      ...
+
+    def print_impl(self):
+      print("+++ HELLO FROM SIMPLE_IO")
+
+  iface = rt.PyModuleInterface("simple_io", SimpleIO)
+  iface.export("print", "0v_v", SimpleIO.print_impl)
+  return iface.create()
+
+
+config = rt.Config("local-sync")
+main_module = rt.VmModule.from_flatbuffer(config.vm_instance, vmfb_contents)
+modules = config.default_vm_modules + (
+    create_simple_io_module(),
+    main_module,
+)
+context = rt.SystemContext(vm_modules=modules, config=config)
+
+print("--- Running main()")
+context.modules.module.main()