Elvelt

Frontends

Rust Frontend

Rust Frontend

The Rust frontend is a BraceFrontendBase subclass delivered as a single 1,091-line header. It implements the most distinctive Rust features via a pipeline of source-level rewrites that run before the brace parser sees the source:

Rewrite Function Lines
match expressions rewriteMatchExpressions 254-357
if let expressions rewriteIfLetExpressions 359-415
let-else rewriteLetElseExpressions 426-518
if let chains rewriteIfLetChains 584-756

RustFrontend::parse (line 12) applies all four in order before delegating to BraceFrontendBase::parse.

File Lines Purpose
frontends/rust/RustFrontend.h 1,091 Frontend class with all method bodies inline (single header)
frontends/rust/RustStdLib.h 61 findStdLibMapping for println!, eprintln!, std::process::exit, std::fs::*, std::env::*

Language registration

// RustFrontend.h:9-10
std::string languageName() const override { return "rust"; }
std::vector<std::string> fileExtensions() const override { return {".rs"}; }

Only .rs is recognised.


// RustFrontend.h:918-923
return {
    {"println!", true, true},
    {"print!", true, false}
};

Features (file:line)

Variables: let / let mut / const / static

tryParseVarDecl (line 929) recognises:

let x = 42;            // VariableDeclNode(name="x", isConst=false)
let mut x = 42;        // VariableDeclNode(name="x", isConst=false, metadata["mut"]="true")
const X: i32 = 42;     // VariableDeclNode(name="X", isConst=true, metadata["rust_const"]="true")
static Y: i32 = 42;    // VariableDeclNode(name="Y", isConst=false, metadata["rust_static"]="true")
let (a, b) = (1, 2);   // Tuple destructuring — emitted as separate VariableDeclNodes
let Point { x, y } = p; // Struct destructuring — fields become separate VariableDeclNodes

Functions: fn / pub fn / async fn / unsafe fn / const fn

tryParseFunctionDeclHeader (line 946) recognises:

fn add(a: i32, b: i32) -> i32 { a + b }
pub fn add(...) -> i32 { ... }     // metadata["rust_pub"]="true"
async fn fetch(...) -> ... { ... } // metadata["async"]="true"
unsafe fn danger() { ... }          // metadata["unsafe"]="true"
const fn compile_time() { ... }     // metadata["const_fn"]="true"
fn new() -> Self { ... }            // Associated function (no receiver in name)
fn from(self) -> ... { ... }        // Method taking self by value
fn borrow(&self) -> ... { ... }     // Method borrowing self
fn mut_take(&mut self) { ... }      // Method mutably borrowing self

Receiver detection sets metadata["rust_self_kind"]="value"|"ref"|"ref_mut".

impl [Trait for] Type { … }

tryParseLanguageSpecificStatement (line 1054) recognises impl blocks:

impl Point {
    fn new(x: i32, y: i32) -> Point { ... }
}
impl Display for Point {
    fn fmt(&self, f: &mut Formatter) -> Result { ... }
}

Both lower to ClassDeclNode(name="<TargetType>", metadata["rust_impl"]="true").

struct Name { … } / struct Name; / struct Name(T);

tryParseLanguageSpecificStatement (line 1029) handles:

Form IR
struct Point { x: i32, y: i32 } ClassDeclNode(name="Point", isStruct=true)
struct Point; ClassDeclNode(name="Point", isStruct=true, metadata["rust_unit_struct"]="true")
struct Point(i32, i32); ClassDeclNode(name="Point", isStruct=true, metadata["rust_tuple_struct"]="true")

enum Name { … }

Line 1005. Standard enum with variants (tuple, struct, unit) all lower to EnumDeclNode(name=…) with each variant becoming a member.

trait Name { … }

Line 1042. Recognised; lowers to InterfaceDeclNode(name=…). Default method bodies are parsed but stored as method bodies on the interface.

use module::Item; / use module::*;

Line 987. Lowers to ImportNode(module="module::Item").

mod module_name;

Line 996. Treats the module as an ImportNode(module="module_name"). The actual module body is loaded separately by the CLI driver.

macro_rules! name { … }

Line 1072. Detected; lowers to FunctionDeclNode(name="…", metadata["macro_rules"]="true").

Match expressions

rewriteMatchExpressions (line 254-357) translates match expr { pat => body, … } to switch (expr) { case pat: body; … default: ; } so the brace parser handles it.

match n {
    0 => "zero",
    1 | 2 => "small",
    _ => "big",
}

Lowers to:

switch (n) {
    case 0: "zero";
    case 1: "small";
    case 2: "small";
    default: "big";
}

Or-patterns (1 | 2) are expanded into multiple case entries. Wildcard _ becomes default.

If let chains

rewriteIfLetChains (line 584-756) handles if let … = … && let … = … and the alternative pattern else if let:

if let Some(x) = a && let Some(y) = b && x != y {
    do(x, y)
} else {
    fallback()
}

Lowers to nested if statements:

