|
MimIR 0.1
MimIR is my Intermediate Representation
|
In MimIR, rewriting means rebuilding an IR graph, usually into a target World, while preserving sharing and handling mutable nodes correctly. The rewriting infrastructure is used for tasks like copying IR between worlds, rebuilding terms after substitutions, and reconstructing terms after local changes. It is also the foundation of several phase families.
Rewriter is the general mechanism for recursively rebuilding IR.
Conceptually, it walks a graph of Defs and constructs corresponding nodes in a target world. While doing so, it maintains a mapping from old nodes to new nodes. This is what makes rewriting graph-aware rather than tree-only: if the same old node is referenced multiple times, the rewritten graph will reuse the same rebuilt node instead of duplicating it.
A useful way to think about Rewriter is:
The mapping is also the basis for termination on cyclic or recursive structures. For mutable nodes, rewriting typically proceeds by first creating a stub, recording the mapping from old mutable to new mutable, and only then rewriting and filling its operands. This allows recursive references to resolve to the stub instead of recursing forever.
So the two key ideas behind Rewriter are:
At a high level, Rewriter preserves:
Rewriter uses a stack of mappings rather than a single global map. This allows temporary, scoped rewrites. This matters whenever rewriting enters a context where some old node should mean something different locally than globally. Inner mappings shadow outer ones, and leaving the scope removes the temporary bindings again. This scoped structure is important for binder-sensitive rewrites and for transformations that temporarily substitute certain nodes while rebuilding a larger term.
In practice, mutable rewrites and scoped substitutions go together. When a transformation may encounter recursion, cycles, or binder-introduced variables, the usual pattern is:
The base Rewriter follows this pattern through mim::Rewriter::rewrite_stub, and custom phases often mirror it manually when they need more control over how a mutable is rebuilt. The crucial point is that recursive self-references must see the already-created replacement mutable instead of triggering another rebuild of the same node.
The same stack discipline is used for binder-sensitive substitutions. Temporary mappings introduced while rewriting under a binder should stay local to that binder, typically via mim::Rewriter::push / mim::Rewriter::pop. This is the common pattern when rewriting dependent codomains under fresh variables, substituting a variable with a temporary representative, or rebuilding a body under newly introduced binder variables.
So the practical rule of thumb is:
If a rewrite replaces an external mutable with a fresh one, preserve the root explicitly by calling mim::Def::transfer_external or re-externalizing the replacement node.
Most rebuilds preserve debug metadata explicitly. The default Rewriter restores the old debug info with new_def->set(old_def->dbg()), and hand-written transformations commonly propagate names or add derived names with helpers such as mim::Def::debug_suffix.
If you create a replacement node that should still be recognizable in dumps, diagnostics, or generated code, preserve or adapt its debug payload immediately instead of treating naming as an afterthought.
Rebuild a term into another world:
VarRewriter is a specialized form of Rewriter for variable substitution. It extends the generic rewriting machinery with knowledge about Vars and binders. Instead of rebuilding everything indiscriminately, it only rewrites parts of the graph that may actually mention substituted variables.
A good summary is:
The main extra idea is that VarRewriter tracks which variables are currently relevant for substitution and uses free-variable information to decide whether a node needs to be rebuilt at all. If a subgraph cannot contain any of the substituted variables, it can be reused unchanged. This makes substitution much more efficient on large terms: unaffected subgraphs are skipped entirely.
Like Rewriter, VarRewriter is scope-aware. This is essential because substitution interacts with binders. When rewriting enters a mutable binder that introduces a variable, VarRewriter tracks that variable in the current scope. This allows it to reason correctly about which free variables are relevant in nested terms.
So VarRewriter is not just “replace Var with Def”. It is substitution that respects binder structure and avoids rebuilding irrelevant parts of the graph.
Substitute one variable with one argument:
Build it incrementally:
Conceptually, this rewrites only those parts of def whose free variables intersect the substituted variables.
VarRewriter is a specialization of Rewriter:
Rewriter provides the general graph-rebuilding machinery:
If you just want to rebuild IR, use Rewriter:
What is the corresponding version of this IR in another rewriting context?
VarRewriter adds substitution-specific behavior:
If you want to substitute variables while rebuilding, use VarRewriter:
What do I get if I substitute these variables and rebuild only where necessary?