Skip to content

Tehforsch/molt

Repository files navigation

Molt

Molt is a syntax-aware search and replace tool for Rust. Molt is experimental software.

Overview

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.

Examples

Replace method call

Suppose we have a main.rs containing:

fn main() {
    let x = foo.get("key").insert("value");
    let y = bar.get_map().get(CustomEnum::Key).insert(10);
}

Suppose we want to replace .get(key).insert(val) with a single method call insert_at_key(key, val). We write the following molt file:

merge_fns.molt:

fn main(input: Expr) {
    let key: Expr;
    let val: Expr;
    let expr: Expr;
    let { $expr.get($key).insert($val) } = input;
    input -> { $expr.insert_at_key($key, $val) };
}

Running molt merge_fns.molt in the rust project 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);
}

Conditionally rewrite statements

Molt being aims to make it easy to express complex replacement logic. For example, the following rule rewrites foo(); statements inside functions named do_something, while leaving other foo(); statements untouched.

fn main(input: Fn) {
    if let { do_something } = input.name {
        for stmt in input.stmts {
            if let { foo(); } = stmt {
                stmt -> { bar(); };
            }
        }
    }
}

Input Rust:

fn do_something() {
    foo();
    some_other_fn();
    let x = 3;
    foo();
}

fn some_other_fn() {
    foo();
}

After running Molt:

fn do_something() {
    bar();
    some_other_fn();
    let x = 3;
    bar();
}

fn some_other_fn() {
    foo();
}

List matching and iteration

Molt can bind a list of syntax nodes and then inspect or rewrite each element.

This rule matches an array behind a reference, prints the matched list, and replaces each element with 5:

fn main(input: Expr) {
    let list: List<Expr>;
    let { [$list] } = input;
    print(list);
    for item in list {
        print(item);
        item -> { 5 };
    }
}

Input Rust:

const X: &[usize] = &[1, 2, 3];

After running Molt:

const X: &[usize] = &[5, 5, 5];

Printed output:

[1, 2, 3]
1
2
3

Installation

To install, use cargo install:

cargo install --git https://github.com/tehforsch/molt

This will install the molt binary in your Cargo bin directory (usually ~/.cargo/bin).

Molt Syntax

A Molt file is a small program that runs against Rust syntax nodes.

Program entry point

Molts entry point is the main function, which takes a syntax node as an argument

fn main(input: Expr) { }

Alternative program entry point

Alternatively, the main function can be omitted, but a input variable needs to be present and its type needs to be annotated:

let input: Expr;

Pattern matching with let

Patterns are written as Rust syntax inside { }. Patterns can be matched/destructured with the let statement, which takes a pattern on the left-hand side and a syntax node expression on the right-hand side:

let input: Expr;
let x: Expr;
let y: Expr;
let { $x + $y } = input;

The let statement checks for matches against the right hand side and binds the LHS variables to the matched nodes if a match was found. If no match was found, the let statement implicitly returns from the current function, so none of the checks below the let statement will take place if a single let fails.

Pattern matching with if let

let input: Expr;
let value: Expr;
if let { Some($value) } = input {
    print(value);
}

Binding variables in patterns

Variables inside Rust patterns are referenced with $name. If the variable has not been declared yet, a type annotation which declares its syntactic kind needs to be present before the match:

let input: Expr;
let receiver: Expr;
let key: Expr;
if let { $receiver.get($key) } = input {
    print(key);
}

Rewrites

Use target -> { replacement }; to replace a matched syntax node:

let input: Expr;
let arg: Expr;
if let { old_name($arg) } = input {
    input -> { new_name($arg) };
}

Field access

Some syntactic nodes offer their sub-nodes via field access:

let input: Fn;
if let { foo } = input.name {
    input.generics -> { <T> };
}

Lists and for loops

Use List<T> to bind multiple syntax nodes at the same level. The for loop iterates over items of a list.

fn main(input: Fn) {
    let args: List<FnArg>;
    let name: Ident;
    let stmts: List<Stmt>;
    let {
        fn $name($args) {
            $stmts
        }
    } = input;

    for arg in args {
        print(arg);
    }
}

print

Molt currently has a small set of builtin functions.

print simply prints the given variable / syntax node:

let input: Fn;
print(input.name);
fn example() {}
example

dbg

The dbg builtin prints the given variable along with some surrounding context

let input: Fn;
dbg(input.name);
fn example() {}
note: 
  ┌─ test input:1:4
  │
1 │ fn example() {}
  │    ^^^^^^^

Syntactic kinds

The following kinds are currently supported.

Ident - Identifiers

Matches any identifier.

let input: Ident;
print(input);
fn main() {
    let x = 3;
}
main
x

Lit - Literals

Matches any literal value.

let input: Lit;
print(input)
fn main() {
    let x = 3;
    let y = "foo";
}
3
"foo"

Expr - Expressions

Matches any Rust expression. Note that this will often recursively expressions along with their subexpressions:

let input: Expr;
print(input);
fn main() {
    let x = 3 + 3;
}
3
3
3 + 3

Stmt - Statements

Matches any Rust statement. Examples:

let input: Stmt;
if let { let foo = 3; } = input {
    input -> { let foo = 4; };
}
fn main() {
    let foo = 3;
    let bar = 5;
}
fn main() {
    let foo = 4;
    let bar = 5;
}

Type - Types

Matches any type. Examples:

fn main(input: Type) {
    let ty: Type;
    if let { Vec<$ty> } = input {
        input -> { Box<[$ty]> };
    }
}

Pat - Patterns

Matches any rust pattern in match expressions or destructuring. Examples:

let input: Pat;
let pat: Pat;
if let { Some($pat) } = input {
    input -> { $pat };
}
fn main() {
    if let Some("bar") = foo() {
        println!("baz");
    }
}
fn main() {
    if let "bar" = foo() {
        println!("baz");
    }
}

Arm - Match Arms

Matches match expression arms. Examples:

let input: Arm;
let pat: Pat;
let val: Expr;
if let { $pat => Ok($val) } = input {
    input.body -> { $val };
}
fn main() {
    match x {
        MyEnum::A => Ok(1),
        MyEnum::B => Ok(2),
        MyEnum::C => Ok(3),
    }
}
fn main() {
    match x {
        MyEnum::A => 1,
        MyEnum::B => 2,
        MyEnum::C => 3,
    }
}

Field - Struct Fields

Matches struct field definitions. Examples:

let input: Field;
print(input);
struct Foo {
    a: A,
    b: B,
    c: C,
}
a: A
b: B
c: C

Vis - Visibility Modifiers

Matches visibility modifiers. Examples:

let input: Vis;
if let { pub } = input {
    input -> { pub(crate) };
}
pub fn foo() { }

pub(crate) fn bar() { }
pub(crate) fn foo() { }

pub(crate) fn bar() { }

Generics - Generic Parameters

Matches generic parameter lists. Examples:

let input: Generics;
print(input);
fn foo<A, B, C: std::fmt::Debug>() { }
<A, B, C: std::fmt::Debug>

Item - Top-Level Items

Matches top-level Rust items such as functions, trait definitions, use statements, constants. Examples:

let input: Item;
print(input);
const X: usize = 3;

fn foo() { }
const X: usize = 3;
fn foo() { }

Attribution

The Rust parser for this project is adapted from syn.

About

Automatic syntax-aware search and replace for Rust

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors