Skip to main content

How to Extend Morph and Analysis

This guide covers the extension points that are active today:

  • NIR optimizer morphs
  • structural trail semantic components
  • root lowering components

The default NIR optimizer is Morph-backed. Trail-owned lowering is also Morph-backed, but core only provides a structural host. Domain rules stay in morphs/<Domain>/.

NIR Optimizer Morph

1. Keep rewrite logic in an OptimizationPass

Optimizer morphs still execute normal OptimizationPass implementations so the rewrite logic stays directly unit-testable.

#include "OptimizerInternal.h"

class MyPass final : public OptimizationPass {
public:
bool runOnModule(Module *module) override;
const char *getName() const override { return "My Pass"; }
};

2. Register the morph in a domain library

Do not hardcode default optimizer behavior in Optimizer.cpp or src/nir/morph/MorphPipeline.cpp.

The canonical source of truth is:

  • morphs/<Domain>/morph.toml
  • morphs/pipelines.toml
  • morphs/<Domain>/plugin/*.cpp
  • morphs/<Domain>/host/*.cpp

Each optimizer component must declare:

  • stable component_id
  • domain_id
  • phase optimizer
  • artifact pair nir -> nir
  • capability list in the core.*, domain.*, or target.* namespaces

The plugin descriptor is metadata. The executable in-tree runtime is wired through the optimizer host registry.

3. Add the component to morphs/pipelines.toml

The default optimizer order is not hardcoded in Optimizer.cpp.

If a morph must participate in the default optimizer pipeline, add its component_id to morphs/pipelines.toml. Missing or incompatible entries are fatal and covered by tests.

4. Add sources to CMake

If you add new source files under src/nir/ or morphs/, register them in the relevant CMakeLists.txt files.

5. Verify

morphc.bat test "Morph|Nir|Optimizer"

Structural Trail Lowering

Lowering is no longer authored as string-driven host callbacks and it is no longer backed by Input-specific sema types in core.

The current canonical path is:

  • raw parser AST remains generic
  • core builds a structural TrailView
  • morphs/<Domain>/features/<Feature>/feature.toml defines root and chain signatures
  • plugin semantic components build an opaque TrailFact
  • the root lowering component emits NIR from that fact

Input is the first implemented slice of this model.

1. Keep core structural

Core owns only generic infrastructure:

  • TrailView
  • semantic dispatch
  • lowering dispatch
  • global type registry
  • global builtin registry

Core does not own Input semantics, chain legality, or root-specific type rules.

2. Define the schema in feature.toml

Describe the surface in morphs/<Domain>/features/<Feature>/feature.toml.

The active schema uses:

  • [<semantic.family>]
  • optional [<semantic.family>.repl]
  • nested [<semantic.family>.chain.<name>]
  • operator surfaces such as [tensor.add] with operator = "operator.add"
  • route sections such as [tensor.add.route.hostllvm]
  • generic_*
  • param_*
  • returns
  • builtin_family
  • source
  • component
  • component_class

Manifest identifiers are compiled into manifest/runtime ids during load. Author code should not dispatch on those names as raw strings.

3. Put semantic ownership in plugin components

Root and chain semantic components live in domain sources such as:

  • morphs/Core/features/Input/Input.cpp
  • morphs/Core/features/Input/chain/Default.cpp
  • morphs/Core/features/Input/chain/TimeoutMs.cpp
  • morphs/Core/features/Input/repl/Behavior.cpp

Root components live inside their own directory. That directory is the scope anchor for related chain/ and optional repl/ components. Do not flatten root sources back to morphs/<Domain>/Lowering/<Root>.cpp; the chain loader assumes the root folder layout.

The root component creates the initial TrailFact. Chain components validate and update that fact. Final NIR emission happens only in the root lowering component.

4. Lower from typed facts, not raw syntax

Lowering code should consume:

  • TrailFact
  • resultType()
  • global type metadata
  • global builtin family resolution

These are explicitly out of bounds in lowering code:

  • stage.method == "Min" style dispatch
  • Input-specific parser or sema node types in core
  • raw type-name string mapping such as int -> target kind
  • component_id-keyed lowering dispatch

5. Verify

Use the morph, NIR, parser, semantic, and REPL coverage together:

morphc.bat test "Morph|Nir|Parser|Semantic|ReplPipeline"

Legacy Notes

  • Directly editing the default optimizer order in Optimizer.cpp is legacy.
  • Lowering host executors keyed by component_id are removed for the trail lowering path.
  • New Morph-owned lowering work should follow the structural trail + plugin-owned semantic chain model.