Repository: BayesWitnesses/m2cgen (language: rust)

Issue:
Any plan for Rust generation?



Discussion / hints:
Will try to add Rust in a next release.

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index dcb8d8fa..d0a1a362 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -22,7 +22,7 @@ jobs:
           - 3.9
         lang:
           - "API"
-          - "c_lang or python or java or go_lang or javascript or php or haskell or ruby"
+          - "c_lang or python or java or go_lang or javascript or php or haskell or ruby or rust"
           - "c_sharp or visual_basic or f_sharp"
           - "r_lang"
           - "dart or powershell"
diff --git a/Dockerfile b/Dockerfile
index 5c16b2e3..e86d8155 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,6 +3,7 @@ FROM ubuntu:focal
 ARG python=3.9
 
 ENV JAVA_HOME=/usr/lib/jvm/zulu-8-amd64 \
+    PATH="/root/.cargo/bin:$PATH" \
     LANG=en_US.UTF-8 \
     LC_ALL=en_US.UTF-8 \
     TZ=Etc/UTC \
@@ -37,6 +38,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
     add-apt-repository "deb http://repos.azulsystems.com/ubuntu stable main" -y && \
     apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9 && \
     add-apt-repository "deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/" -y && \
+    curl https://sh.rustup.rs -sSf | sh -s -- --no-modify-path --default-toolchain stable -y && \
     apt-get update && \
     apt-get install --no-install-recommends -y \
         dart \