{
    let __tmp_a = a;
    if (__tmp_a is_some) {
        let x = __tmp_a.unwrap();
        let __tmp_b = b;
        if (__tmp_b is_some) {
            let y = __tmp_b.unwrap();
            if (x != y) {
                do(x, y)
            } else {
                fallback()
            }
        } else {
            fallback()
        }
    } else {
        fallback()
    }
}

If let expressions

rewriteIfLetExpressions (line 359-415) handles the expression form if let Pat = expr { … } else { … } (used as an expression value):

let result = if let Some(x) = opt { x + 1 } else { 0 };

Lowers to:

{
    let __result;
    if (let_rewritten) {
        __result = x + 1;
    } else {
        __result = 0;
    }
    __result
}

Let-else

rewriteLetElseExpressions (line 426-518) handles let Pat = expr else { diverge; };:

let Some(x) = opt else {
    return;
};

Lowers to:

let Some(x) = opt;

The diverge body is dropped (with a comment at line 421 noting the limitation for destructuring patterns). See limitations.md.

async fn / .await

async fn sets metadata["async"]="true". .await expressions are parsed by the brace parser's expression parser, emitting AwaitNode.

gen blocks / yield

yield and gen blocks (Rust nightly, MCP 2024) are not yet supported beyond recognising yield expr; as a YieldNode (inherited from BraceFrontendBase).


Lifetimes

Lifetimes (<'a>, &'a T, struct Foo<'a>, …) are passed through to the C++ backend. The frontend doesn't currently enforce lifetime correctness (Rust's borrow checker is not implemented). See limitations.md.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

The lifetime annotations are stored in metadata["rust_lifetimes"]="a" on the function declaration, but the compiler doesn't validate them.


Ownership and borrowing

Ownership (move) and borrowing (&, &mut) are type-erased at the IR level. The frontend doesn't enforce ownership rules — it's treated as a syntactic transformation only.


Closures

Closures (|x| x + 1, move |x| x + 1, || { … }) are parsed by BraceFrontendBase's expression parser. move closures set metadata["rust_move_closure"]="true".


Async / await

async fn (line 949) sets metadata["async"]="true". await is parsed as an expression by BraceFrontendBase. async {} block expressions and async move {} are recognised.


Standard library mapping (RustStdLib.h)

The complete table at frontends/rust/RustStdLib.h:38-50:

Rust MPL module.function
println!(...) Console.WriteLine(...)
print!(...) Console.Write(...)
eprintln!(...) Console.ErrorWriteLine(...)
eprint!(...) Console.ErrorWrite(...)
std::process::exit(code) Process.Exit(code)
std::env::var(name) Environment.GetVariable(name)
std::env::current_dir() Environment.GetCurrentDirectory()
std::fs::read_to_string(path) IO.ReadFileText(path)
std::fs::write(path, contents) IO.WriteFileText(path, contents)

IR examples

Example 1 — match expression

fn describe(n: i32) -> &'static str {
    match n {
        0 => "zero",
        1 | 2 => "small",
        _ => "big",
    }
}

After rewriteMatchExpressions, the source becomes:

fn describe(n: i32) -> &'static str {
    switch (n) {
        case 0: "zero";
        case 1: "small";
        case 2: "small";
        default: "big";
    }
}

Lowers to:

FunctionDeclNode(name="describe", parameters=[("auto","n")], returnType="&'static str")
  body:
    SwitchNode(expr=IdentifierNode("n"))
      cases:
        case 0: body = "zero"
        case 1: body = "small"
        case 2: body = "small"
      default: body = "big"

Example 2 — if let chain

fn process(opt_a: Option<i32>, opt_b: Option<i32>) -> i32 {
    if let Some(x) = opt_a && let Some(y) = opt_b && x != y {
        x + y
    } else {
        -1
    }
}

After rewriteIfLetChains, the body is a sequence of nested ifs with binding extraction.

Example 3 — let-else

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    let Some(i) = s.find(' ') else {
        return &s[..];
    };
    &s[..i]
}

let Some(i) = s.find(' ') else { return &s[..]; }; becomes:

let Some(i) = s.find(' ');

with the else { return &s[..]; } branch dropped (limitation).


What is not supported (Rust-specific gaps)

  • Borrow checker: lifetimes are not validated. The compiler will accept &'a T returning a different reference than T with no complaint.
  • Trait object dispatch: dyn Trait syntax passes through but isn't lowered to a virtual table.
  • Generic type parameters: <T: Bound> are stripped to the type identifier.
  • Destructuring patterns in let-else: diverge body is dropped.
  • Async traits (async fn in traits): not modelled.
  • Gen blocks (gen {}): not yet supported beyond yield expr.
  • impl Trait return types: stripped.
  • ? operator: not yet lowered to a Try-step call.
  • Const generics: <const N: usize>: stripped.
  • Where clauses: where T: Bound are dropped.
  • Visibility modifiers other than pub (pub(crate), pub(super), pub(in path)): dropped.