diff --git a/test/unit-modcc/CMakeLists.txt b/test/unit-modcc/CMakeLists.txt
index 813c03ff32ae7b6ca6c32a7bc0b0a8097a1d4410..5f000613bf73505c526f966cd610647b995a6796 100644
--- a/test/unit-modcc/CMakeLists.txt
+++ b/test/unit-modcc/CMakeLists.txt
@@ -1,5 +1,6 @@
 set(unit-modcc_sources
     # unit tests
+    test_fn_rewriters.cpp
     test_lexer.cpp
     test_kinetic_rewriter.cpp
     test_module.cpp
diff --git a/test/unit-modcc/test_fn_rewriters.cpp b/test/unit-modcc/test_fn_rewriters.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..77a4cab848837d5616e781fa2f01d30fc632c761
--- /dev/null
+++ b/test/unit-modcc/test_fn_rewriters.cpp
@@ -0,0 +1,455 @@
+#include <cmath>
+#include <memory>
+
+#include "common.hpp"
+
+#include "expression.hpp"
+#include "functionexpander.hpp"
+#include "functioninliner.hpp"
+#include "module.hpp"
+#include "parser.hpp"
+#include "scope.hpp"
+
+// Test FunctionCallLowerer
+
+using symbol_map = scope_type::symbol_map;
+
+struct symbol_map_store {
+    std::vector<std::unique_ptr<symbol_map>> keepies;
+
+    scope_ptr make_scope() {
+        keepies.push_back(std::make_unique<symbol_map>());
+        return std::make_shared<scope_type>(*keepies.back());
+    }
+} sym_store;
+
+struct mock_definitions {
+    std::vector<std::string> globals = {
+        "a", "b", "c"
+    };
+
+    std::vector<std::string> functions = {
+        "FUNCTION f(p, q, r) { f = (q-p)/(r-p) }",
+        "FUNCTION g(a, b) { g = a+b }",
+        "FUNCTION h(x) { h = exp(x) }",
+        "FUNCTION long(x) {\n"
+        "    LOCAL y, z\n"
+        "    y = h(x)\n"
+        "    z = g(y, x)\n"
+        "    if (y>z) { long = f(x, 3, z)\n }\n"
+        "    else { long = h(z)\n }\n"
+        "}",
+        "FUNCTION assign2(x) {\n"
+        "    assign2 = x * 2\n"
+        "    if (x<2) { assign2 = 2\n }\n"
+        "}",
+        "FUNCTION shadow(x) {\n"
+        "    LOCAL x\n"
+        "    x = 2\n"
+        "    shadow = 1\n"
+        "}"
+    };
+
+    std::vector<std::string> procedures = {
+        "PROCEDURE p0() { a = 3 }",
+        "PROCEDURE p1(x) { a = x }",
+        "PROCEDURE p2(x, y) { a = g(x, y+3) }",
+    };
+};
+
+scope_ptr mock_symbols(const mock_definitions& defs = mock_definitions{}) {
+    scope_ptr scp = sym_store.make_scope();
+    scp->in_api_context(false);
+
+    symbol_map& symbols = *scp->globals();
+
+    for (auto& g: defs.globals) {
+        auto var = new VariableExpression(Location{}, g);
+        var->visibility(visibilityKind::global);
+        symbols[g] = symbol_ptr{std::move(var)};
+    }
+
+    for (auto& f: defs.functions) {
+        auto s = Parser{f}.parse_function();
+        auto name = s->name();
+        symbols[name] = std::move(s);
+    }
+
+    for (auto& p: defs.procedures) {
+        auto s = Parser{p}.parse_procedure();
+        auto name = s->name();
+        symbols[name] = std::move(s);
+    }
+
+    return scp;
+}
+
+void inline_all_functions(scope_ptr scp) {
+    auto& globals = *scp->globals();
+
+    for (auto& entry: globals) {
+        auto& sym = entry.second;
+        if (auto fn = sym->is_function()) {
+            fn->semantic(globals);
+            fn->body(inline_function_calls(fn->name(), fn->body()));
+            fn->semantic(globals);
+        }
+    }
+}
+
+expression_ptr parse_block(const std::string& defn, scope_ptr syms, bool inner = false) {
+    Parser parser{defn};
+    expression_ptr expr = parser.parse_block(inner);
+    EXPECT_TRUE(expr) << parser.error_message();
+    if (expr) expr->semantic(syms);
+    return expr;
+}
+
+TEST(lower_functions, simple) {
+    // If there are no call arguments to be lowered, expect block
+    // to be unchanged.
+
+    const char* tests[] = {
+        "{ a = b + c\n b = g(a, c)\n }",
+        "{ c = h(a)\n b = g(a, c)\n a = b + c\n c = f(a, b, a)\n }"
+    };
+
+    for (auto& defn: tests) {
+        auto bexpr = parse_block(defn, mock_symbols());
+        ASSERT_TRUE(bexpr);
+        auto block = bexpr->is_block();
+        ASSERT_TRUE(block);
+
+        EXPECT_EXPR_EQ(block, lower_functions(block));
+    }
+}
+
+// Note: ordering and name of introduced locals is dependent-upon
+// the implementation.
+
+TEST(lower_functions, compound_args) {
+    struct { const char *before, *after; } tests[] = {
+        {
+            "{ a = g(c, a + b)\n }",
+            "{ LOCAL ll0_\n"
+            "  ll0_ = a + b\n"
+            "  a = g(c, ll0_)\n }"
+        },
+        {
+            "{ a = f(1, 2, a)\n }",
+            "{ LOCAL ll0_\n ll0_ = f(1, 2, a)\n a = ll0_\n }"
+        },
+        {
+            "{ a = h(b + c)\n"
+            "  b = g(a + b, b)\n"
+            "  c = f(a, b, c)\n }",
+            "{ LOCAL ll3_\n"
+            "  LOCAL ll2_\n"
+            "  LOCAL ll1_\n"
+            "  LOCAL ll0_\n"
+            "  ll0_ = b + c\n"
+            "  a = h(ll0_)\n"
+            "  ll1_ = a + b\n"
+            "  ll2_ = g(ll1_, b)\n"
+            "  b = ll2_\n"
+            "  ll3_ = f(a, b, c)\n"
+            "  c = ll3_\n }"
+        }
+    };
+
+    auto syms = mock_symbols();
+    for (auto& test: tests) {
+        auto expr1 = parse_block(test.before, mock_symbols());
+        ASSERT_TRUE(expr1);
+        auto before = expr1->is_block();
+        ASSERT_TRUE(before);
+
+        auto expr2 = parse_block(test.after, mock_symbols());
+        ASSERT_TRUE(expr2);
+        auto expected = expr2->is_block();
+        ASSERT_TRUE(expected);
+
+        EXPECT_EXPR_EQ(expected, lower_functions(before));
+    }
+}
+
+TEST(lower_functions, compound_rhs) {
+    struct { const char *before, *after; } tests[] = {
+        {
+            "{ a = f(b, c) + g(a, a + b)\n }",
+            "{ LOCAL ll2_\n"
+            "  LOCAL ll1_\n"
+            "  LOCAL ll0_\n"
+            "  ll0_ = f(b, c)\n"
+            "  ll1_ = a + b\n"
+            "  ll2_ = g(a, ll1_)\n"
+            "  a = ll0_ + ll2_\n }"
+        },
+        {
+            "{ a = log(exp(h(b)))\n }",
+            "{ LOCAL ll0_\n"
+            "  ll0_ = h(b)\n"
+            "  a = log(exp(ll0_))\n }"
+        }
+    };
+
+    auto syms = mock_symbols();
+    for (auto& test: tests) {
+        auto expr1 = parse_block(test.before, mock_symbols());
+        ASSERT_TRUE(expr1);
+        auto before = expr1->is_block();
+        ASSERT_TRUE(before);
+
+        auto expr2 = parse_block(test.after, mock_symbols());
+        ASSERT_TRUE(expr2);
+        auto expected = expr2->is_block();
+        ASSERT_TRUE(expected);
+
+        EXPECT_EXPR_EQ(expected, lower_functions(before));
+    }
+}
+
+TEST(lower_functions, nested_calls) {
+    struct { const char *before, *after; } tests[] = {
+        {
+            "{ a = h(g(a, a + b))\n }",
+            "{ LOCAL ll1_\n"
+            "  LOCAL ll0_\n"
+            "  ll0_ = a + b\n"
+            "  ll1_ = g(a, ll0_)\n"
+            "  a = h(ll1_)\n }"
+        },
+        {
+            "{ p2(g(a, b), h(h(c)))\n }",
+            "{ LOCAL ll2_\n"
+            "  LOCAL ll1_\n"
+            "  LOCAL ll0_\n"
+            "  ll0_ = g(a, b)\n"
+            "  ll1_ = h(c)\n"
+            "  ll2_ = h(ll1_)\n"
+            "  p2(ll0_, ll2_)\n }"
+        }
+    };
+
+    auto syms = mock_symbols();
+    for (auto& test: tests) {
+        auto expr1 = parse_block(test.before, mock_symbols());
+        ASSERT_TRUE(expr1);
+        auto before = expr1->is_block();
+        ASSERT_TRUE(before);
+
+        auto expr2 = parse_block(test.after, mock_symbols());
+        ASSERT_TRUE(expr2);
+        auto expected = expr2->is_block();
+        ASSERT_TRUE(expected);
+
+        EXPECT_EXPR_EQ(expected, lower_functions(before));
+    }
+}
+
+TEST(lower_functions, ifexpr) {
+    struct { const char *before, *after; } tests[] = {
+
+    // Can't test current implementation without shenanigans,
+    // as LOCAL declarations will not be parsed in nested blocks.
+#if 0
+        {
+            "{ if (a>1) { p1(h(a))\n } else { p1(2+a)\n }\n }",
+            "{ if (a>1) {\n"
+            "      LOCAL ll0_\n"
+            "      ll0_ = h(a)\n"
+            "      p1(ll0_)\n"
+            "  } else {\n"
+            "      LOCAL ll1_\n"
+            "      ll1_ = 2 + a\n"
+            "      p1(ll1_)\n"
+            "  } } \n"
+        },
+#endif
+        {
+            "{ if (f(a, 1, c)) { p1(a)\n }\n }",
+            "{ LOCAL ll0_\n"
+            "  ll0_ = f(a, 1, c)\n"
+            "  if (ll0_ != 0) { p1(a)\n }\n }"
+        },
+        {
+            "{ if (h(a + 2) > 1) { p1(a)\n }\n }",
+            "{ LOCAL ll1_\n"
+            "  LOCAL ll0_\n"
+            "  ll0_ = a + 2\n"
+            "  ll1_ = h(ll0_)\n"
+            "  if (ll1_ > 1) { p1(a)\n }\n }"
+        }
+    };
+
+    auto syms = mock_symbols();
+    for (auto& test: tests) {
+        auto expr1 = parse_block(test.before, mock_symbols());
+        ASSERT_TRUE(expr1);
+        auto before = expr1->is_block();
+        ASSERT_TRUE(before);
+
+        auto expr2 = parse_block(test.after, mock_symbols());
+        ASSERT_TRUE(expr2);
+        auto expected = expr2->is_block();
+        ASSERT_TRUE(expected);
+
+        EXPECT_EXPR_EQ(expected, lower_functions(before));
+    }
+}
+
+// Note: function inliner should only be run after all
+// function calls in the block have been lowered.
+
+TEST(inline_functions, simple) {
+    // There should be no changes to blocks without
+    // function calls.
+
+    const char* tests[] = {
+        "{ a = b + c\n p2(a, x)\n }",
+        "{ if (a>b) { p1(exp(a))\n }\n }"
+    };
+
+    for (auto& defn: tests) {
+        auto bexpr = parse_block(defn, mock_symbols());
+        ASSERT_TRUE(bexpr);
+        auto block = bexpr->is_block();
+        ASSERT_TRUE(block);
+
+        EXPECT_EXPR_EQ(block, inline_function_calls("", block));
+    }
+}
+
+TEST(inline_functions, compound) {
+    // Check inlining with 'long' function that includes
+    // locals, further function calls, and an if clause.
+
+    const char* before_defn =
+        "{ a = 2\n"
+        "  if (b>3) { b = h(2)\n }\n"
+        "  p2(a, b)\n"
+        "  c = long(c)\n"
+        "  a = long(b)\n }";
+
+    const char* after_defn =
+        "{ a = 2\n"
+        "  if (b>3) { b = exp(2)\n }\n"
+        "  p2(a, b)\n"
+        "  LOCAL r_0_, r_1_\n"
+        "  r_0_ = exp(c)\n"
+        "  r_1_ = r_0_ + c\n"
+        "  if (r_0_>r_1_) { c = (3-c)/(r_1_-c)\n }\n"
+        "  else { c = exp(r_1_)\n }\n"
+        "  LOCAL r_2_, r_3_\n"
+        "  r_2_ = exp(b)\n"
+        "  r_3_ = r_2_ + b\n"
+        "  if (r_2_>r_3_) { a = (3-b)/(r_3_-b)\n }\n"
+        "  else { a = exp(r_3_)\n }\n }";
+
+    auto expr1 = parse_block(before_defn, mock_symbols());
+    ASSERT_TRUE(expr1);
+    auto before = expr1->is_block();
+    ASSERT_TRUE(before);
+
+    auto expr2 = parse_block(after_defn, mock_symbols());
+    ASSERT_TRUE(expr2);
+    auto expected = expr2->is_block();
+    ASSERT_TRUE(expected);
+
+    EXPECT_EXPR_EQ(expected, inline_function_calls("", before));
+}
+
+TEST(inline_functions, twice_assign) {
+    // What happens if function return variable is assigned twice?
+    // Test uses mocked definition:
+    //
+    //      FUNCTION assign2(x) {
+    //          assign2 = x * 2
+    //          if (x<2) {
+    //              assign2 = 2
+    //          }
+    //      }
+    //
+    // Expect that the function lowerer will take care of this.
+
+    const char* before_defn =
+        "{ a = assign2(a)\n }";
+
+    const char* after_defn =
+        "{ LOCAL ll0_\n"
+        "  ll0_ = a * 2\n"
+        "  if (a<2) {\n"
+        "      ll0_ = 2\n"
+        "  }\n"
+        "  a = ll0_\n }";
+
+    auto expr1 = parse_block(before_defn, mock_symbols());
+    ASSERT_TRUE(expr1);
+    auto before = expr1->is_block();
+    ASSERT_TRUE(before);
+
+    auto lowered_expr = lower_functions(before);
+    ASSERT_TRUE(lowered_expr);
+    auto lowered = lowered_expr->is_block();
+    ASSERT_TRUE(lowered);
+
+    auto expr2 = parse_block(after_defn, mock_symbols());
+    ASSERT_TRUE(expr2);
+    auto expected = expr2->is_block();
+    ASSERT_TRUE(expected);
+
+    EXPECT_EXPR_EQ(expected, inline_function_calls("", lowered));
+}
+
+TEST(inline_functions, recursion) {
+    // What happens if we try to inline a recursive function?
+    // We should get an error, but error should mention recursion.
+
+    mock_definitions defs;
+    defs.functions.push_back(
+        "FUNCTION recurse1(x) {\n"
+        "    LOCAL y\n"
+        "    if (x<2) { recurse1 = x\n }\n"
+        "    else { y = x/2\n recurse1 = recurse2(y)\n }\n"
+        "}");
+
+    defs.functions.push_back(
+        "FUNCTION recurse2(x) {\n"
+        "    LOCAL y\n"
+        "    if (x<2) { recurse2 = x\n }\n"
+        "    else { y = x/5\n recuse2 = recurse1(y)\n }\n"
+        "}");
+
+    auto scp = mock_symbols(defs);
+    EXPECT_THROW(inline_all_functions(scp), compiler_exception);
+}
+
+TEST(inline_functions, local_shadow) {
+    // shadow(x) has a local x inside, shadowing the parameter:
+    //
+    //      FUNCTION shadow(x) {
+    //          LOCAL x
+    //          x = 2
+    //          shadow = 1
+    //      }
+
+    const char* before_defn =
+        "{ a = shadow(b)\n }";
+
+    const char* after_defn =
+        "{ LOCAL r_0_\n"
+        "  r_0_ = 2\n"
+        "  a = 1\n }";
+
+    auto expr1 = parse_block(before_defn, mock_symbols());
+    ASSERT_TRUE(expr1);
+    auto before = expr1->is_block();
+    ASSERT_TRUE(before);
+
+    auto expr2 = parse_block(after_defn, mock_symbols());
+    ASSERT_TRUE(expr2);
+    auto expected = expr2->is_block();
+    ASSERT_TRUE(expected);
+
+    EXPECT_EXPR_EQ(expected, inline_function_calls("", before));
+}