diff --git a/modcc/expression.cpp b/modcc/expression.cpp
index 05fdb33dd23c5516384057e5cc2946b5c8c6fab4..e2694ea6f36e5dc33622e9d4fbe725fbd13d24d4 100644
--- a/modcc/expression.cpp
+++ b/modcc/expression.cpp
@@ -274,8 +274,12 @@ void ReactionExpression::semantic(std::shared_ptr<scope_type> scp) {
     scope_ = scp;
     lhs()->semantic(scp);
     rhs()->semantic(scp);
+
     fwd_rate()->semantic(scp);
     rev_rate()->semantic(scp);
+    if(fwd_rate_->is_procedure_call() || rev_rate_->is_procedure_call()) {
+        error("procedure calls can't be made in an expression");
+    }
 }
 
 /*******************************************************************************
@@ -323,6 +327,25 @@ void StoichExpression::semantic(std::shared_ptr<scope_type> scp) {
     }
 }
 
+/*******************************************************************************
+  ConserveExpression
+*******************************************************************************/
+
+expression_ptr ConserveExpression::clone() const {
+    return make_expression<ConserveExpression>(
+        location_, lhs()->clone(), rhs()->clone());
+}
+
+void ConserveExpression::semantic(std::shared_ptr<scope_type> scp) {
+    scope_ = scp;
+    lhs_->semantic(scp);
+    rhs_->semantic(scp);
+
+    if(rhs_->is_procedure_call()) {
+        error("procedure calls can't be made in an expression");
+    }
+}
+
 /*******************************************************************************
   CallExpression
 *******************************************************************************/
@@ -869,6 +892,9 @@ void BinaryExpression::accept(Visitor *v) {
 void AssignmentExpression::accept(Visitor *v) {
     v->visit(this);
 }
+void ConserveExpression::accept(Visitor *v) {
+    v->visit(this);
+}
 void ReactionExpression::accept(Visitor *v) {
     v->visit(this);
 }
diff --git a/modcc/expression.hpp b/modcc/expression.hpp
index a9f752ec382fbac4df6ff83000bdb03532a3eb4b..a7d84e61a6a72220e4bbc34e692de703e1681a94 100644
--- a/modcc/expression.hpp
+++ b/modcc/expression.hpp
@@ -44,6 +44,7 @@ class AssignmentExpression;
 class ReactionExpression;
 class StoichExpression;
 class StoichTermExpression;
+class ConserveExpression;
 class AddBinaryExpression;
 class SubBinaryExpression;
 class MulBinaryExpression;
@@ -166,7 +167,10 @@ public:
     virtual BinaryExpression*      is_binary()            {return nullptr;}
     virtual UnaryExpression*       is_unary()             {return nullptr;}
     virtual AssignmentExpression*  is_assignment()        {return nullptr;}
+    virtual ConserveExpression*    is_conserve()          {return nullptr;}
     virtual ReactionExpression*    is_reaction()          {return nullptr;}
+    virtual StoichExpression*      is_stoich()            {return nullptr;}
+    virtual StoichTermExpression*  is_stoich_term()       {return nullptr;}
     virtual ConditionalExpression* is_conditional()       {return nullptr;}
     virtual InitialBlock*          is_initial_block()     {return nullptr;}
     virtual SolveExpression*       is_solve_statement()   {return nullptr;}
@@ -876,6 +880,8 @@ public:
       coeff_(std::move(coeff)), ident_(std::move(ident))
     {}
 
+    StoichTermExpression* is_stoich_term() override {return this;}
+
     std::string to_string() const override {
         return pprintf("% %", coeff()->to_string(), ident()->to_string());
     }
@@ -889,6 +895,11 @@ public:
     expression_ptr& ident() { return ident_; }
     const expression_ptr& ident() const { return ident_; }
 
+    bool negative() const {
+        auto iexpr = coeff_->is_integer();
+        return iexpr && iexpr->integer_value()<0;
+    }
+
 private:
     expression_ptr coeff_;
     expression_ptr ident_;
@@ -904,6 +915,8 @@ public:
     : Expression(loc)
     {}
 
+    StoichExpression* is_stoich() override {return this;}
+
     std::string to_string() const override;
     void semantic(std::shared_ptr<scope_type> scp) override;
     expression_ptr clone() const override;
@@ -1228,6 +1241,20 @@ public:
     void accept(Visitor *v) override;
 };
 
