diff --git a/modcc/expression.hpp b/modcc/expression.hpp
index b2eb6e463c254a7d5182fec4e55ad6f9e36c9e8e..a9f752ec382fbac4df6ff83000bdb03532a3eb4b 100644
--- a/modcc/expression.hpp
+++ b/modcc/expression.hpp
@@ -877,7 +877,7 @@ public:
     {}
 
     std::string to_string() const override {
-        return pprintf("%%", coeff()->to_string(), ident()->to_string());
+        return pprintf("% %", coeff()->to_string(), ident()->to_string());
     }
     void semantic(std::shared_ptr<scope_type> scp) override;
     expression_ptr clone() const override;
diff --git a/modcc/lexer.cpp b/modcc/lexer.cpp
index 74da1a07eed937ef20d94f0df4f26a1a34dc960b..2528f19297c38be9330e5c2b4cbed7d04a6ffe05 100644
--- a/modcc/lexer.cpp
+++ b/modcc/lexer.cpp
@@ -119,6 +119,10 @@ Token Lexer::parse() {
                 t.type = tok::rbrace;
                 t.spelling += character();
                 return t;
+            case '~':
+                t.type = tok::tilde;
+                t.spelling += character();
+                return t;
             case '=': {
                 t.spelling += character();
                 if(*current_=='=') {
diff --git a/modcc/parser.cpp b/modcc/parser.cpp
index 74692fc91c4f32670e90011ed103d06ac6561070..173da807b6287969c18874230725b0d2f99582f1 100644
--- a/modcc/parser.cpp
+++ b/modcc/parser.cpp
@@ -967,11 +967,18 @@ expression_ptr Parser::parse_stoich_expression() {
     auto here = location_;
 
     if(token_.type==tok::integer || token_.type==tok::identifier) {
-        terms.push_back(parse_stoich_term());
+        auto term = parse_stoich_term();
+        if (!term) return nullptr;
+
+        terms.push_back(std::move(term));
 
         while(token_.type==tok::plus) {
             get_token(); // consume plus
-            terms.push_back(parse_stoich_term());
+
+            auto term = parse_stoich_term();
+            if (!term) return nullptr;
+
+            terms.push_back(std::move(term));
         }
     }
 
@@ -981,37 +988,48 @@ expression_ptr Parser::parse_stoich_expression() {
 expression_ptr Parser::parse_reaction_expression() {
     auto here = location_;
 
-    // consume tilde
-    get_token();
+    if(token_.type!=tok::tilde) {
+        error(pprintf("expected '%', found '%'", yellow("~"), yellow(token_.spelling)));
+        return nullptr;
+    }
 
+    get_token(); // consume tilde
     expression_ptr lhs = parse_stoich_expression();
+    if (!lhs) return nullptr;
 
     if(token_.type != tok::arrow) {
         error(pprintf("expected '%', found '%'", yellow("<->"), yellow(token_.spelling)));
         return nullptr;
     }
 
+    get_token(); // consume arrow
     expression_ptr rhs = parse_stoich_expression();
+    if (!rhs) return nullptr;
 
     if(token_.type != tok::lparen) {
         error(pprintf("expected '%', found '%'", yellow("("), yellow(token_.spelling)));
         return nullptr;
     }
 
+    get_token(); // consume lparen
     expression_ptr fwd = parse_expression();
+    if (!fwd) return nullptr;
 
     if(token_.type != tok::comma) {
         error(pprintf("expected '%', found '%'", yellow(","), yellow(token_.spelling)));
         return nullptr;
     }
 
+    get_token(); // consume comma
     expression_ptr rev = parse_expression();
+    if (!rev) return nullptr;
 
     if(token_.type != tok::rparen) {
         error(pprintf("expected '%', found '%'", yellow(")"), yellow(token_.spelling)));
         return nullptr;
     }
 
+    get_token(); // consume rparen
     return make_expression<ReactionExpression>(here, std::move(lhs), std::move(rhs),
         std::move(fwd), std::move(rev));
 }
diff --git a/tests/modcc/driver.cpp b/tests/modcc/driver.cpp
index f32ac59d1471f94c6fb3d7d6333eeaf86d2e1c28..67505a0d20d3406d1ed8142715fb2243e74c6b0b 100644
--- a/tests/modcc/driver.cpp
+++ b/tests/modcc/driver.cpp
@@ -2,10 +2,17 @@
  * unit test driver
  **************************************************************/
 
+#include <cstring>
+
 #include "test.hpp"
 
+bool g_verbose_flag = false;
+
 int main(int argc, char **argv) {
     ::testing::InitGoogleTest(&argc, argv);
+    if (argc>1 && (!std::strcmp(argv[1],"-v") || !std::strcmp(argv[1],"--verbose"))) {
+        g_verbose_flag = true;
+    }
     return RUN_ALL_TESTS();
 }
 
diff --git a/tests/modcc/test.hpp b/tests/modcc/test.hpp
index 588b75a36afcb09107adc65b862fd65eceb5a9c6..d3de72506d2ef679227fcc71f0d76aab141b3a9c 100644
--- a/tests/modcc/test.hpp
+++ b/tests/modcc/test.hpp
@@ -5,12 +5,9 @@
 #include "parser.hpp"
 #include "modccutil.hpp"
 
-//#define VERBOSE_TEST
-#ifdef VERBOSE_TEST
-#define VERBOSE_PRINT(x) std::cout << (x) << std::endl;
-#else
-#define VERBOSE_PRINT(x)
-#endif
+extern bool g_verbose_flag;
+
+#define VERBOSE_PRINT(x) (g_verbose_flag && std::cout << (x) << "\n")
 
 inline expression_ptr parse_line_expression(std::string const& s) {
     return Parser(s).parse_line_expression();
diff --git a/tests/modcc/test_lexer.cpp b/tests/modcc/test_lexer.cpp
index b881f5a0a69c344a8ae0a6e9f8ebd2adc46ea495..b8ba84d92c85ac3bb8efc5533c1115efb031d12d 100644
--- a/tests/modcc/test_lexer.cpp
+++ b/tests/modcc/test_lexer.cpp
@@ -1,11 +1,37 @@
 #include <cmath>
 #include <iterator>
+#include <utility>
 
 #include "test.hpp"
 #include "lexer.hpp"
 
-//#define PRINT_LEX_STRING std::cout << "________________\n" << string << "\n________________\n";
-#define PRINT_LEX_STRING
+void verbose_print(const char* string) {
+    if (!g_verbose_flag) return;
+    std::cout << "________________\n" << string << "\n________________\n";
+}
+
+void verbose_print(const Token& token) {
+    if (!g_verbose_flag) return;
+    std::cout << "tok: " << token << "\n";
+}
+
+class VerboseLexer: public Lexer {
+public:
+    template <typename... Args>
+    VerboseLexer(Args&&... args): Lexer(std::forward<Args>(args)...) {
+        if (g_verbose_flag) {
+            std::cout << "________________\n" << std::string(begin_, end_) << "\n________________\n";
+        }
+    }
+
+    Token parse() {
+        auto tok = Lexer::parse();
+        if (g_verbose_flag) {
+            std::cout << "token: " << tok << "\n";
+        }
+        return tok;
+    }
+};
 
 /**************************************************************
  * lexer tests
@@ -13,8 +39,7 @@
 // test identifiers
 TEST(Lexer, identifiers) {
     char string[] = "_foo:\nbar, buzz f_zz";
-    PRINT_LEX_STRING
-    Lexer lexer(string, string+sizeof(string));
+    VerboseLexer lexer(string, string+sizeof(string));
 
     auto t1 = lexer.parse();
     EXPECT_EQ(t1.type, tok::identifier);
@@ -43,9 +68,8 @@ TEST(Lexer, identifiers) {
 
 // test keywords
 TEST(Lexer, keywords) {
-    char string[] = "NEURON UNITS SOLVE else TITLE CONDUCTANCE";
-    PRINT_LEX_STRING
-    Lexer lexer(string, string+sizeof(string));
+    char string[] = "NEURON UNITS SOLVE else TITLE CONDUCTANCE KINETIC";
+    VerboseLexer lexer(string, string+sizeof(string));
 
     // should skip all white space and go straight to eof
     auto t1 = lexer.parse();
@@ -69,19 +93,22 @@ TEST(Lexer, keywords) {
     EXPECT_NE(t5.type, tok::identifier);
     EXPECT_EQ(t5.spelling, "TITLE");
 
+    auto t6 = lexer.parse();
+    EXPECT_EQ(t6.type, tok::conductance);
+    EXPECT_EQ(t6.spelling, "CONDUCTANCE");
+
     auto t7 = lexer.parse();
-    EXPECT_EQ(t7.type, tok::conductance);
-    EXPECT_EQ(t7.spelling, "CONDUCTANCE");
+    EXPECT_EQ(t7.type, tok::kinetic);
+    EXPECT_EQ(t7.spelling, "KINETIC");
 
-    auto t6 = lexer.parse();
-    EXPECT_EQ(t6.type, tok::eof);
+    auto tlast = lexer.parse();
+    EXPECT_EQ(tlast.type, tok::eof);
 }
 
 // test white space
 TEST(Lexer, whitespace) {
     char string[] = " \t\v\f";
-    PRINT_LEX_STRING
-    Lexer lexer(string, string+sizeof(string));
+    VerboseLexer lexer(string, string+sizeof(string));
 
     // should skip all white space and go straight to eof
     auto t1 = lexer.parse();
@@ -91,8 +118,7 @@ TEST(Lexer, whitespace) {
 // test new line
 TEST(Lexer, newline) {
     char string[] = "foo \n    bar \n +\r\n-";
-    PRINT_LEX_STRING
-    Lexer lexer(string, string+sizeof(string));
+    VerboseLexer lexer(string, string+sizeof(string));
 
     // get foo
     auto t1 = lexer.parse();
@@ -123,9 +149,8 @@ TEST(Lexer, newline) {
 
 // test operators
 TEST(Lexer, symbols) {
-    char string[] = "+-/*, t= ^ h'";
-    PRINT_LEX_STRING
-    Lexer lexer(string, string+sizeof(string));
+    char string[] = "+-/*, t= ^ h'<->~";
+    VerboseLexer lexer(string, string+sizeof(string));
 
     auto t1 = lexer.parse();
     EXPECT_EQ(t1.type, tok::plus);
@@ -161,13 +186,18 @@ TEST(Lexer, symbols) {
     EXPECT_EQ(t10.type, tok::prime);
 
     auto t11 = lexer.parse();
-    EXPECT_EQ(t11.type, tok::eof);
+    EXPECT_EQ(t11.type, tok::arrow);
+
+    auto t12 = lexer.parse();
+    EXPECT_EQ(t12.type, tok::tilde);
+
+    auto tlast = lexer.parse();
+    EXPECT_EQ(tlast.type, tok::eof);
 }
 
 TEST(Lexer, comparison_operators) {
     char string[] = "< <= > >= == != !";
-    PRINT_LEX_STRING
-    Lexer lexer(string, string+sizeof(string));
+    VerboseLexer lexer(string, string+sizeof(string));
 
     auto t1 = lexer.parse();
     EXPECT_EQ(t1.type, tok::lt);
@@ -191,8 +221,7 @@ TEST(Lexer, comparison_operators) {
 // test braces
 TEST(Lexer, braces) {
     char string[] = "foo}";
-    PRINT_LEX_STRING
-    Lexer lexer(string, string+sizeof(string));
+    VerboseLexer lexer(string, string+sizeof(string));
 
     auto t1 = lexer.parse();
     EXPECT_EQ(t1.type, tok::identifier);
@@ -209,8 +238,7 @@ TEST(Lexer, comments) {
     char string[] = "foo:this is one line\n"
                     "bar : another comment\n"
                     "foobar ? another comment\n";
-    PRINT_LEX_STRING
-    Lexer lexer(string, string+sizeof(string));
+    VerboseLexer lexer(string, string+sizeof(string));
 
     auto t1 = lexer.parse();
     EXPECT_EQ(t1.type, tok::identifier);
@@ -243,7 +271,7 @@ TEST(Lexer, numbers) {
     std::vector<long long> check_ints = {1, 23, 3};
     std::vector<long long> ints;
 
-    Lexer lexer(floats_stream.str());
+    VerboseLexer lexer(floats_stream.str());
     auto t = lexer.parse();
     auto iter = floats.cbegin();
     while (t.type != tok::eof && iter != floats.cend()) {
diff --git a/tests/modcc/test_optimization.cpp b/tests/modcc/test_optimization.cpp
index 9b69cee7d2aa5c16af82e2a143cc9632c9b359b5..01f0a69c7e9cc8ec918a307bb77dd2a00d5e961b 100644
--- a/tests/modcc/test_optimization.cpp
+++ b/tests/modcc/test_optimization.cpp
@@ -9,23 +9,23 @@ TEST(Optimizer, constant_folding) {
     auto v = make_unique<ConstantFolderVisitor>();
     {
         auto e = parse_line_expression("x = 2*3");
-        VERBOSE_PRINT( e->to_string() )
+        VERBOSE_PRINT( e->to_string() );
         e->accept(v.get());
         EXPECT_EQ(e->is_assignment()->rhs()->is_number()->value(), 6);
-        VERBOSE_PRINT( e->to_string() )
-        VERBOSE_PRINT( "" )
+        VERBOSE_PRINT( e->to_string() );
+        VERBOSE_PRINT( "" );
     }
     {
         auto e = parse_line_expression("x = 1 + 2 + 3");
-        VERBOSE_PRINT( e->to_string() )
+        VERBOSE_PRINT( e->to_string() );
         e->accept(v.get());
         EXPECT_EQ(e->is_assignment()->rhs()->is_number()->value(), 6);
-        VERBOSE_PRINT( e->to_string() )
-        VERBOSE_PRINT( "" )
+        VERBOSE_PRINT( e->to_string() );
+        VERBOSE_PRINT( "" );
     }
     {
         auto e = parse_line_expression("x = exp(2)");
-        VERBOSE_PRINT( e->to_string() )
+        VERBOSE_PRINT( e->to_string() );
         e->accept(v.get());
         // The tolerance has to be loosend to 1e-15, because the optimizer performs
         // all intermediate calculations in 80 bit precision, which disagrees in
@@ -33,24 +33,24 @@ TEST(Optimizer, constant_folding) {
         // This is a good thing: by using the constant folder we increase accuracy
         // over the unoptimized code!
         EXPECT_EQ(std::fabs(e->is_assignment()->rhs()->is_number()->value()-std::exp(2.0))<1e-15, true);
-        VERBOSE_PRINT( e->to_string() )
-        VERBOSE_PRINT( "" )
+        VERBOSE_PRINT( e->to_string() );
+        VERBOSE_PRINT( "" );
     }
     {
         auto e = parse_line_expression("x= 2*2 + 3");
-        VERBOSE_PRINT( e->to_string() )
+        VERBOSE_PRINT( e->to_string() );
         e->accept(v.get());
         EXPECT_EQ(e->is_assignment()->rhs()->is_number()->value(), 7);
-        VERBOSE_PRINT( e->to_string() )
-        VERBOSE_PRINT( "" )
+        VERBOSE_PRINT( e->to_string() );
+        VERBOSE_PRINT( "" );
     }
     {
         auto e = parse_line_expression("x= 3 + 2*2");
-        VERBOSE_PRINT( e->to_string() )
+        VERBOSE_PRINT( e->to_string() );
         e->accept(v.get());
         EXPECT_EQ(e->is_assignment()->rhs()->is_number()->value(), 7);
-        VERBOSE_PRINT( e->to_string() )
-        VERBOSE_PRINT( "" )
+        VERBOSE_PRINT( e->to_string() );
+        VERBOSE_PRINT( "" );
     }
     {
         // this doesn't work: the (y+2) expression is not a constant, so folding stops.
@@ -58,23 +58,23 @@ TEST(Optimizer, constant_folding) {
         // one approach would be try sorting communtative operations so that numbers
         // are adjacent to one another in the tree
         auto e = parse_line_expression("x= y + 2 + 3");
-        VERBOSE_PRINT( e->to_string() )
+        VERBOSE_PRINT( e->to_string() );
         e->accept(v.get());
-        VERBOSE_PRINT( e->to_string() )
-        VERBOSE_PRINT( "" )
+        VERBOSE_PRINT( e->to_string() );
+        VERBOSE_PRINT( "" );
     }
     {
         auto e = parse_line_expression("x= 2 + 3 + y");
-        VERBOSE_PRINT( e->to_string() )
+        VERBOSE_PRINT( e->to_string() );
         e->accept(v.get());
-        VERBOSE_PRINT( e->to_string() )
-        VERBOSE_PRINT("");
+        VERBOSE_PRINT( e->to_string() );
+        VERBOSE_PRINT("");;
     }
     {
         auto e = parse_line_expression("foo(2+3, log(32), 2*3 + x)");
-        VERBOSE_PRINT( e->to_string() )
+        VERBOSE_PRINT( e->to_string() );
         e->accept(v.get());
-        VERBOSE_PRINT( e->to_string() )
-        VERBOSE_PRINT("");
+        VERBOSE_PRINT( e->to_string() );
+        VERBOSE_PRINT("");;
     }
 }
diff --git a/tests/modcc/test_parser.cpp b/tests/modcc/test_parser.cpp
index 1edfe1a9ad61a0e3837dbddba2877037fb30aee2..dd9a3ea90ee04a26c2893a2aedb6b5fc0a5fa6cc 100644
--- a/tests/modcc/test_parser.cpp
+++ b/tests/modcc/test_parser.cpp
@@ -1,12 +1,76 @@
 #include <cmath>
+#include <memory>
 
 #include "test.hpp"
 #include "module.hpp"
+#include "modccutil.hpp"
 #include "parser.hpp"
 
+template <typename EPtr>
+void verbose_print(const EPtr& e, Parser& p, const char* text) {
+    if (!g_verbose_flag) return;
+
+    if (e) std::cout << e->to_string() << "\n";
+    if (p.status()==lexerStatus::error)
+        std::cout << "in " << red(text) << "\t" << p.error_message() << "\n";
+}
+
+template <typename Derived, typename RetUniqPtr>
+::testing::AssertionResult check_parse(
+    std::unique_ptr<Derived>& derived,
+    RetUniqPtr (Parser::*pmemfn)(),
+    const char* text)
+{
+    Parser p(text);
+    auto e = (p.*pmemfn)();
+    verbose_print(e, p, text);
+
+    if (e==nullptr) {
+        return ::testing::AssertionFailure() << "failed to parse '" << text << "'";
+    }
+
+    if (p.status()!=lexerStatus::happy) {
+        return ::testing::AssertionFailure() << "parser status is not happy";
+    }
+
+    Derived *ptr = e? dynamic_cast<Derived*>(e.get()): nullptr;
+    if (ptr==nullptr) {
+        return ::testing::AssertionFailure() << "failed to cast to derived type";
+    }
+    else {
+        e.release();
+        derived.reset(ptr);
+    }
+
+    return ::testing::AssertionSuccess();
+}
+
+template <typename RetUniqPtr>
+::testing::AssertionResult check_parse(RetUniqPtr (Parser::*pmemfn)(), const char* text) {
+    std::unique_ptr<Expression> e;
+    return check_parse(e, pmemfn, text);
+}
+
+template <typename RetUniqPtr>
+::testing::AssertionResult check_parse_fail(RetUniqPtr (Parser::*pmemfn)(), const char* text) {
+    Parser p(text);
+    auto e = (p.*pmemfn)();
+    verbose_print(e, p, text);
+
+    if (p.status()!=lexerStatus::error) {
+        return ::testing::AssertionFailure() << "parser status is not error";
+    }
+
+    if (e!=nullptr) {
+        return ::testing::AssertionFailure() << "parser returned non-null expression";
+    }
+
+    return ::testing::AssertionSuccess();
+}
+
 TEST(Parser, full_file) {
     Module m(DATADIR "/test.mod");
-    if(m.buffer().size()==0) {
+    if (m.buffer().size()==0) {
         std::cout << "skipping Parser.full_file test because unable to open input file" << std::endl;
         return;
     }
@@ -15,481 +79,339 @@ TEST(Parser, full_file) {
 }
 
 TEST(Parser, procedure) {
-    std::vector<const char*> calls =
-{
-"PROCEDURE foo(x, y) {"
-"  LOCAL a\n"
-"  LOCAL b\n"
-"  LOCAL c\n"
-"  a = 3\n"
-"  b = x * y + 2\n"
-"  y = x + y * 2\n"
-"  y = a + b +c + a + b\n"
-"  y = a + b *c + a + b\n"
-"}"
-,
-"PROCEDURE trates(v) {\n"
-"    LOCAL qt\n"
-"    qt=q10^((celsius-22)/10)\n"
-"    minf=1-1/(1+exp((v-vhalfm)/km))\n"
-"    hinf=1/(1+exp((v-vhalfh)/kh))\n"
-"    mtau = 0.6\n"
-"    htau = 1500\n"
-"}"
-};
-    for(auto const& str : calls) {
-        Parser p(str);
-        auto e = p.parse_procedure();
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
-        if(p.status()==lexerStatus::error) {
-            std::cout << str << std::endl;
-            std::cout << red("error ") << p.error_message() << std::endl;
-        }
+    std::vector<const char*> calls = {
+        "PROCEDURE foo(x, y) {\n"
+        "  LOCAL a\n"
+        "  LOCAL b\n"
+        "  LOCAL c\n"
+        "  a = 3\n"
+        "  b = x * y + 2\n"
+        "  y = x + y * 2\n"
+        "  y = a + b +c + a + b\n"
+        "  y = a + b *c + a + b\n"
+        "}"
+        ,
+        "PROCEDURE trates(v) {\n"
+        "    LOCAL qt\n"
+        "    qt=q10^((celsius-22)/10)\n"
+        "    minf=1-1/(1+exp((v-vhalfm)/km))\n"
+        "    hinf=1/(1+exp((v-vhalfh)/kh))\n"
+        "    mtau = 0.6\n"
+        "    htau = 1500\n"
+        "}"
+    };
+
+    for (const auto& str: calls) {
+        EXPECT_TRUE(check_parse(&Parser::parse_procedure, str));
     }
 }
 
 TEST(Parser, net_receive) {
     char str[] =
-    "NET_RECEIVE (x, y) {   \n"
-    "  LOCAL a              \n"
-    "  a = 3                \n"
-    "  x = a+3              \n"
-    "  y = x+a              \n"
-    "}";
-    Parser p(str);
-    auto e = p.parse_procedure();
-    #ifdef VERBOSE_TEST
-    if(e) std::cout << e->to_string() << std::endl;
-    #endif
-
-    EXPECT_NE(e, nullptr);
-    EXPECT_EQ(p.status(), lexerStatus::happy);
-
-    auto nr = e->is_symbol()->is_net_receive();
-    EXPECT_NE(nr, nullptr);
-    if(nr) {
-        EXPECT_EQ(nr->args().size(), (unsigned)2);
-    }
-    if(p.status()==lexerStatus::error) {
-        std::cout << str << std::endl;
-        std::cout << red("error ") << p.error_message() << std::endl;
+        "NET_RECEIVE (x, y) {   \n"
+        "  LOCAL a              \n"
+        "  a = 3                \n"
+        "  x = a+3              \n"
+        "  y = x+a              \n"
+        "}";
+
+    std::unique_ptr<Symbol> sym;
+
+    EXPECT_TRUE(check_parse(sym, &Parser::parse_procedure, str));
+    if (sym) {
+        auto nr = sym->is_net_receive();
+        EXPECT_NE(nullptr, nr);
+        if (nr) {
+            EXPECT_EQ(2u, nr->args().size());
+        }
     }
 }
 
 TEST(Parser, function) {
-    std::vector< const char*> calls =
-{
-"FUNCTION foo(x, y) {"
-"  LOCAL a\n"
-"  a = 3\n"
-"  b = x * y + 2\n"
-"  y = x + y * 2\n"
-"  foo = a * x + y\n"
-"}"
-};
-    for(auto const& str : calls) {
-        Parser p(str);
-        auto e = p.parse_function();
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
-        if(p.status()==lexerStatus::error) {
-            std::cout << str << std::endl;
-            std::cout << red("error ") << p.error_message() << std::endl;
-        }
-    }
+    char str[] =
+        "FUNCTION foo(x, y) {"
+        "  LOCAL a\n"
+        "  a = 3\n"
+        "  b = x * y + 2\n"
+        "  y = x + y * 2\n"
+        "  foo = a * x + y\n"
+        "}";
+
+    std::unique_ptr<Symbol> sym;
+    EXPECT_TRUE(check_parse(sym, &Parser::parse_function, str));
 }
 
 TEST(Parser, parse_solve) {
-    {
-        Parser p("SOLVE states METHOD cnexp");
-        auto e = p.parse_solve();
-
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
-
-        if(e) {
-            SolveExpression* s = dynamic_cast<SolveExpression*>(e.get());
-            EXPECT_EQ(s->method(), solverMethod::cnexp);
-            EXPECT_EQ(s->name(), "states");
-        }
+    std::unique_ptr<SolveExpression> s;
 
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error) {
-            std::cout << red("error") << p.error_message() << std::endl;
-        }
+    EXPECT_TRUE(check_parse(s, &Parser::parse_solve, "SOLVE states METHOD cnexp"));
+    if (s) {
+        EXPECT_EQ(s->method(), solverMethod::cnexp);
+        EXPECT_EQ(s->name(), "states");
     }
-    {
-        Parser p("SOLVE states");
-        auto e = p.parse_solve();
 
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
-
-        if(e) {
-            SolveExpression* s = dynamic_cast<SolveExpression*>(e.get());
-            EXPECT_EQ(s->method(), solverMethod::none);
-            EXPECT_EQ(s->name(), "states");
-        }
-
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error) {
-            std::cout << red("error") << p.error_message() << std::endl;
-        }
+    EXPECT_TRUE(check_parse(s, &Parser::parse_solve, "SOLVE states"));
+    if (s) {
+        EXPECT_EQ(s->method(), solverMethod::none);
+        EXPECT_EQ(s->name(), "states");
     }
 }
 
 TEST(Parser, parse_conductance) {
-    {
-        Parser p("CONDUCTANCE g USEION na");
-        auto e = p.parse_conductance();
-
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
-
-        if(e) {
-            ConductanceExpression* s = dynamic_cast<ConductanceExpression*>(e.get());
-            EXPECT_EQ(s->ion_channel(), ionKind::Na);
-            EXPECT_EQ(s->name(), "g");
-        }
+    std::unique_ptr<ConductanceExpression> s;
 
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error) {
-            std::cout << red("error") << p.error_message() << std::endl;
-        }
+    EXPECT_TRUE(check_parse(s, &Parser::parse_conductance, "CONDUCTANCE g USEION na"));
+    if (s) {
+        EXPECT_EQ(s->ion_channel(), ionKind::Na);
+        EXPECT_EQ(s->name(), "g");
     }
-    {
-        Parser p("CONDUCTANCE gnda");
-        auto e = p.parse_conductance();
 
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
-
-        if(e) {
-            ConductanceExpression* s = dynamic_cast<ConductanceExpression*>(e.get());
-            EXPECT_EQ(s->ion_channel(), ionKind::nonspecific);
-            EXPECT_EQ(s->name(), "gnda");
-        }
-
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error) {
-            std::cout << red("error") << p.error_message() << std::endl;
-        }
+    EXPECT_TRUE(check_parse(s, &Parser::parse_conductance, "CONDUCTANCE gnda"));
+    if (s) {
+        EXPECT_EQ(s->ion_channel(), ionKind::nonspecific);
+        EXPECT_EQ(s->name(), "gnda");
     }
 }
 
 TEST(Parser, parse_if) {
-    {
-        char expression[] =
+    std::unique_ptr<IfExpression> s;
+
+    EXPECT_TRUE(check_parse(s, &Parser::parse_if,
         "   if(a<b) {      \n"
         "       a = 2+b    \n"
         "       b = 4^b    \n"
-        "   }              \n";
-        Parser p(expression);
-        auto e = p.parse_if();
-        EXPECT_NE(e, nullptr);
-        if(e) {
-            auto ife = e->is_if();
-            EXPECT_NE(e->is_if(), nullptr);
-            if(ife) {
-                EXPECT_NE(ife->condition()->is_binary(), nullptr);
-                EXPECT_NE(ife->true_branch()->is_block(), nullptr);
-                EXPECT_EQ(ife->false_branch(), nullptr);
-            }
-            //std::cout << e->to_string() << std::endl;
-        }
-        else {
-            std::cout << p.error_message() << std::endl;
-        }
+        "   }              \n"
+    ));
+    if (s) {
+        EXPECT_NE(s->condition()->is_binary(), nullptr);
+        EXPECT_NE(s->true_branch()->is_block(), nullptr);
+        EXPECT_EQ(s->false_branch(), nullptr);
     }
-    {
-        char expression[] =
+
+    EXPECT_TRUE(check_parse(s, &Parser::parse_if,
         "   if(a<b) {      \n"
         "       a = 2+b    \n"
         "   } else {       \n"
         "       a = 2+b    \n"
-        "   }                ";
-        Parser p(expression);
-        auto e = p.parse_if();
-        EXPECT_NE(e, nullptr);
-        if(e) {
-            auto ife = e->is_if();
-            EXPECT_NE(ife, nullptr);
-            if(ife) {
-                EXPECT_NE(ife->condition()->is_binary(), nullptr);
-                EXPECT_NE(ife->true_branch()->is_block(), nullptr);
-                EXPECT_NE(ife->false_branch(), nullptr);
-            }
-            //std::cout << std::endl << e->to_string() << std::endl;
-        }
-        else {
-            std::cout << p.error_message() << std::endl;
-        }
+        "   }                "
+    ));
+    if (s) {
+        EXPECT_NE(s->condition()->is_binary(), nullptr);
+        EXPECT_NE(s->true_branch()->is_block(), nullptr);
+        EXPECT_NE(s->false_branch(), nullptr);
     }
-    {
-        char expression[] =
+
+    EXPECT_TRUE(check_parse(s, &Parser::parse_if,
         "   if(a<b) {      \n"
         "       a = 2+b    \n"
         "   } else if(b>a){\n"
         "       a = 2+b    \n"
-        "   }              ";
-        Parser p(expression);
-        auto e = p.parse_if();
-        EXPECT_NE(e, nullptr);
-        if(e) {
-            auto ife = e->is_if();
-            EXPECT_NE(ife, nullptr);
-            if(ife) {
-                EXPECT_NE(ife->condition()->is_binary(), nullptr);
-                EXPECT_NE(ife->true_branch()->is_block(), nullptr);
-                EXPECT_NE(ife->false_branch(), nullptr);
-                EXPECT_NE(ife->false_branch()->is_if(), nullptr);
-                EXPECT_EQ(ife->false_branch()->is_if()->false_branch(), nullptr);
-            }
-            //std::cout << std::endl << e->to_string() << std::endl;
-        }
-        else {
-            std::cout << p.error_message() << std::endl;
-        }
+        "   }              "
+    ));
+    if (s) {
+        EXPECT_NE(s->condition()->is_binary(), nullptr);
+        EXPECT_NE(s->true_branch()->is_block(), nullptr);
+        ASSERT_NE(s->false_branch(), nullptr);
+        ASSERT_NE(s->false_branch()->is_if(), nullptr);
+        EXPECT_EQ(s->false_branch()->is_if()->false_branch(), nullptr);
     }
 }
 
 TEST(Parser, parse_local) {
-    ////////////////////// test for valid expressions //////////////////////
-    {
-        Parser p("LOCAL xyz");
-        auto e = p.parse_local();
-
-        #ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-        #endif
-        EXPECT_NE(e, nullptr);
-        if(e) {
-            EXPECT_NE(e->is_local_declaration(), nullptr);
-            EXPECT_EQ(p.status(), lexerStatus::happy);
-        }
+    std::unique_ptr<LocalDeclaration> s;
+    EXPECT_TRUE(check_parse(s, &Parser::parse_local, "LOCAL xyz"));
+    if (s) {
+        ASSERT_EQ(1u, s->variables().size());
+    }
 
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error)
-            std::cout << red("error") << p.error_message() << std::endl;
-    }
-
-    {
-        Parser p("LOCAL x, y, z");
-        auto e = p.parse_local();
-
-        #ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-        #endif
-        EXPECT_NE(e, nullptr);
-        if(e) {
-            EXPECT_NE(e->is_local_declaration(), nullptr);
-            EXPECT_EQ(p.status(), lexerStatus::happy);
-            auto vars = e->is_local_declaration()->variables();
-            EXPECT_EQ(vars.size(), (unsigned)3);
-            EXPECT_NE(vars.find("x"), vars.end());
-            EXPECT_NE(vars.find("y"), vars.end());
-            EXPECT_NE(vars.find("z"), vars.end());
-        }
+    EXPECT_TRUE(check_parse(s, &Parser::parse_local, "LOCAL x, y, z"));
+    if (s) {
+        auto vars = s->variables();
+        ASSERT_EQ(3u, vars.size());
+        ASSERT_TRUE(vars.count("x"));
+        ASSERT_TRUE(vars.count("y"));
+        ASSERT_TRUE(vars.count("z"));
+    }
+
+    EXPECT_TRUE(check_parse_fail(&Parser::parse_local, "LOCAL x,"));
+}
 
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error)
-            std::cout << red("error") << p.error_message() << std::endl;
+TEST(Parser, parse_unary_expression) {
+    const char* good_expr[] = {
+        "+x             ",
+        "-x             ",
+        "(x + -y)       ",
+        "-(x - + -y)    ",
+        "exp(x + y)     ",
+        "-exp(x + -y)   "
+    };
+
+    for (auto& text: good_expr) {
+        EXPECT_TRUE(check_parse(&Parser::parse_unaryop, text));
     }
+}
+
+// test parsing of parenthesis expressions
+TEST(Parser, parse_parenthesis_expression) {
+    const char* good_expr[] = {
+        "((celsius-22)/10)      ",
+        "((celsius-22)+10)      ",
+        "(x+2)                  ",
+        "((x))                  ",
+        "(((x)))                ",
+        "(x + (x * (y*(2)) + 4))",
+    };
 
-    ////////////////////// test for invalid expressions //////////////////////
-    {
-        Parser p("LOCAL 2");
-        auto e = p.parse_local();
+    for (auto& text: good_expr) {
+        EXPECT_TRUE(check_parse(&Parser::parse_parenthesis_expression, text));
+    }
 
-        EXPECT_EQ(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::error);
+    const char* bad_expr[] = {
+        "(x             ",
+        "((x+3)         ",
+        "(x+ +)         ",
+        "(x=3)          ",  // assignment inside parenthesis isn't allowed
+        "(a + (b*2^(x)) ",  // missing closing parenthesis
+    };
 
-        #ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-        if(p.status()==lexerStatus::error)
-            std::cout << "in " << cyan(bad_expression) << "\t" << p.error_message() << std::endl;
-        #endif
+    for (auto& text: bad_expr) {
+        EXPECT_TRUE(check_parse_fail(&Parser::parse_parenthesis_expression, text));
     }
+}
+
+// test parsing of line expressions
+TEST(Parser, parse_line_expression) {
+    const char* good_expr[] = {
+        "qt=q10^((celsius-22)/10)"
+        "x=2        ",
+        "x=2        ",
+        "x = -y\n   "
+        "x=2*y      ",
+        "x=y + 2 * z",
+        "x=(y + 2) * z      ",
+        "x=(y + 2) * z ^ 3  ",
+        "x=(y + 2 * z ^ 3)  ",
+        "foo(x+3, y, bar(21.4))",
+        "y=exp(x+3) + log(exp(x/y))",
+        "a=x^y^z",
+        "a=x/y/z"
+    };
 
-    {
-        Parser p("LOCAL x, ");
-        auto e = p.parse_local();
+    for (auto& text: good_expr) {
+        EXPECT_TRUE(check_parse(&Parser::parse_line_expression, text));
+    }
 
-        EXPECT_EQ(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::error);
+    const char* bad_expr[] = {
+        "x=2+        ",      // incomplete binary expression on rhs
+        "x=          ",      // missing rhs of assignment
+        "x=)y + 2 * z",
+        "x=(y + 2    ",
+        "x=(y ++ z   ",
+        "x/=3        ",      // compound binary expressions not supported
+        "foo+8       ",      // missing assignment
+        "foo()=8     ",      // lhs of assingment must be an lvalue
+    };
 
-        #ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-        if(p.status()==lexerStatus::error)
-            std::cout << "in " << cyan(bad_expression) << "\t" << p.error_message() << std::endl;
-        #endif
+    for (auto& text: bad_expr) {
+        EXPECT_TRUE(check_parse_fail(&Parser::parse_line_expression, text));
     }
 }
 
-TEST(Parser, parse_unary_expression) {
-    std::vector<const char*> good_expressions =
-    {
-"+x             ",
-"-x             ",
-"(x + -y)       ",
-"-(x - + -y)    ",
-"exp(x + y)     ",
-"-exp(x + -y)   ",
+TEST(Parser, parse_stoich_term) {
+    const char* good_expr[] = {
+        "B", "B3", "3B3", "0A", "12A"
     };
 
-    for(auto const& expression : good_expressions) {
-        Parser p(expression);
-        auto e = p.parse_unaryop();
+    for (auto& text: good_expr) {
+        std::unique_ptr<StoichTermExpression> s;
+        EXPECT_TRUE(check_parse(s, &Parser::parse_stoich_term, text));
+    }
 
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
+    const char* bad_expr[] = {
+        "-A", "-3A", "0.2A", "5"
+    };
 
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error)
-            std::cout << red("error") << p.error_message() << std::endl;
+    for (auto& text: bad_expr) {
+        EXPECT_TRUE(check_parse_fail(&Parser::parse_stoich_term, text));
     }
 }
 
-// test parsing of parenthesis expressions
-TEST(Parser, parse_parenthesis_expression) {
-    std::vector<const char*> good_expressions =
-    {
-"((celsius-22)/10)      ",
-"((celsius-22)+10)      ",
-"(x+2)                  ",
-"((x))                  ",
-"(((x)))                ",
-"(x + (x * (y*(2)) + 4))",
+TEST(Parser, parse_stoich_expression) {
+    const char* single_expr[] = {
+        "B", "B3", "3xy"
     };
 
-    for(auto const& expression : good_expressions) {
-        Parser p(expression);
-        auto e = p.parse_parenthesis_expression();
-
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
-
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error)
-            std::cout << cyan(expression) << "\t"
-                      << red("error") << p.error_message() << std::endl;
-    }
-
-    std::vector<const char*> bad_expressions =
-    {
-"(x             ",
-"((x+3)         ",
-"(x+ +)         ",
-"(x=3)          ",  // assignment inside parenthesis isn't allowed
-"(a + (b*2^(x)) ",  // missing closing parenthesis
+    for (auto& text: single_expr) {
+        std::unique_ptr<StoichExpression> s;
+        EXPECT_TRUE(check_parse(s, &Parser::parse_stoich_expression, text));
+        EXPECT_EQ(1, s->terms().size());
+    }
+
+    const char* double_expr[] = {
+        "B+A", "a1 + 2bn", "4c+d"
     };
 
-    for(auto const& expression : bad_expressions) {
-        Parser p(expression);
-        auto e = p.parse_parenthesis_expression();
+    for (auto& text: double_expr) {
+        std::unique_ptr<StoichExpression> s;
+        EXPECT_TRUE(check_parse(s, &Parser::parse_stoich_expression, text));
+        EXPECT_EQ(2, s->terms().size());
+    }
 
-        EXPECT_EQ(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::error);
+    const char* other_good_expr[] = {
+        "", "a+b+c", "1a+2b+3c+4d"
+    };
 
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-        if(p.status()==lexerStatus::error)
-            std::cout << "in " << cyan(expression) << "\t" << p.error_message() << std::endl;
-#endif
+    for (auto& text: other_good_expr) {
+        std::unique_ptr<StoichExpression> s;
+        EXPECT_TRUE(check_parse(s, &Parser::parse_stoich_expression, text));
     }
-}
 
-// test parsing of line expressions
-TEST(Parser, parse_line_expression) {
-    std::vector<const char*> good_expressions =
-    {
-"qt=q10^((celsius-22)/10)"
-"x=2        ",
-"x=2        ",
-"x = -y\n   "
-"x=2*y      ",
-"x=y + 2 * z",
-"x=(y + 2) * z      ",
-"x=(y + 2) * z ^ 3  ",
-"x=(y + 2 * z ^ 3)  ",
-"foo(x+3, y, bar(21.4))",
-"y=exp(x+3) + log(exp(x/y))",
-"a=x^y^z",
-"a=x/y/z"
+    const char* bad_expr[] = {
+        "A+B+", "A+5+B"
     };
 
-    for(auto const& expression : good_expressions) {
-        Parser p(expression);
-        auto e = p.parse_line_expression();
-
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
-
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error)
-            std::cout << red("error") << p.error_message() << std::endl;
-    }
-
-    std::vector<const char*> bad_expressions =
-    {
-"x=2+       ",      // incomplete binary expression on rhs
-"x=         ",      // missing rhs of assignment
-"x=)y + 2 * z",
-"x=(y + 2   ",
-"x=(y ++ z  ",
-"x/=3       ",      // compound binary expressions not supported
-"foo+8      ",      // missing assignment
-"foo()=8    ",      // lhs of assingment must be an lvalue
+    for (auto& text: bad_expr) {
+        EXPECT_TRUE(check_parse_fail(&Parser::parse_stoich_expression, text));
+    }
+}
+
+// test parsing of stoich and reaction expressions
+TEST(Parser, parse_reaction_expression) {
+    const char* good_expr[] = {
+        "~ A + B <-> C + D (k1, k2)",
+        "~ 2B <-> C + D + E (k1(3,v), k2)",
+        "~ <-> C + D + 7 E (k1, f(a,b)-2)",
+        "~ <-> (f,g)",
+        "~ A + 3B + C<-> (f,g)"
     };
 
-    for(auto const& expression : bad_expressions) {
-        Parser p(expression);
-        auto e = p.parse_line_expression();
+    for (auto& text: good_expr) {
+        std::unique_ptr<ReactionExpression> s;
+        EXPECT_TRUE(check_parse(s, &Parser::parse_reaction_expression, text));
+    }
 
-        EXPECT_EQ(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::error);
+    const char* bad_expr[] = {
+        "~ A + B <-> C + D (k1, k2, k3)",
+        "~ A + B <-> C + (k1, k2)",
+        "~ 2.3B <-> C + D + E (k1(3,v), k2)",
+        "~ <-> C + D + 7E",
+        "~ <-> (,g)",
+        "~ A - 3B + C<-> (f,g)",
+        "  A <-> B (k1, k2)",
+        "~ A <- B (k1)",
+        "~ A -> B (k2)",
+    };
 
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-        if(p.status()==lexerStatus::error)
-            std::cout << "in " << cyan(expression) << "\t" << p.error_message() << std::endl;
-#endif
+    for (auto& text: bad_expr) {
+        EXPECT_TRUE(check_parse_fail(&Parser::parse_reaction_expression, text));
     }
 }
 
 long double eval(Expression *e) {
-    if(auto n = e->is_number()) {
+    if (auto n = e->is_number()) {
         return n->value();
     }
-    if(auto b = e->is_binary()) {
+    if (auto b = e->is_binary()) {
         auto lhs = eval(b->lhs());
         auto rhs = eval(b->rhs());
         switch(b->op()) {
@@ -501,7 +423,7 @@ long double eval(Expression *e) {
             default:;
         }
     }
-    if(auto u = e->is_unary()) {
+    if (auto u = e->is_unary()) {
         auto val = eval(u->expression());
         switch(u->op()) {
             case tok::plus  : return  val;
@@ -515,8 +437,7 @@ long double eval(Expression *e) {
 // test parsing of expressions for correctness
 // by parsing rvalue expressions with numeric atoms, which can be evalutated using eval
 TEST(Parser, parse_binop) {
-    std::vector<std::pair<const char*, double>> tests =
-    {
+    std::pair<const char*, double> tests[] = {
         // simple
         {"2+3", 2.+3.},
         {"2-3", 2.-3.},
@@ -540,15 +461,9 @@ TEST(Parser, parse_binop) {
         {"3^2*5.", std::pow(3.,2.)*5.},
     };
 
-    for(auto const& test_case : tests) {
-        Parser p(test_case.first);
-        auto e = p.parse_expression();
-
-#ifdef VERBOSE_TEST
-        if(e) std::cout << e->to_string() << std::endl;
-#endif
-        EXPECT_NE(e, nullptr);
-        EXPECT_EQ(p.status(), lexerStatus::happy);
+    for (const auto& test_case: tests) {
+        std::unique_ptr<Expression> e;
+        EXPECT_TRUE(check_parse(e, &Parser::parse_expression, test_case.first));
 
         // A loose tolerance of 1d-10 is required here because the eval()
         // function uses long double for intermediate results (like constant
@@ -556,15 +471,11 @@ TEST(Parser, parse_binop) {
         // operations this can see relatively large divergence between the
         // double and long double results.
         EXPECT_NEAR(eval(e.get()), test_case.second, 1e-10);
-
-        // always print the compiler errors, because they are unexpected
-        if(p.status()==lexerStatus::error)
-            std::cout << red("error") << p.error_message() << std::endl;
     }
 }
 
 TEST(Parser, parse_state_block) {
-    std::vector<const char*> state_blocks = {
+    const char* state_blocks[] = {
         "STATE {\n"
         "    h\n"
         "    m r\n"
@@ -587,14 +498,12 @@ TEST(Parser, parse_state_block) {
         "}"
     };
 
-    for (auto const& str: state_blocks) {
-        Module m(str, sizeof(str));
+    expression_ptr null;
+    for (auto& text: state_blocks) {
+        Module m(text, sizeof(text));
         Parser p(m, false);
         p.parse_state_block();
         EXPECT_EQ(lexerStatus::happy, p.status());
-        if (p.status() == lexerStatus::error) {
-            std::cout << str << "\n"
-                      << red("error") << p.error_message() << "\n";
-        }
+        verbose_print(null, p, text);
     }
 }