Molt is a syntax-aware search and replace tool for Rust. Molt is experimental software.
Molt provides a simple pattern-matching language for the Rust programming language. It's particularly useful for mechanical refactorings, API migrations and pattern finding across large code bases.
Suppose you have a main.rs containing:
fn main() {
let x = foo.get("key").insert("value");
let y = bar.get_map().get(CustomEnum::Key).insert(10);
}Also suppose that we want to use a different API that simplifies having to do .get(key).insert(val) with a single method call insert_at_key(key, val). To do so, we write a molt file:
merge_fns.molt:
let key, val, expr: Expr;
let ident: Ident;
let old: Expr = $expr.get($key).insert($val);
let new: Expr = $expr.insert_at_key($key, $val);
transform old -> new;Running molt merge_fns.molt in the rust project containing main.rs will result in:
main.rs, after:
fn main() {
let x = foo.insert_at_key("key", "value");
let y = bar.get_map().insert_at_key(CustomEnum::Key, 10);
}A lot of times, simply matching for syntactic patterns is not enough, and we need to add type annotations:
let ident: Ident;
type ident = Foo; // Only match variables with type `Foo`
let old: Expr = $ident.foo();
let new: Expr = $ident.bar();
transform old -> new;This works, but is currently implemented by asking rust-analyzer via a "hover request" to obtain the type of the identifier. Unsurprisngly, this turns out to be quite fragile since the info rust-analyzer returns is meant for human consumption. Therefore, this will have to be replaced by directly using rust-analyzer or rustc bindings.
Molt can match multiple items at the same syntactic levels at once:
let expr, val: Expr;
let pat: Pat;
let input: Expr = match $expr {$($arm_in)*};
let arm_in: Arm = { $pat => Ok($val) };Here, the regex-like $($arm_in)* syntax means any amount of patterns that look like arm_in.
Simply matching patterns in this way works, but transforming them is not implemented currently. The goal is to be able to write something like
let expr, val: Expr;
let pat: Pat;
let input: Expr = match $expr { $($arm_in)* };
let arm_in: Arm = { $pat => Ok($val) };
let output: Expr = match Ok($expr { $($arm_out)* });
let arm_out: Arm = { $pat => $val };
transform input -> output;The machinery to do this exists, but transforming the output requires carefully analyzing the "tree" of transformations and doing them in the right order, which is not implemented yet.
Molt is not currently published on crates.io. To install, clone the repository and use cargo install:
git clone https://github.com/tehforsch/molt.git
cd molt
cargo install --path .This will install the molt binary in your Cargo bin directory (usually ~/.cargo/bin).
A molt file consists of variable declarations and commands.
Variables are declared using let statements with a kind annotation (see below for the different syntax kinds):
let var_name: Kind;Variables can also be assigned a pattern to match against:
let var_name: Kind = <pattern>;
// Some patterns, such as most statements need to be enclosed in `{` `}` to be parsed correctly
let var_name: Kind = { <pattern> };Multiple variables of the same type can be declared together.
let var1, var2, var3: Kind;Patterns are written as Rust code that specifies the form of the pattern to match. The given Rust code needs to match the declared syntax kind.
let expr: Expr = 5; // Valid, `5` is an expression.
let expr: Ident = 5; // Invalid, `5` is not an identifier.Other variables can be referenced within patterns by prepending the name of the variable with a $:
let expr: Expr;
let pattern: Expr = foo($expr);Using a variable in this way can do one of two things:
- If the variable is declared using an explicit pattern, the matcher will check that the concrete code we are looking at matches the pattern.
- If the variable is declared without an explicit pattern, the matcher will bind that variable to the concrete syntactic expression in place of the variable. If the variable occurs multiple times, for example in
let expr: Expr;
let pattern: Expr = $expr + $expr;then the matcher will make sure that the second occurence matches the pattern bound to the variable by the first occurence.
Use $($var)* syntax to match zero or more repetitions:
let arm: Arm;
let pattern: Expr = match expr { $($arm)* };Molt supports two main commands:
The match command searches for patterns in the code without modifying it:
let expr: Expr;
let pattern: Expr = foo($expr);
match pattern;Running molt on this molt file searches for all occurrences of pattern and prints them.
In most cases, match can be omitted since the match variable can be inferred, so this would be equivalent:
let expr: Expr;
let pattern: Expr = foo($expr);The transform command searches for a pattern and replaces it with another:
let old: Expr = <old_pattern>;
let new: Expr = <new_pattern>;
transform old -> new;Use ignore to ignore pattern matching for all occurences of a specific syntactic kind:
ignore Vis;The motivation for this is that in some cases, specifying a pattern in its most general form can be tedious. For example, assume we want to match on a constant declaration:
let ident: Ident;
let expr: Expr;
let c: Stmt = {
const $ident: Foo = Foo::new($expr);
};Due to a mismatch in visibility, this pattern would not match on a constant declared like this:
pub const X: Foo = Foo::new(5);We can fix this by matching on any kind of visibility explicitly:
let ident: Ident;
let expr: Expr;
let vis: Vis;
let c: Stmt = {
$vis const $ident: Foo = Foo::new($expr);
};However, this is easy to forget and tedious. ignore exists for this use case:
let ident: Ident;
let expr: Expr;
ignore Vis;
let c: Stmt = {
const $ident: Foo = Foo::new($expr);
};Visibilities can also be ignored/set to strict only for specific kinds:
strict Vis;
ignore Vis(Const, Trait); // Only ignore visibilities in constant declarations and traits.strict is the opposite of ignore. It has no use currently, since strict is currently the default for all visibilities, something that might be changed in the future.
The following kinds are currently supported.
Matches any identifier.
let input: Ident = some_variable_name;Matches any literal value.
let lit: Lit = "foo"; // A String
let lit: Lit = 5; // A NumberMatches any Rust expression. Examples:
let e: Expr;
let pattern: Expr = $e.bla(); // Method calls
let pattern: Expr = $e.bar; // Field access
let e1, e2, e3: Expr;
let pattern: Expr = (-$e1) + ($e2 * $e3); // Unary, binary, parentheses
// If-else expressions
let cond, then, else_: Expr;
let pattern: Expr = {
if $cond {
$then
} else {
$else_
}
};Matches any Rust statement. Examples:
// Let bindings
let stmt: Stmt = { let foo = 3; };
// Nested statements
let stmt: Stmt;
let fn: Stmt = fn inner() { $stmt };Matches any type. Examples:
let ty: Type;
let pattern: Type = [$ty; 3]; // Arrays
let pattern: Type = ( $ty, $ty ); // Tuples
let pattern: Type = Foo<$ty>; // GenericsMatches any pattern in match expressions or destructuring. Examples:
let pat: Pat = Some(_);
let pat1, pat2: Pat;
let pattern: Pat = $pat1 | $pat2; // Or-patterns
let pattern: Pat = ($pat1, $pat2); // TuplesMatches match expression arms. Examples:
let pat: Pat;
let val: Expr;
let arm: Arm = { $pat => Ok($val) };Matches struct field definitions. Examples:
let ident: Ident;
let type: Type;
let x: Field = $ident: $type;Matches visibility modifiers. Examples:
let vis: Vis;
let ident: Ident;
let pattern: Item = $vis fn $ident() {};Matches generic parameter lists. Examples:
let ty: Type;
let generics: Generics = < $ty >;
let pattern: Item = fn process $generics (input: $ty) -> $ty {
input.clone()
};Matches top-level Rust items. Examples:
let vis: Vis;
let ident: Ident;
let pattern: Item = $vis fn $ident() {}; // Functions
let pattern: Item = $vis const $ident(): usize = 5; // Constants
let pattern: Item = { $vis mod $ident { } }; // ModulesThe Rust parser for this project is adapted from syn.