+class ConserveExpression : public BinaryExpression {
+public:
+    ConserveExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
+    :   BinaryExpression(loc, tok::eq, std::move(lhs), std::move(rhs))
+    {}
+
+    ConserveExpression* is_conserve() override {return this;}
+    expression_ptr clone() const override;
+
+    void semantic(std::shared_ptr<scope_type> scp) override;
+
+    void accept(Visitor *v) override;
+};
+
 class AddBinaryExpression : public BinaryExpression {
 public:
     AddBinaryExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
diff --git a/modcc/parser.cpp b/modcc/parser.cpp
index 173da807b6287969c18874230725b0d2f99582f1..43855a7ef1ad0915e7312aea048c23448015e06a 100644
--- a/modcc/parser.cpp
+++ b/modcc/parser.cpp
@@ -817,6 +817,8 @@ expression_ptr Parser::parse_statement() {
             return parse_local();
         case tok::identifier :
             return parse_line_expression();
+        case tok::conserve :
+            return parse_conserve_expression();
         case tok::tilde :
             return parse_reaction_expression();
         case tok::initial :
@@ -950,6 +952,12 @@ expression_ptr Parser::parse_line_expression() {
 expression_ptr Parser::parse_stoich_term() {
     expression_ptr coeff = make_expression<IntegerExpression>(location_, 1);
     auto here = location_;
+    bool negative = false;
+
+    while(token_.type==tok::minus) {
+        negative = !negative;
+        get_token(); // consume '-'
+    }
 
     if(token_.type==tok::integer) {
         coeff = parse_integer();
@@ -959,6 +967,10 @@ expression_ptr Parser::parse_stoich_term() {
         error(pprintf("expected an identifier, found '%'", yellow(token_.spelling)));
         return nullptr;
     }
+
+    if(negative) {
+        coeff = make_expression<IntegerExpression>(here, -coeff->is_integer()->integer_value());
+    }
     return make_expression<StoichTermExpression>(here, std::move(coeff), parse_identifier());
 }
 
@@ -966,14 +978,16 @@ expression_ptr Parser::parse_stoich_expression() {
     std::vector<expression_ptr> terms;
     auto here = location_;
 
-    if(token_.type==tok::integer || token_.type==tok::identifier) {
+    if(token_.type==tok::integer || token_.type==tok::identifier || token_.type==tok::minus) {
         auto term = parse_stoich_term();
         if (!term) return nullptr;
 
         terms.push_back(std::move(term));
 
-        while(token_.type==tok::plus) {
-            get_token(); // consume plus
+        while(token_.type==tok::plus || token_.type==tok::minus) {
+            if (token_.type==tok::plus) {
+                get_token(); // consume plus
+            }
 
             auto term = parse_stoich_term();
             if (!term) return nullptr;
@@ -997,6 +1011,18 @@ expression_ptr Parser::parse_reaction_expression() {
     expression_ptr lhs = parse_stoich_expression();
     if (!lhs) return nullptr;
 
+    // reaction halves must comprise non-negative terms
+    for (const auto& term: lhs->is_stoich()->terms()) {
+        // should always be true
+        if (auto sterm = term->is_stoich_term()) {
+            if (sterm->negative()) {
+                error(pprintf("expected only non-negative terms in reaction lhs, found '%'",
+                    yellow(term->to_string())));
+                return nullptr;
+            }
+        }
+    }
+
     if(token_.type != tok::arrow) {
         error(pprintf("expected '%', found '%'", yellow("<->"), yellow(token_.spelling)));
         return nullptr;
@@ -1006,6 +1032,17 @@ expression_ptr Parser::parse_reaction_expression() {
     expression_ptr rhs = parse_stoich_expression();
     if (!rhs) return nullptr;
 
+    for (const auto& term: rhs->is_stoich()->terms()) {
+        // should always be true
+        if (auto sterm = term->is_stoich_term()) {
+            if (sterm->negative()) {
+                error(pprintf("expected only non-negative terms in reaction rhs, found '%'",
+                    yellow(term->to_string())));
+                return nullptr;
+            }
+        }
+    }
+
     if(token_.type != tok::lparen) {
         error(pprintf("expected '%', found '%'", yellow("("), yellow(token_.spelling)));
         return nullptr;
@@ -1034,6 +1071,30 @@ expression_ptr Parser::parse_reaction_expression() {
         std::move(fwd), std::move(rev));
 }
 
+expression_ptr Parser::parse_conserve_expression() {
+    auto here = location_;
+
+    if(token_.type!=tok::conserve) {
+        error(pprintf("expected '%', found '%'", yellow("CONSERVE"), yellow(token_.spelling)));
+        return nullptr;
+    }
+
+    get_token(); // consume 'CONSERVE'
+    auto lhs = parse_stoich_expression();
+    if (!lhs) return nullptr;
+
+    if(token_.type != tok::eq) {
+        error(pprintf("expected '%', found '%'", yellow("="), yellow(token_.spelling)));
+        return nullptr;
+    }
+
+    get_token(); // consume '='
+    auto rhs = parse_expression();
+    if (!rhs) return nullptr;
+
+    return make_expression<ConserveExpression>(here, std::move(lhs), std::move(rhs));
+}
+
 expression_ptr Parser::parse_expression() {
     auto lhs = parse_unaryop();
 
diff --git a/modcc/parser.hpp b/modcc/parser.hpp
index ed7b3317b5bea5a6ff26c213e7bbbcea645298c5..dc673e9d7b9f9d1084f8d024801ec4eaf87b9d88 100644
--- a/modcc/parser.hpp
+++ b/modcc/parser.hpp
@@ -27,6 +27,7 @@ public:
     expression_ptr parse_stoich_expression();
     expression_ptr parse_stoich_term();
     expression_ptr parse_reaction_expression();
+    expression_ptr parse_conserve_expression();
     expression_ptr parse_binop(expression_ptr&&, Token);
     expression_ptr parse_unaryop();
     expression_ptr parse_local();
diff --git a/modcc/token.cpp b/modcc/token.cpp
index 58de39c9377c695a450e342a12bbadd205cab120..7383ca9fc684b4f47122a98e8f1af035724c8d9a 100644
--- a/modcc/token.cpp
+++ b/modcc/token.cpp
@@ -43,6 +43,7 @@ static Keyword keywords[] = {
     {"WRITE",       tok::write},
     {"RANGE",       tok::range},
     {"LOCAL",       tok::local},
+    {"CONSERVE",    tok::conserve},
     {"SOLVE",       tok::solve},
     {"THREADSAFE",  tok::threadsafe},
     {"GLOBAL",      tok::global},
diff --git a/modcc/token.hpp b/modcc/token.hpp
index 2f0d23caadeb3bb8a84704618aa47e368c21f3fd..c31cdbc409a2dd411cc5c074bd351fd994d4d029 100644
--- a/modcc/token.hpp
+++ b/modcc/token.hpp
@@ -57,7 +57,7 @@ enum class tok {
     unitsoff, unitson,
     suffix, nonspecific_current, useion,
     read, write,
-    range, local,
+    range, local, conserve,
     solve, method,
     threadsafe, global,
     point_process,
diff --git a/tests/modcc/test_lexer.cpp b/tests/modcc/test_lexer.cpp
index b8ba84d92c85ac3bb8efc5533c1115efb031d12d..2b49e3b120049bdd542d20a246b5095cbc66fbde 100644
--- a/tests/modcc/test_lexer.cpp
+++ b/tests/modcc/test_lexer.cpp
@@ -68,7 +68,7 @@ TEST(Lexer, identifiers) {
 
 // test keywords
 TEST(Lexer, keywords) {
-    char string[] = "NEURON UNITS SOLVE else TITLE CONDUCTANCE KINETIC";
+    char string[] = "NEURON UNITS SOLVE else TITLE CONDUCTANCE KINETIC CONSERVE LOCAL";
     VerboseLexer lexer(string, string+sizeof(string));
 
     // should skip all white space and go straight to eof
@@ -101,6 +101,14 @@ TEST(Lexer, keywords) {
     EXPECT_EQ(t7.type, tok::kinetic);
     EXPECT_EQ(t7.spelling, "KINETIC");
 
+    auto t8 = lexer.parse();
+    EXPECT_EQ(t8.type, tok::conserve);
+    EXPECT_EQ(t8.spelling, "CONSERVE");
+
+    auto t9 = lexer.parse();
+    EXPECT_EQ(t9.type, tok::local);
+    EXPECT_EQ(t9.spelling, "LOCAL");
+
     auto tlast = lexer.parse();
     EXPECT_EQ(tlast.type, tok::eof);
 }
diff --git a/tests/modcc/test_parser.cpp b/tests/modcc/test_parser.cpp
index dd9a3ea90ee04a26c2893a2aedb6b5fc0a5fa6cc..2f1bb0d76b182916876eed028160684f217b4062 100644
--- a/tests/modcc/test_parser.cpp
+++ b/tests/modcc/test_parser.cpp
@@ -318,17 +318,27 @@ TEST(Parser, parse_line_expression) {
 }
 
 TEST(Parser, parse_stoich_term) {
-    const char* good_expr[] = {
+    const char* good_pos_expr[] = {
         "B", "B3", "3B3", "0A", "12A"
     };
 
-    for (auto& text: good_expr) {
+    for (auto& text: good_pos_expr) {
         std::unique_ptr<StoichTermExpression> s;
         EXPECT_TRUE(check_parse(s, &Parser::parse_stoich_term, text));
+        EXPECT_TRUE((s && !s->negative()));
     }
 
+    const char* good_neg_expr[] = {
+        "-3B3", "-A", "-12A"
+    };
+
+    for (auto& text: good_neg_expr) {
+        std::unique_ptr<StoichTermExpression> s;
+        EXPECT_TRUE(check_parse(s, &Parser::parse_stoich_term, text));
+        EXPECT_TRUE((s && s->negative()));
+    }
     const char* bad_expr[] = {
-        "-A", "-3A", "0.2A", "5"
+        "0.2A", "5"
     };
 
     for (auto& text: bad_expr) {
@@ -358,7 +368,7 @@ TEST(Parser, parse_stoich_expression) {
     }
 
     const char* other_good_expr[] = {
-        "", "a+b+c", "1a+2b+3c+4d"
+        "", "a+b+c", "1a-2b+3c+4d"
     };
 
     for (auto& text: other_good_expr) {
@@ -366,6 +376,18 @@ TEST(Parser, parse_stoich_expression) {
         EXPECT_TRUE(check_parse(s, &Parser::parse_stoich_expression, text));
     }
 
+    const char* check_coeff = "-3a+2b-c+d";
+    {
+        std::unique_ptr<StoichExpression> s;
+        EXPECT_TRUE(check_parse(s, &Parser::parse_stoich_expression, check_coeff));
+        EXPECT_EQ(4, s->terms().size());
+        std::vector<int> confirm = {-3,2,-1,1};
+        for (unsigned i = 0; i<4; ++i) {
+            auto term = s->terms()[i]->is_stoich_term();
+            EXPECT_EQ(confirm[i], term->coeff()->is_integer()->integer_value());
+        }
+    }
+
     const char* bad_expr[] = {
         "A+B+", "A+5+B"
     };
@@ -407,6 +429,58 @@ TEST(Parser, parse_reaction_expression) {
     }
 }
 
+TEST(Parser, parse_conserve) {
+    std::unique_ptr<ConserveExpression> s;
+    const char* text;
+
+    text = "CONSERVE a + b = 1";
+    ASSERT_TRUE(check_parse(s, &Parser::parse_conserve_expression, text));
+    EXPECT_TRUE(s->rhs()->is_number());
+    ASSERT_TRUE(s->lhs()->is_stoich());
+    EXPECT_EQ(2, s->lhs()->is_stoich()->terms().size());
+
+    text = "CONSERVE a = 1.23e-2";
+    ASSERT_TRUE(check_parse(s, &Parser::parse_conserve_expression, text));
+    EXPECT_TRUE(s->rhs()->is_number());
+    ASSERT_TRUE(s->lhs()->is_stoich());
+    EXPECT_EQ(1, s->lhs()->is_stoich()->terms().size());
+
+    text = "CONSERVE = 0";
+    ASSERT_TRUE(check_parse(s, &Parser::parse_conserve_expression, text));
+    EXPECT_TRUE(s->rhs()->is_number());
+    ASSERT_TRUE(s->lhs()->is_stoich());
+    EXPECT_EQ(0, s->lhs()->is_stoich()->terms().size());
+
+    text = "CONSERVE -2a + b -c = foo*2.3-bar";
+    ASSERT_TRUE(check_parse(s, &Parser::parse_conserve_expression, text));
+    EXPECT_TRUE(s->rhs()->is_binary());
+    ASSERT_TRUE(s->lhs()->is_stoich());
+    {
+        auto& terms = s->lhs()->is_stoich()->terms();
+        ASSERT_EQ(3, terms.size());
+        auto coeff = terms[0]->is_stoich_term()->coeff()->is_integer();
+        ASSERT_TRUE(coeff);
+        EXPECT_EQ(-2, coeff->integer_value());
+        coeff = terms[1]->is_stoich_term()->coeff()->is_integer();
+        ASSERT_TRUE(coeff);
+        EXPECT_EQ(1, coeff->integer_value());
+        coeff = terms[2]->is_stoich_term()->coeff()->is_integer();
+        ASSERT_TRUE(coeff);
+        EXPECT_EQ(-1, coeff->integer_value());
+    }
+
+    const char* bad_expr[] = {
+        "CONSERVE a + 3*b -c = 1",
+        "CONSERVE a + 3b -c = ",
+        "a+b+c = 2",
+        "CONSERVE a + 3b +c"
+    };
+
+    for (auto& text: bad_expr) {
+        EXPECT_TRUE(check_parse_fail(&Parser::parse_conserve_expression, text));
+    }
+}
+
 long double eval(Expression *e) {
     if (auto n = e->is_number()) {
         return n->value();
@@ -507,3 +581,17 @@ TEST(Parser, parse_state_block) {
         verbose_print(null, p, text);
     }
 }
+
+TEST(Parser, parse_kinetic) {
+    char str[] =
+        "KINETIC kin {\n"
+        "    rates(v)             \n"
+        "    ~ s1 <-> s2 (f1, r1) \n"
+        "    ~ s2 <-> s3 (f2, r2) \n"
+        "    ~ s2 <-> s4 (f3, r3) \n"
+        "    CONSERVE s1 + s3 + s4 - s2 = 2.3\n"
+        "}";
+
+    std::unique_ptr<Symbol> sym;
+    EXPECT_TRUE(check_parse(sym, &Parser::parse_procedure, str));
+}