diff --git a/README.md b/README.md
index a333e732..a2bba683 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
 [![PyPI Version](https://img.shields.io/pypi/v/m2cgen.svg?logo=pypi&logoColor=white)](https://pypi.org/project/m2cgen)
 [![Downloads](https://pepy.tech/badge/m2cgen)](https://pepy.tech/project/m2cgen)
 
-**m2cgen** (Model 2 Code Generator) - is a lightweight library which provides an easy way to transpile trained statistical models into a native code (Python, C, Java, Go, JavaScript, Visual Basic, C#, PowerShell, R, PHP, Dart, Haskell, Ruby, F#).
+**m2cgen** (Model 2 Code Generator) - is a lightweight library which provides an easy way to transpile trained statistical models into a native code (Python, C, Java, Go, JavaScript, Visual Basic, C#, PowerShell, R, PHP, Dart, Haskell, Ruby, F#, Rust).
 
 * [Installation](#installation)
 * [Supported Languages](#supported-languages)
@@ -39,6 +39,7 @@ pip install m2cgen
 - Python
 - R
 - Ruby
+- Rust
 - Visual Basic (VBA-compatible)
 
 ## Supported Models
diff --git a/m2cgen/__init__.py b/m2cgen/__init__.py
index 1155edc1..a14ebc0f 100644
--- a/m2cgen/__init__.py
+++ b/m2cgen/__init__.py
@@ -15,6 +15,7 @@
     export_to_haskell,
     export_to_ruby,
     export_to_f_sharp,
+    export_to_rust,
 )
 
 __all__ = [
@@ -32,6 +33,7 @@
     export_to_haskell,
     export_to_ruby,
     export_to_f_sharp,
+    export_to_rust,
 ]
 
 with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
diff --git a/m2cgen/cli.py b/m2cgen/cli.py
index 793c12f3..1bdecdd9 100644
--- a/m2cgen/cli.py
+++ b/m2cgen/cli.py
@@ -32,6 +32,7 @@
     "haskell": (m2cgen.export_to_haskell, ["module_name", "indent", "function_name"]),
     "ruby": (m2cgen.export_to_ruby, ["indent", "function_name"]),
     "f_sharp": (m2cgen.export_to_f_sharp, ["indent", "function_name"]),
+    "rust": (m2cgen.export_to_rust, ["indent", "function_name"]),
 }
 
 
diff --git a/m2cgen/exporters.py b/m2cgen/exporters.py
index ede6ce94..4e7516d2 100644
--- a/m2cgen/exporters.py
+++ b/m2cgen/exporters.py
@@ -401,6 +401,30 @@ def export_to_f_sharp(model, indent=4, function_name="score"):
     return _export(model, interpreter)
 
 
+def export_to_rust(model, indent=4, function_name="score"):
+    """
+    Generates a Rust code representation of the given model.
+
+    Parameters
+    ----------
+    model : object
+        The model object that should be transpiled into code.
+    indent : int, optional
+        The size of indents in the generated code.
+    function_name : string, optional
+        Name of the function in the generated code.
+
+    Returns
+    -------
+    code : string
+    """
+    interpreter = interpreters.RustInterpreter(
+        indent=indent,
+        function_name=function_name,
+    )
+    return _export(model, interpreter)
+
+
 def _export(model, interpreter):
     assembler_cls = assemblers.get_assembler_cls(model)
     model_ast = assembler_cls(model).assemble()
diff --git a/m2cgen/interpreters/__init__.py b/m2cgen/interpreters/__init__.py
index 21275638..d0ef80da 100644
--- a/m2cgen/interpreters/__init__.py
+++ b/m2cgen/interpreters/__init__.py
@@ -12,6 +12,7 @@
 from m2cgen.interpreters.haskell.interpreter import HaskellInterpreter
 from m2cgen.interpreters.ruby.interpreter import RubyInterpreter
 from m2cgen.interpreters.f_sharp.interpreter import FSharpInterpreter
+from m2cgen.interpreters.rust.interpreter import RustInterpreter
 
 __all__ = [
     JavaInterpreter,
@@ -28,4 +29,5 @@
     HaskellInterpreter,
     RubyInterpreter,
     FSharpInterpreter,
+    RustInterpreter,
 ]
diff --git a/m2cgen/interpreters/powershell/code_generator.py b/m2cgen/interpreters/powershell/code_generator.py
index d1feb762..7cb5302f 100644
--- a/m2cgen/interpreters/powershell/code_generator.py
+++ b/m2cgen/interpreters/powershell/code_generator.py
@@ -6,7 +6,7 @@
 
 class PowershellCodeGenerator(CLikeCodeGenerator):
 
-    tpl_var_declare = CodeTemplate("{var_type}{var_name} = {init_val}")
+    tpl_var_declaration = CodeTemplate("{var_type}{var_name} = {init_val}")
     tpl_var_assignment = CodeTemplate("{var_name} = {value}")
     tpl_array_index_access = CodeTemplate("${array_name}[{index}]")
     tpl_return_statement = CodeTemplate("return {value}")
@@ -45,9 +45,9 @@ def get_var_name(self):
     def add_var_declaration(self, size):
         var_name = self.get_var_name()
         self.add_code_line(
-            self.tpl_var_declare(var_type=self._get_var_declare_type(size > 1),
-                                 var_name=var_name,
-                                 init_val="@(0.0)" if size > 1 else "0.0"))
+            self.tpl_var_declaration(var_type=self._get_var_declare_type(size > 1),
+                                     var_name=var_name,
+                                     init_val="@(0.0)" if size > 1 else "0.0"))
         return var_name
 
     def vector_init(self, values):
diff --git a/m2cgen/interpreters/rust/__init__.py b/m2cgen/interpreters/rust/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/m2cgen/interpreters/rust/code_generator.py b/m2cgen/interpreters/rust/code_generator.py
new file mode 100644
index 00000000..f14d6fdc
--- /dev/null
+++ b/m2cgen/interpreters/rust/code_generator.py
@@ -0,0 +1,34 @@
+import contextlib
+
+from m2cgen.interpreters.code_generator import CLikeCodeGenerator, CodeTemplate
+
+
+class RustCodeGenerator(CLikeCodeGenerator):
+
+    tpl_var_declaration = CodeTemplate("let {var_name}: {var_type};")
+    tpl_num_value = CodeTemplate("{value}_f64")
+    tpl_if_statement = CodeTemplate("if {if_def} {{")
+
+    scalar_type = "f64"
+    vector_type = "Vec<f64>"
+
+    def add_function_def(self, name, args, is_scalar_output):
+        func_args = ", ".join([
+            f"{n}: {self._get_var_declare_type(is_vector)}"
+            for is_vector, n in args])
+        return_type = self._get_var_declare_type(not is_scalar_output)
+        function_def = f"fn {name}({func_args}) -> {return_type} {{"
+        self.add_code_line(function_def)
+        self.increase_indent()
+
+    @contextlib.contextmanager
+    def function_definition(self, name, args, is_scalar_output):
+        self.add_function_def(name, args, is_scalar_output)
+        yield
+        self.add_block_termination()
+
+    def vector_init(self, values):
+        return (f"vec![{', '.join(values)}]")
+
+    def _get_var_declare_type(self, is_vector):
+        return self.vector_type if is_vector else self.scalar_type
diff --git a/m2cgen/interpreters/rust/interpreter.py b/m2cgen/interpreters/rust/interpreter.py
new file mode 100644
index 00000000..fa9e973f
--- /dev/null
+++ b/m2cgen/interpreters/rust/interpreter.py
@@ -0,0 +1,73 @@
+import os
+
+from m2cgen import ast
+from m2cgen.interpreters import mixins, utils
+from m2cgen.interpreters.rust.code_generator import RustCodeGenerator
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
+
+
+class RustInterpreter(ImperativeToCodeInterpreter,
+                      mixins.LinearAlgebraMixin):
+
+    supported_bin_vector_ops = {
+        ast.BinNumOpType.ADD: "add_vectors",
+    }
+
+    supported_bin_vector_num_ops = {
+        ast.BinNumOpType.MUL: "mul_vector_number",
+    }
+
+    abs_function_name = "f64::abs"
+    atan_function_name = "f64::atan"
+    exponent_function_name = "f64::exp"
+    logarithm_function_name = "f64::ln"
+    log1p_function_name = "f64::ln_1p"
+    power_function_name = "f64::powf"
+    sigmoid_function_name = "sigmoid"
+    softmax_function_name = "softmax"
+    sqrt_function_name = "f64::sqrt"
+    tanh_function_name = "f64::tanh"
+
+    with_sigmoid_expr = False
+    with_softmax_expr = False
+
+    def __init__(self, indent=4, function_name="score", *args, **kwargs):
+        self.function_name = function_name
+
+        cg = RustCodeGenerator(indent=indent)
+        super().__init__(cg, *args, **kwargs)
+
+    def interpret(self, expr):
+        self._cg.reset_state()
+        self._reset_reused_expr_cache()
+
+        with self._cg.function_definition(
+                name=self.function_name,
+                args=[(True, self._feature_array_name)],
+                is_scalar_output=expr.output_size == 1):
+            last_result = self._do_interpret(expr)
+            self._cg.add_return_statement(last_result)
+
+        current_dir = os.path.dirname(__file__)
+
+        if self.with_linear_algebra:
+            filename = os.path.join(current_dir, "linear_algebra.rs")
+            self._cg.add_code_lines(utils.get_file_content(filename))
+
+        if self.with_softmax_expr:
+            filename = os.path.join(current_dir, "softmax.rs")
+            self._cg.add_code_lines(utils.get_file_content(filename))
+
+        if self.with_sigmoid_expr:
+            filename = os.path.join(current_dir, "sigmoid.rs")
+            self._cg.add_code_lines(utils.get_file_content(filename))
+
+        return self._cg.finalize_and_get_generated_code()
+
+    def interpret_sigmoid_expr(self, expr, **kwargs):
+        self.with_sigmoid_expr = True
+        return super().interpret_sigmoid_expr(expr, **kwargs)
+
+    def interpret_softmax_expr(self, expr, **kwargs):
+        self.with_softmax_expr = True
+        return super().interpret_softmax_expr(expr, **kwargs)
diff --git a/m2cgen/interpreters/rust/linear_algebra.rs b/m2cgen/interpreters/rust/linear_algebra.rs
new file mode 100644
index 00000000..a8fcc13e
--- /dev/null
+++ b/m2cgen/interpreters/rust/linear_algebra.rs
@@ -0,0 +1,6 @@
+fn add_vectors(v1: Vec<f64>, v2: Vec<f64>) -> Vec<f64> {
+    return v1.iter().zip(v2.iter()).map(|(&x, &y)| x + y).collect::<Vec<f64>>();
+}
+fn mul_vector_number(v1: Vec<f64>, num: f64) -> Vec<f64> {
+    return v1.iter().map(|&i| i * num).collect::<Vec<f64>>();
+}
diff --git a/m2cgen/interpreters/rust/sigmoid.rs b/m2cgen/interpreters/rust/sigmoid.rs
new file mode 100644
index 00000000..99ed08dd
--- /dev/null
+++ b/m2cgen/interpreters/rust/sigmoid.rs
@@ -0,0 +1,7 @@
+fn sigmoid(x: f64) -> f64 {
+    if x < 0.0_f64 {
+        let z: f64 = x.exp();
+        return z / (1.0_f64 + z);
+    }
+    return 1.0_f64 / (1.0_f64 + (-x).exp());
+}
diff --git a/m2cgen/interpreters/rust/softmax.rs b/m2cgen/interpreters/rust/softmax.rs
new file mode 100644
index 00000000..8c3ad6c8
--- /dev/null
+++ b/m2cgen/interpreters/rust/softmax.rs
@@ -0,0 +1,11 @@
+fn softmax(x: Vec<f64>) -> Vec<f64> {
+    let size: usize = x.len();
+    let m: f64 = x.iter().fold(std::f64::MIN, |a, b| a.max(*b));
+    let mut exps: Vec<f64> = vec![0.0_f64; size];
+    let mut s: f64 = 0.0_f64;
+    for (i, &v) in x.iter().enumerate() {
+        exps[i] = (v - m).exp();
+        s += exps[i];
+    }
+    return exps.iter().map(|&i| i / s).collect::<Vec<f64>>();
+}


Test patch:
diff --git a/tests/e2e/executors/__init__.py b/tests/e2e/executors/__init__.py
index 0b8cc294..e27fcbfc 100644
--- a/tests/e2e/executors/__init__.py
+++ b/tests/e2e/executors/__init__.py
@@ -12,6 +12,7 @@
 from tests.e2e.executors.haskell import HaskellExecutor
 from tests.e2e.executors.ruby import RubyExecutor
 from tests.e2e.executors.f_sharp import FSharpExecutor
+from tests.e2e.executors.rust import RustExecutor
 
 __all__ = [
     JavaExecutor,
@@ -28,4 +29,5 @@
     HaskellExecutor,
     RubyExecutor,
     FSharpExecutor,
+    RustExecutor,
 ]
diff --git a/tests/e2e/executors/rust.py b/tests/e2e/executors/rust.py
new file mode 100644
index 00000000..f0fa2d2b
--- /dev/null
+++ b/tests/e2e/executors/rust.py
@@ -0,0 +1,66 @@
+import os
+import subprocess
+
+from m2cgen import assemblers, interpreters
+from tests import utils
+from tests.e2e.executors.base import BaseExecutor
+
+EXECUTOR_CODE_TPL = """
+use std::env;
+
+{model_code}
+
+fn main() {{
+    let args_raw: Vec<String> = env::args().collect::<Vec<String>>();
+    let args: Vec<f64> = args_raw[1..].iter().flat_map(|x| x.parse::<f64>()).collect::<Vec<f64>>();
+    {execute_code}
+    println!("{{}}", res);
+}}
+"""
+
+EXECUTE_SCALAR = """
+    let res: f64 = score(args);
+"""
+
+EXECUTE_VECTOR = """
+    let res_vec: Vec<f64> = score(args);
+    let res: String = res_vec.iter().map(|&x| x.to_string()).collect::<Vec<String>>().join(" ");
+"""
+
+
+class RustExecutor(BaseExecutor):
+
+    def __init__(self, model):
+        self.model_name = "score"
+        self.model = model
+        self.interpreter = interpreters.RustInterpreter()
+
+        assembler_cls = assemblers.get_assembler_cls(model)
+        self.model_ast = assembler_cls(model).assemble()
+
+        self.exec_path = None
+
+    def predict(self, X):
+        exec_args = [self.exec_path, *map(utils.format_arg, X)]
+        return utils.predict_from_commandline(exec_args)
+
+    def prepare(self):
+        if self.model_ast.output_size > 1:
+            execute_code = EXECUTE_VECTOR
+        else:
+            execute_code = EXECUTE_SCALAR
+
+        executor_code = EXECUTOR_CODE_TPL.format(
+            model_code=self.interpreter.interpret(self.model_ast),
+            execute_code=execute_code)
+
+        executor_file_name = os.path.join(self._resource_tmp_dir, f"{self.model_name}.rs")
+        with open(executor_file_name, "w") as f:
+            f.write(executor_code)
+        self.exec_path = os.path.join(self._resource_tmp_dir, self.model_name)
+        subprocess.call([
+            "rustc",
+            executor_file_name,
+            "-o",
+            self.exec_path
+        ])
diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py
index d46d3bec..97a05ca4 100644
--- a/tests/e2e/test_e2e.py
+++ b/tests/e2e/test_e2e.py
@@ -33,6 +33,7 @@
 HASKELL = pytest.mark.haskell
 RUBY = pytest.mark.ruby
 F_SHARP = pytest.mark.f_sharp
+RUST = pytest.mark.rust
 REGRESSION = pytest.mark.regr
 REGRESSION_WITH_MISSING_VALUES = pytest.mark.regr_missing_val
 CLASSIFICATION_WITH_MISSING_VALUES = pytest.mark.clf_missing_val
@@ -181,6 +182,7 @@ def classification_binary_random_w_missing_values(model, test_fraction=0.02):
         (executors.HaskellExecutor, HASKELL),
         (executors.RubyExecutor, RUBY),
         (executors.FSharpExecutor, F_SHARP),
+        (executors.RustExecutor, RUST),
     ],
 
     # These models will be tested against each language specified in the
diff --git a/tests/interpreters/test_rust.py b/tests/interpreters/test_rust.py
new file mode 100644
index 00000000..a6d5d0d8
--- /dev/null
+++ b/tests/interpreters/test_rust.py
@@ -0,0 +1,375 @@
+from m2cgen import ast
+from m2cgen.interpreters import RustInterpreter
+from tests import utils
+
+
+def test_if_expr():
+    expr = ast.IfExpr(
+        ast.CompExpr(ast.NumVal(1), ast.FeatureRef(0), ast.CompOpType.EQ),
+        ast.NumVal(2),
+        ast.NumVal(3))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    let var0: f64;
+    if (1.0_f64) == (input[0]) {
+        var0 = 2.0_f64;
+    } else {
+        var0 = 3.0_f64;
+    }
+    return var0;
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_bin_num_expr():
+    expr = ast.BinNumExpr(
+        ast.BinNumExpr(
+            ast.FeatureRef(0), ast.NumVal(-2), ast.BinNumOpType.DIV),
+        ast.NumVal(2),
+        ast.BinNumOpType.MUL)
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return ((input[0]) / (-2.0_f64)) * (2.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_dependable_condition():
+    left = ast.BinNumExpr(
+        ast.IfExpr(
+            ast.CompExpr(ast.NumVal(1),
+                         ast.NumVal(1),
+                         ast.CompOpType.EQ),
+            ast.NumVal(1),
+            ast.NumVal(2)),
+        ast.NumVal(2),
+        ast.BinNumOpType.ADD)
+
+    right = ast.BinNumExpr(ast.NumVal(1), ast.NumVal(2), ast.BinNumOpType.DIV)
+    bool_test = ast.CompExpr(left, right, ast.CompOpType.GTE)
+
+    expr = ast.IfExpr(bool_test, ast.NumVal(1), ast.FeatureRef(0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    let var0: f64;
+    let var1: f64;
+    if (1.0_f64) == (1.0_f64) {
+        var1 = 1.0_f64;
+    } else {
+        var1 = 2.0_f64;
+    }
+    if ((var1) + (2.0_f64)) >= ((1.0_f64) / (2.0_f64)) {
+        var0 = 1.0_f64;
+    } else {
+        var0 = input[0];
+    }
+    return var0;
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_nested_condition():
+    left = ast.BinNumExpr(
+        ast.IfExpr(
+            ast.CompExpr(ast.NumVal(1),
+                         ast.NumVal(1),
+                         ast.CompOpType.EQ),
+            ast.NumVal(1),
+            ast.NumVal(2)),
+        ast.NumVal(2),
+        ast.BinNumOpType.ADD)
+
+    bool_test = ast.CompExpr(ast.NumVal(1), left, ast.CompOpType.EQ)
+
+    expr_nested = ast.IfExpr(bool_test, ast.FeatureRef(2), ast.NumVal(2))
+
+    expr = ast.IfExpr(bool_test, expr_nested, ast.NumVal(2))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    let var0: f64;
+    let var1: f64;
+    if (1.0_f64) == (1.0_f64) {
+        var1 = 1.0_f64;
+    } else {
+        var1 = 2.0_f64;
+    }
+    if (1.0_f64) == ((var1) + (2.0_f64)) {
+        let var2: f64;
+        if (1.0_f64) == (1.0_f64) {
+            var2 = 1.0_f64;
+        } else {
+            var2 = 2.0_f64;
+        }
+        if (1.0_f64) == ((var2) + (2.0_f64)) {
+            var0 = input[2];
+        } else {
+            var0 = 2.0_f64;
+        }
+    } else {
+        var0 = 2.0_f64;
+    }
+    return var0;
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_raw_array():
+    expr = ast.VectorVal([ast.NumVal(3), ast.NumVal(4)])
+
+    expected_code = """
+fn score(input: Vec<f64>) -> Vec<f64> {
+    return vec![3.0_f64, 4.0_f64];
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_multi_output():
+    expr = ast.IfExpr(
+        ast.CompExpr(
+            ast.NumVal(1),
+            ast.NumVal(1),
+            ast.CompOpType.EQ),
+        ast.VectorVal([ast.NumVal(1), ast.NumVal(2)]),
+        ast.VectorVal([ast.NumVal(3), ast.NumVal(4)]))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> Vec<f64> {
+    let var0: Vec<f64>;
+    if (1.0_f64) == (1.0_f64) {
+        var0 = vec![1.0_f64, 2.0_f64];
+    } else {
+        var0 = vec![3.0_f64, 4.0_f64];
+    }
+    return var0;
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_bin_vector_expr():
+    expr = ast.BinVectorExpr(
+        ast.VectorVal([ast.NumVal(1), ast.NumVal(2)]),
+        ast.VectorVal([ast.NumVal(3), ast.NumVal(4)]),
+        ast.BinNumOpType.ADD)
+
+    expected_code = """
+fn score(input: Vec<f64>) -> Vec<f64> {
+    return add_vectors(vec![1.0_f64, 2.0_f64], vec![3.0_f64, 4.0_f64]);
+}
+fn add_vectors(v1: Vec<f64>, v2: Vec<f64>) -> Vec<f64> {
+    return v1.iter().zip(v2.iter()).map(|(&x, &y)| x + y).collect::<Vec<f64>>();
+}
+fn mul_vector_number(v1: Vec<f64>, num: f64) -> Vec<f64> {
+    return v1.iter().map(|&i| i * num).collect::<Vec<f64>>();
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_bin_vector_num_expr():
+    expr = ast.BinVectorNumExpr(
+        ast.VectorVal([ast.NumVal(1), ast.NumVal(2)]),
+        ast.NumVal(1),
+        ast.BinNumOpType.MUL)
+
+    expected_code = """
+fn score(input: Vec<f64>) -> Vec<f64> {
+    return mul_vector_number(vec![1.0_f64, 2.0_f64], 1.0_f64);
+}
+fn add_vectors(v1: Vec<f64>, v2: Vec<f64>) -> Vec<f64> {
+    return v1.iter().zip(v2.iter()).map(|(&x, &y)| x + y).collect::<Vec<f64>>();
+}
+fn mul_vector_number(v1: Vec<f64>, num: f64) -> Vec<f64> {
+    return v1.iter().map(|&i| i * num).collect::<Vec<f64>>();
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_abs_expr():
+    expr = ast.AbsExpr(ast.NumVal(-1.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return f64::abs(-1.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_exp_expr():
+    expr = ast.ExpExpr(ast.NumVal(1.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return f64::exp(1.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_pow_expr():
+    expr = ast.PowExpr(ast.NumVal(2.0), ast.NumVal(3.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return f64::powf(2.0_f64, 3.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_sqrt_expr():
+    expr = ast.SqrtExpr(ast.NumVal(2.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return f64::sqrt(2.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_tanh_expr():
+    expr = ast.TanhExpr(ast.NumVal(2.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return f64::tanh(2.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_log_expr():
+    expr = ast.LogExpr(ast.NumVal(2.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return f64::ln(2.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_log1p_expr():
+    expr = ast.Log1pExpr(ast.NumVal(2.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return f64::ln_1p(2.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_atan_expr():
+    expr = ast.AtanExpr(ast.NumVal(2.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return f64::atan(2.0_f64);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_softmax_expr():
+    expr = ast.SoftmaxExpr([ast.NumVal(2.0), ast.NumVal(3.0)])
+
+    expected_code = """
+fn score(input: Vec<f64>) -> Vec<f64> {
+    return softmax(vec![2.0_f64, 3.0_f64]);
+}
+fn softmax(x: Vec<f64>) -> Vec<f64> {
+    let size: usize = x.len();
+    let m: f64 = x.iter().fold(std::f64::MIN, |a, b| a.max(*b));
+    let mut exps: Vec<f64> = vec![0.0_f64; size];
+    let mut s: f64 = 0.0_f64;
+    for (i, &v) in x.iter().enumerate() {
+        exps[i] = (v - m).exp();
+        s += exps[i];
+    }
+    return exps.iter().map(|&i| i / s).collect::<Vec<f64>>();
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_sigmoid_expr():
+    expr = ast.SigmoidExpr(ast.NumVal(2.0))
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    return sigmoid(2.0_f64);
+}
+fn sigmoid(x: f64) -> f64 {
+    if x < 0.0_f64 {
+        let z: f64 = x.exp();
+        return z / (1.0_f64 + z);
+    }
+    return 1.0_f64 / (1.0_f64 + (-x).exp());
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_reused_expr():
+    reused_expr = ast.ExpExpr(ast.NumVal(1.0), to_reuse=True)
+    expr = ast.BinNumExpr(reused_expr, reused_expr, ast.BinNumOpType.DIV)
+
+    expected_code = """
+fn score(input: Vec<f64>) -> f64 {
+    let var0: f64;
+    var0 = f64::exp(1.0_f64);
+    return (var0) / (var0);
+}
+"""
+
+    interpreter = RustInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)