From 366c1d1247004815a62d96a2c408ebe98d6b8369 Mon Sep 17 00:00:00 2001
From: Sam Yates <halfflat@gmail.com>
Date: Thu, 23 May 2019 11:11:06 +0200
Subject: [PATCH] Generalized ion names (#749)

* Replace ionKind enumerations with corresponding string values.
* Rename `ion.hpp` as `ion_info.hpp`.
* Re-jig 'nonspecific' current handling in modcc: make explicit the external source of the corresponding current variable, and treat as non-indexed those locals with an external variable that has no data source, as opposed to special-casing the `ionKind::nonspecific` value.
* Add another field in ion dependency information that captures a write to an ionic reversal potential.

Fixes #748.
---
 arbor/backends/gpu/mechanism.hpp          |  4 +-
 arbor/backends/gpu/shared_state.cpp       |  7 +--
 arbor/backends/gpu/shared_state.hpp       |  5 ++-
 arbor/backends/multicore/mechanism.hpp    |  4 +-
 arbor/backends/multicore/shared_state.cpp |  9 ++--
 arbor/backends/multicore/shared_state.hpp | 21 ++++-----
 arbor/fvm_layout.cpp                      |  4 +-
 arbor/fvm_layout.hpp                      |  4 +-
 arbor/fvm_lowered_cell_impl.hpp           | 10 +++--
 arbor/include/arbor/cable_cell.hpp        |  9 ++--
 arbor/include/arbor/ion.hpp               | 30 -------------
 arbor/include/arbor/ion_info.hpp          | 11 +++++
 arbor/include/arbor/mechanism.hpp         |  1 -
 arbor/include/arbor/mechinfo.hpp          |  5 +--
 modcc/blocks.hpp                          |  6 +--
 modcc/expression.cpp                      |  8 ++--
 modcc/expression.hpp                      | 44 +++++++++---------
 modcc/identifier.hpp                      | 50 ++-------------------
 modcc/module.cpp                          | 54 +++++++++++++++--------
 modcc/module.hpp                          |  8 ++--
 modcc/parser.cpp                          | 25 +++--------
 modcc/printer/cprinter.cpp                |  5 +--
 modcc/printer/cudaprinter.cpp             |  9 ++--
 modcc/printer/infoprinter.cpp             |  6 +--
 modcc/printer/printerutil.cpp             |  2 +-
 modcc/symdiff.cpp                         |  1 +
 test/unit/test_fvm_layout.cpp             | 14 +++---
 test/unit/test_fvm_lowered.cpp            |  6 +--
 28 files changed, 151 insertions(+), 211 deletions(-)
 delete mode 100644 arbor/include/arbor/ion.hpp
 create mode 100644 arbor/include/arbor/ion_info.hpp

diff --git a/arbor/backends/gpu/mechanism.hpp b/arbor/backends/gpu/mechanism.hpp
index 98e70ce3..94144bf3 100644
--- a/arbor/backends/gpu/mechanism.hpp
+++ b/arbor/backends/gpu/mechanism.hpp
@@ -99,10 +99,10 @@ protected:
     using field_default_entry = std::pair<const char*, value_type>;
     using mechanism_field_default_table = std::vector<field_default_entry>;
 
-    using ion_state_entry = std::pair<ionKind, ion_state_view*>;
+    using ion_state_entry = std::pair<const char*, ion_state_view*>;
     using mechanism_ion_state_table = std::vector<ion_state_entry>;
 
-    using ion_index_entry = std::pair<ionKind, const index_type**>;
+    using ion_index_entry = std::pair<const char*, const index_type**>;
     using mechanism_ion_index_table = std::vector<ion_index_entry>;
 
     virtual void nrn_init() = 0;
diff --git a/arbor/backends/gpu/shared_state.cpp b/arbor/backends/gpu/shared_state.cpp
index 1f8c7331..149f0bd5 100644
--- a/arbor/backends/gpu/shared_state.cpp
+++ b/arbor/backends/gpu/shared_state.cpp
@@ -3,7 +3,7 @@
 
 #include <arbor/constants.hpp>
 #include <arbor/fvm_types.hpp>
-#include <arbor/ion.hpp>
+#include <arbor/ion_info.hpp>
 
 #include "backends/event.hpp"
 #include "backends/gpu/gpu_store_types.hpp"
@@ -132,13 +132,14 @@ shared_state::shared_state(
 {}
 
 void shared_state::add_ion(
+    const std::string& ion_name,
     ion_info info,
     const std::vector<fvm_index_type>& cv,
     const std::vector<fvm_value_type>& iconc_norm_area,
     const std::vector<fvm_value_type>& econc_norm_area)
 {
     ion_data.emplace(std::piecewise_construct,
-        std::forward_as_tuple(info.kind),
+        std::forward_as_tuple(ion_name),
         std::forward_as_tuple(info, cv, iconc_norm_area, econc_norm_area, 1u));
 }
 
@@ -210,7 +211,7 @@ std::ostream& operator<<(std::ostream& o, shared_state& s) {
     o << " current    " << s.current_density << "\n";
     o << " conductivity " << s.conductivity << "\n";
     for (auto& ki: s.ion_data) {
-        auto kn = to_string(ki.first);
+        auto& kn = ki.first;
         auto& i = const_cast<ion_state&>(ki.second);
         o << " " << kn << ".current_density        " << i.iX_ << "\n";
         o << " " << kn << ".reversal_potential     " << i.eX_ << "\n";
diff --git a/arbor/backends/gpu/shared_state.hpp b/arbor/backends/gpu/shared_state.hpp
index fe662720..27174810 100644
--- a/arbor/backends/gpu/shared_state.hpp
+++ b/arbor/backends/gpu/shared_state.hpp
@@ -6,7 +6,7 @@
 #include <vector>
 
 #include <arbor/fvm_types.hpp>
-#include <arbor/ion.hpp>
+#include <arbor/ion_info.hpp>
 
 #include "backends/gpu/gpu_store_types.hpp"
 
@@ -80,7 +80,7 @@ struct shared_state {
     array conductivity;      // Maps CV index to membrane conductivity [kS/m²].
     array temperature_degC;  // Global temperature [°C] (length 1 array).
 
-    std::unordered_map<ionKind, ion_state> ion_data;
+    std::unordered_map<std::string, ion_state> ion_data;
 
     deliverable_event_stream deliverable_events;
 
@@ -94,6 +94,7 @@ struct shared_state {
     );
 
     void add_ion(
+        const std::string& ion_name,
         ion_info info,
         const std::vector<fvm_index_type>& cv,
         const std::vector<fvm_value_type>& iconc_norm_area,
diff --git a/arbor/backends/multicore/mechanism.hpp b/arbor/backends/multicore/mechanism.hpp
index f3819ab1..8054c656 100644
--- a/arbor/backends/multicore/mechanism.hpp
+++ b/arbor/backends/multicore/mechanism.hpp
@@ -112,10 +112,10 @@ protected:
     using field_default_entry = std::pair<const char*, value_type>;
     using mechanism_field_default_table = std::vector<field_default_entry>;
 
-    using ion_state_entry = std::pair<ionKind, ion_state_view*>;
+    using ion_state_entry = std::pair<const char*, ion_state_view*>;
     using mechanism_ion_state_table = std::vector<ion_state_entry>;
 
-    using ion_index_entry = std::pair<ionKind, iarray*>;
+    using ion_index_entry = std::pair<const char*, iarray*>;
     using mechanism_ion_index_table = std::vector<ion_index_entry>;
 
     virtual void nrn_init() = 0;
diff --git a/arbor/backends/multicore/shared_state.cpp b/arbor/backends/multicore/shared_state.cpp
index 6af8064d..c2917d2f 100644
--- a/arbor/backends/multicore/shared_state.cpp
+++ b/arbor/backends/multicore/shared_state.cpp
@@ -10,7 +10,7 @@
 #include <arbor/common_types.hpp>
 #include <arbor/constants.hpp>
 #include <arbor/fvm_types.hpp>
-#include <arbor/ion.hpp>
+#include <arbor/ion_info.hpp>
 #include <arbor/math.hpp>
 #include <arbor/simd/simd.hpp>
 
@@ -149,13 +149,14 @@ shared_state::shared_state(
 }
 
 void shared_state::add_ion(
+    const std::string& ion_name,
     ion_info info,
     const std::vector<fvm_index_type>& cv,
     const std::vector<fvm_value_type>& iconc_norm_area,
     const std::vector<fvm_value_type>& econc_norm_area)
 {
     ion_data.emplace(std::piecewise_construct,
-        std::forward_as_tuple(info.kind),
+        std::forward_as_tuple(ion_name),
         std::forward_as_tuple(info, cv, iconc_norm_area, econc_norm_area, alignment));
 }
 
@@ -266,8 +267,8 @@ std::ostream& operator<<(std::ostream& out, const shared_state& s) {
     out << "voltage    " << csv(s.voltage) << "\n";
     out << "current    " << csv(s.current_density) << "\n";
     out << "conductivity " << csv(s.conductivity) << "\n";
-    for (auto& ki: s.ion_data) {
-        auto kn = to_string(ki.first);
+    for (const auto& ki: s.ion_data) {
+        auto& kn = ki.first;
         auto& i = const_cast<ion_state&>(ki.second);
         out << kn << ".current_density        " << csv(i.iX_) << "\n";
         out << kn << ".reversal_potential     " << csv(i.eX_) << "\n";
diff --git a/arbor/backends/multicore/shared_state.hpp b/arbor/backends/multicore/shared_state.hpp
index be3b5725..bedfaf57 100644
--- a/arbor/backends/multicore/shared_state.hpp
+++ b/arbor/backends/multicore/shared_state.hpp
@@ -10,7 +10,7 @@
 #include <arbor/assert.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/fvm_types.hpp>
-#include <arbor/ion.hpp>
+#include <arbor/ion_info.hpp>
 #include <arbor/simd/simd.hpp>
 
 #include "backends/event.hpp"
@@ -42,15 +42,15 @@ namespace multicore {
 struct ion_state {
     unsigned alignment = 1; // Alignment and padding multiple.
 
-    iarray node_index_; // Instance to CV map.
-    array iX_;          // (nA) current
-    array eX_;          // (mV) reversal potential
-    array Xi_;          // (mM) internal concentration
-    array Xo_;          // (mM) external concentration
-    array weight_Xi_;   // (1) concentration weight internal
-    array weight_Xo_;   // (1) concentration weight external
+    iarray node_index_;     // Instance to CV map.
+    array iX_;              // (nA) current
+    array eX_;              // (mV) reversal potential
+    array Xi_;              // (mM) internal concentration
+    array Xo_;              // (mM) external concentration
+    array weight_Xi_;       // (1) concentration weight internal
+    array weight_Xo_;       // (1) concentration weight external
 
-    int charge;    // charge of ionic species
+    int charge;             // charge of ionic species
     fvm_value_type default_int_concentration; // (mM) default internal concentration
     fvm_value_type default_ext_concentration; // (mM) default external concentration
 
@@ -99,7 +99,7 @@ struct shared_state {
     array conductivity;       // Maps CV index to membrane conductivity [kS/m²].
     fvm_value_type temperature_degC;  // Global temperature [°C].
 
-    std::unordered_map<ionKind, ion_state> ion_data;
+    std::unordered_map<std::string, ion_state> ion_data;
 
     deliverable_event_stream deliverable_events;
 
@@ -113,6 +113,7 @@ struct shared_state {
     );
 
     void add_ion(
+        const std::string& ion_name,
         ion_info info,
         const std::vector<fvm_index_type>& cv,
         const std::vector<fvm_value_type>& iconc_norm_area,
diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp
index 1612734b..144aaf5b 100644
--- a/arbor/fvm_layout.cpp
+++ b/arbor/fvm_layout.cpp
@@ -306,10 +306,10 @@ fvm_mechanism_data fvm_build_mechanism_data(const mechanism_catalogue& catalogue
     // Record for each stimulus the CV and clamp data.
     std::vector<std::pair<size_type, i_clamp>> stimuli;
 
-    // Temporary table for presence of ion channels, mapping ionKind to _sorted_
+    // Temporary table for presence of ion channels, mapping ion name to _sorted_
     // collection of segment indices.
 
-    std::unordered_map<ionKind, std::set<size_type>> ion_segments;
+    std::unordered_map<std::string, std::set<size_type>> ion_segments;
 
     auto update_paramset_and_validate =
         [&catalogue]
diff --git a/arbor/fvm_layout.hpp b/arbor/fvm_layout.hpp
index 459fb513..e470a719 100644
--- a/arbor/fvm_layout.hpp
+++ b/arbor/fvm_layout.hpp
@@ -133,8 +133,8 @@ struct fvm_mechanism_data {
     // Mechanism config, indexed by mechanism name.
     std::unordered_map<std::string, fvm_mechanism_config> mechanisms;
 
-    // Ion config, indexed by ionKind.
-    std::unordered_map<ionKind, fvm_ion_config> ions;
+    // Ion config, indexed by ion name.
+    std::unordered_map<std::string, fvm_ion_config> ions;
 
     // Total number of targets (point-mechanism points)
     std::size_t ntarget = 0;
diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp
index c3e1d8da..2e5574fc 100644
--- a/arbor/fvm_lowered_cell_impl.hpp
+++ b/arbor/fvm_lowered_cell_impl.hpp
@@ -18,7 +18,6 @@
 
 #include <arbor/assert.hpp>
 #include <arbor/common_types.hpp>
-#include <arbor/ion.hpp>
 #include <arbor/recipe.hpp>
 
 #include "builtin_mechanisms.hpp"
@@ -404,10 +403,13 @@ void fvm_lowered_cell_impl<B>::initialize(
     // Instantiate mechanisms and ions.
 
     for (auto& i: mech_data.ions) {
-        ionKind kind = i.first;
+        const std::string& ion_name = i.first;
 
-        if (auto ion = value_by_key(global_props.ion_default, to_string(kind))) {
-            state_->add_ion(ion.value(), i.second.cv, i.second.iconc_norm_area, i.second.econc_norm_area);
+        if (auto ion = value_by_key(global_props.ion_default, ion_name)) {
+            state_->add_ion(ion_name, ion.value(), i.second.cv, i.second.iconc_norm_area, i.second.econc_norm_area);
+        }
+        else {
+            throw cable_cell_error("unrecognized ion '"+ion_name+"' in mechanism");
         }
     }
 
diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp
index dd865de8..14be4c23 100644
--- a/arbor/include/arbor/cable_cell.hpp
+++ b/arbor/include/arbor/cable_cell.hpp
@@ -7,7 +7,7 @@
 #include <arbor/arbexcept.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/constants.hpp>
-#include <arbor/ion.hpp>
+#include <arbor/ion_info.hpp>
 #include <arbor/mechcat.hpp>
 #include <arbor/morphology.hpp>
 #include <arbor/segment.hpp>
@@ -81,11 +81,10 @@ struct cable_cell_global_properties {
     //
     // Defaults below chosen to match NEURON.
 
-    // Ion species currently limited to just "ca", "na", "k".
     std::unordered_map<std::string, ion_info> ion_default = {
-        {"ca", { ionKind::ca, 2, 5e-5, 2.  }},
-        {"na", { ionKind::na, 1, 10.,  140.}},
-        {"k",  { ionKind::k,  1, 54.4, 2.5 }}
+        {"ca", { 2, 5e-5, 2.  }},
+        {"na", { 1, 10.,  140.}},
+        {"k",  { 1, 54.4, 2.5 }}
     };
 
     double temperature_K = constant::hh_squid_temp; // [K]
diff --git a/arbor/include/arbor/ion.hpp b/arbor/include/arbor/ion.hpp
deleted file mode 100644
index b0bd0c3c..00000000
--- a/arbor/include/arbor/ion.hpp
+++ /dev/null
@@ -1,30 +0,0 @@
-#pragma once
-
-#include <stdexcept>
-#include <string>
-
-namespace arb {
-
-// Fixed set of ion species (to be generalized in the future):
-
-enum class ionKind {ca, na, k};
-inline std::string to_string(ionKind k) {
-    switch (k) {
-    case ionKind::ca: return "ca";
-    case ionKind::na: return "na";
-    case ionKind::k:  return "k";
-    default: throw std::out_of_range("unknown ionKind");
-    }
-}
-
-// Ion (species) description
-
-struct ion_info {
-    ionKind kind;
-    int charge; // charge of ionic species
-    double default_int_concentration; // (mM) default internal concentration
-    double default_ext_concentration; // (mM) default external concentration
-};
-
-} // namespace arb
-
diff --git a/arbor/include/arbor/ion_info.hpp b/arbor/include/arbor/ion_info.hpp
new file mode 100644
index 00000000..a3e97263
--- /dev/null
+++ b/arbor/include/arbor/ion_info.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+namespace arb {
+
+struct ion_info {
+    int charge;                       // charge of ionic species
+    double default_int_concentration; // (mM) default internal concentration
+    double default_ext_concentration; // (mM) default external concentration
+};
+
+} // namespace arb
diff --git a/arbor/include/arbor/mechanism.hpp b/arbor/include/arbor/mechanism.hpp
index 8e17ad66..0fe32db4 100644
--- a/arbor/include/arbor/mechanism.hpp
+++ b/arbor/include/arbor/mechanism.hpp
@@ -5,7 +5,6 @@
 #include <vector>
 
 #include <arbor/fvm_types.hpp>
-#include <arbor/ion.hpp>
 #include <arbor/mechinfo.hpp>
 
 namespace arb {
diff --git a/arbor/include/arbor/mechinfo.hpp b/arbor/include/arbor/mechinfo.hpp
index dce3c70d..5e23e2ea 100644
--- a/arbor/include/arbor/mechinfo.hpp
+++ b/arbor/include/arbor/mechinfo.hpp
@@ -10,8 +10,6 @@
 #include <utility>
 #include <vector>
 
-#include <arbor/ion.hpp>
-
 namespace arb {
 
 struct mechanism_field_spec {
@@ -34,6 +32,7 @@ struct mechanism_field_spec {
 struct ion_dependency {
     bool write_concentration_int;
     bool write_concentration_ext;
+    bool write_reversal_potential;
 };
 
 // A hash of the mechanism dynamics description is used to ensure that offline-compiled
@@ -57,7 +56,7 @@ struct mechanism_info {
     std::unordered_map<std::string, mechanism_field_spec> state;
 
     // Ion dependencies.
-    std::unordered_map<ionKind, ion_dependency> ions;
+    std::unordered_map<std::string, ion_dependency> ions;
 
     mechanism_fingerprint fingerprint;
 
diff --git a/modcc/blocks.hpp b/modcc/blocks.hpp
index 868f60bd..118077df 100644
--- a/modcc/blocks.hpp
+++ b/modcc/blocks.hpp
@@ -11,9 +11,6 @@
 
 // describes a relationship with an ion channel
 struct IonDep {
-    ionKind kind() const {
-        return to_ionKind(name);
-    }
     std::string name;         // name of ion channel
     std::vector<Token> read;  // name of channels parameters to write
     std::vector<Token> write; // name of channels parameters to read
@@ -40,6 +37,9 @@ struct IonDep {
     bool writes_concentration_ext() const {
         return writes_variable(name+"o");
     };
+    bool writes_rev_potential() const {
+        return writes_variable("e"+name);
+    };
 
     bool reads_variable(const std::string& name) const {
         return std::find_if(read.begin(), read.end(),
diff --git a/modcc/expression.cpp b/modcc/expression.cpp
index 960339f8..2d4accd1 100644
--- a/modcc/expression.cpp
+++ b/modcc/expression.cpp
@@ -265,8 +265,8 @@ std::string VariableExpression::to_string() const {
           + colorize("write", is_writeable() ? stringColor::green : stringColor::red) + ", "
           + colorize("read", is_readable() ? stringColor::green : stringColor::red)   + ", "
           + (is_range() ? "range" : "scalar")                 + ", "
-          + "ion" + colorize(::to_string(ion_channel()),
-                             (ion_channel()==ionKind::none) ? stringColor::red : stringColor::green) + ", "
+          + "ion" + (is_ion()? colorize(ion_channel(), stringColor::green)
+                             : colorize("none", stringColor::red)) + ", "
           + "vis "  + ::to_string(visibility()) + ", "
           + "link " + ::to_string(linkage())    + ", "
           + colorize("state", is_state() ? stringColor::green : stringColor::red) + ")";
@@ -278,11 +278,11 @@ std::string VariableExpression::to_string() const {
 *******************************************************************************/
 
 std::string IndexedVariable::to_string() const {
-    auto ch = ::to_string(ion_channel());
     return
         blue("indexed") + " " + yellow(name()) + "->" + yellow(index_name()) + "("
         + (is_write() ? " write-only" : " read-only")
-        + ", ion" + (ion_channel()==ionKind::none ? red(ch) : green(ch)) + ") ";
+        + ", ion" + (is_ion()? colorize(ion_channel(), stringColor::green)
+                             : colorize("none", stringColor::red)) + ") ";
 }
 
 /*******************************************************************************
diff --git a/modcc/expression.hpp b/modcc/expression.hpp
index 12b9fc5f..2157d396 100644
--- a/modcc/expression.hpp
+++ b/modcc/expression.hpp
@@ -9,8 +9,8 @@
 
 #include "error.hpp"
 #include "identifier.hpp"
-#include "memop.hpp"
 #include "scope.hpp"
+#include "token.hpp"
 
 #include "io/pprintf.hpp"
 
@@ -455,8 +455,8 @@ public:
     void range(rangeKind r) {
         range_kind_ = r;
     }
-    void ion_channel(ionKind i) {
-        ion_channel_ = i;
+    void ion_channel(std::string i) {
+        ion_channel_ = std::move(i);
     }
     void state(bool s) {
         is_state_ = s;
@@ -474,7 +474,7 @@ public:
     linkageKind linkage() const {
         return linkage_;
     }
-    ionKind ion_channel() const {
+    const std::string& ion_channel() const {
         return ion_channel_;
     }
 
@@ -482,7 +482,7 @@ public:
         return shadows_;
     }
 
-    bool is_ion()       const {return ion_channel_ != ionKind::none;}
+    bool is_ion()       const {return !ion_channel_.empty();}
     bool is_state()     const {return is_state_;}
     bool is_range()     const {return range_kind_  == rangeKind::range;}
     bool is_scalar()    const {return !is_range();}
@@ -507,7 +507,7 @@ protected:
     visibilityKind visibility_  = visibilityKind::local;
     linkageKind    linkage_     = linkageKind::external;
     rangeKind      range_kind_  = rangeKind::range;
-    ionKind        ion_channel_ = ionKind::none;
+    std::string    ion_channel_ = "";
     double         value_       = std::numeric_limits<double>::quiet_NaN();
     Symbol*        shadows_     = nullptr;
 };
@@ -521,11 +521,11 @@ public:
                     sourceKind data_source,
                     accessKind acc,
                     tok o=tok::eq,
-                    ionKind channel=ionKind::none)
+                    std::string channel="")
     :   Symbol(std::move(loc), std::move(lookup_name), symbolKind::indexed_variable),
         access_(acc),
-        ion_channel_(channel),
-        index_name_(index_name), // (TODO: deprecate/remove this...)
+        ion_channel_(std::move(channel)),
+        index_name_(std::move(index_name)), // (TODO: deprecate/remove this...)
         data_source_(data_source),
         op_(o)
     {
@@ -558,12 +558,13 @@ public:
     std::string to_string() const override;
 
     accessKind access() const { return access_; }
-    ionKind ion_channel() const { return ion_channel_; }
+    std::string ion_channel() const { return ion_channel_; }
     sourceKind data_source() const { return data_source_; }
+    void data_source(sourceKind k) { data_source_ = k; }
     std::string const& index_name() const { return index_name_; }
     tok op() const { return op_; }
 
-    bool is_ion()   const { return ion_channel_ != ionKind::none; }
+    bool is_ion()   const { return !ion_channel_.empty(); }
     bool is_read()  const { return access_ == accessKind::read;   }
     bool is_write() const { return access_ == accessKind::write;  }
 
@@ -573,7 +574,7 @@ public:
     ~IndexedVariable() {}
 protected:
     accessKind  access_;
-    ionKind     ion_channel_;
+    std::string ion_channel_;
     std::string index_name_; // hint to printer only
     sourceKind  data_source_;
     tok op_;
@@ -597,12 +598,11 @@ public :
     }
 
     bool is_indexed() const {
-        return external_!=nullptr && ion_channel()!=ionKind::nonspecific;
+        return external_!=nullptr && external_->data_source()!=sourceKind::no_source;
     }
 
-    ionKind ion_channel() const {
-        if(external_) return external_->ion_channel();
-        return ionKind::none;
+    std::string ion_channel() const {
+        return external_? external_->ion_channel(): "";
     }
 
     bool is_read() const {
@@ -695,20 +695,20 @@ public:
     ConductanceExpression(
             Location loc,
             std::string name,
-            ionKind channel)
-    :   Expression(loc), name_(std::move(name)), ion_channel_(channel)
+            std::string channel)
+    :   Expression(loc), name_(std::move(name)), ion_channel_(std::move(channel))
     {}
 
     std::string to_string() const override {
         return blue("conductance") + "(" + yellow(name_) + ", "
-            + green(::to_string(ion_channel_)) + ")";
+            + green(ion_channel_.empty()? "none": ion_channel_) + ")";
     }
 
     std::string const& name() const {
         return name_;
     }
 
-    ionKind ion_channel() const {
+    std::string const& ion_channel() const {
         return ion_channel_;
     }
 
@@ -725,7 +725,7 @@ public:
 private:
     /// pointer to the variable symbol for the state variable to be solved for
     std::string name_;
-    ionKind ion_channel_;
+    std::string ion_channel_;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -1041,8 +1041,6 @@ protected:
 
 class APIMethod : public ProcedureExpression {
 public:
-    using memop_type = MemOp<Symbol>;
-
     APIMethod( Location loc,
                std::string name,
                std::vector<expression_ptr>&& args,
diff --git a/modcc/identifier.hpp b/modcc/identifier.hpp
index 891e6f49..58aea569 100644
--- a/modcc/identifier.hpp
+++ b/modcc/identifier.hpp
@@ -33,15 +33,6 @@ enum class linkageKind {
     external
 };
 
-/// ion channel that the variable belongs to
-enum class ionKind {
-    none,     ///< not an ion variable
-    nonspecific,  ///< nonspecific current
-    Ca,       ///< calcium ion
-    Na,       ///< sodium ion
-    K         ///< potassium ion
-};
-
 /// possible external data source for indexed variables
 enum class sourceKind {
     voltage,
@@ -64,25 +55,6 @@ inline std::string yesno(bool val) {
 // to_string functions convert types
 // to strings for printing diagnostics
 ////////////////////////////////////////////
-inline std::string to_string(ionKind i) {
-    switch(i) {
-        case ionKind::Ca   : return std::string("ca");
-        case ionKind::Na   : return std::string("na");
-        case ionKind::K    : return std::string("k");
-        case ionKind::none : return std::string("none");
-        case ionKind::nonspecific : return std::string("nonspecific");
-    }
-    throw std::runtime_error("unknown ionKind");
-}
-
-inline ionKind to_ionKind(const std::string& s) {
-    if(s=="k") return ionKind::K;
-    if(s=="na") return ionKind::Na;
-    if(s=="ca") return ionKind::Ca;
-    if(s=="none") return ionKind::Ca;
-    if(s=="nonspecific") return ionKind::nonspecific;
-    throw std::runtime_error("invalid ion description string");
-}
 
 inline std::string to_string(visibilityKind v) {
     switch(v) {
@@ -101,9 +73,6 @@ inline std::string to_string(linkageKind v) {
 }
 
 // ostream writers
-inline std::ostream& operator<< (std::ostream& os, ionKind i) {
-    return os << to_string(i);
-}
 
 inline std::ostream& operator<< (std::ostream& os, visibilityKind v) {
     return os << to_string(v);
@@ -115,25 +84,12 @@ inline std::ostream& operator<< (std::ostream& os, linkageKind l) {
 
 /// ion variable to data source kind
 
-inline sourceKind ion_source(ionKind i, const std::string& var) {
-    std::string ion = to_string(i);
-    if (var=="i"+ion) return sourceKind::ion_current;
+inline sourceKind ion_source(const std::string& ion, const std::string& var) {
+    if (ion.empty()) return sourceKind::no_source;
+    else if (var=="i"+ion) return sourceKind::ion_current;
     else if (var=="e"+ion) return sourceKind::ion_revpot;
     else if (var==ion+"i") return sourceKind::ion_iconc;
     else if (var==ion+"e") return sourceKind::ion_econc;
     else return sourceKind::no_source;
 }
 
-// TODO: deprecate; back-end dependent.
-inline std::string ion_store(ionKind k) {
-    switch(k) {
-        case ionKind::Ca:
-            return "ion_ca";
-        case ionKind::Na:
-            return "ion_na";
-        case ionKind::K:
-            return "ion_k";
-        default:
-            return "";
-    }
-}
diff --git a/modcc/module.cpp b/modcc/module.cpp
index 97debcad..3c86d0de 100644
--- a/modcc/module.cpp
+++ b/modcc/module.cpp
@@ -26,15 +26,20 @@ class NrnCurrentRewriter: public BlockRewriterBase {
         return id(name, loc_);
     }
 
-    static ionKind is_ion_update(Expression* e) {
+    static sourceKind current_update(Expression* e) {
         if(auto a = e->is_assignment()) {
             if(auto sym = a->lhs()->is_identifier()->symbol()) {
                 if(auto var = sym->is_local_variable()) {
-                    return var->ion_channel();
+                    if(auto ext = var->external_variable()) {
+                        sourceKind src = ext->data_source();
+                        if (src==sourceKind::current || src==sourceKind::ion_current) {
+                            return src;
+                        }
+                    }
                 }
             }
         }
-        return ionKind::none;
+        return sourceKind::no_source;
     }
 
     bool has_current_update_ = false;
@@ -81,12 +86,20 @@ public:
         statements_.push_back(e->clone());
         auto loc = e->location();
 
-        auto update_kind = is_ion_update(e);
-        if (update_kind!=ionKind::none) {
-            if (update_kind!=ionKind::nonspecific) {
+        sourceKind current_source = current_update(e);
+        if (current_source != sourceKind::no_source) {
+            has_current_update_ = true;
+
+            if (current_source==sourceKind::ion_current) {
                 ion_current_vars_.insert(e->lhs()->is_identifier()->name());
             }
-            has_current_update_ = true;
+            else {
+                // A 'nonspecific' current contribution.
+                // Remove data source; currents accumulated into `current_` instead.
+
+                e->lhs()->is_identifier()->symbol()->is_local_variable()
+                    ->external_variable()->data_source(sourceKind::no_source);
+            }
 
             linear_test_result L = linear_test(e->rhs(), {"v"});
             if (!L.is_linear) {
@@ -497,7 +510,7 @@ void Module::add_variables_to_symbols() {
     // add indexed variables to the table
     auto create_indexed_variable = [this]
         (std::string const& name, std::string const& indexed_name, sourceKind data_source,
-         tok op, accessKind acc, ionKind ch, Location loc) -> symbol_ptr&
+         tok op, accessKind acc, std::string ch, Location loc) -> symbol_ptr&
     {
         if(symbols_.count(name)) {
             throw compiler_exception(
@@ -508,13 +521,13 @@ void Module::add_variables_to_symbols() {
     };
 
     create_indexed_variable("current_", "vec_i", sourceKind::current, tok::plus,
-                            accessKind::write, ionKind::none, Location());
+                            accessKind::write, "", Location());
     create_indexed_variable("conductivity_", "vec_g", sourceKind::conductivity, tok::plus,
-                            accessKind::write, ionKind::none, Location());
+                            accessKind::write, "", Location());
     create_indexed_variable("v", "vec_v", sourceKind::voltage, tok::eq,
-                            accessKind::read,  ionKind::none, Location());
+                            accessKind::read,  "", Location());
     create_indexed_variable("dt", "vec_dt", sourceKind::dt, tok::eq,
-                            accessKind::read,  ionKind::none, Location());
+                            accessKind::read,  "", Location());
 
     // If we put back support for accessing cell time again from NMODL code,
     // add indexed_variable also for "time" with appropriate cell-index based
@@ -537,7 +550,7 @@ void Module::add_variables_to_symbols() {
 
         if (id.name() == "celsius") {
             create_indexed_variable("celsius", "celsius",
-                sourceKind::temperature, tok::eq, accessKind::read, ionKind::none, Location());
+                sourceKind::temperature, tok::eq, accessKind::read, "", Location());
         }
         else {
             // Parameters are scalar by default, but may later be changed to range.
@@ -575,7 +588,7 @@ void Module::add_variables_to_symbols() {
     // first the ION channels
     // add ion channel variables
     auto update_ion_symbols = [this, create_indexed_variable]
-            (Token const& tkn, accessKind acc, ionKind channel)
+            (Token const& tkn, accessKind acc, const std::string& channel)
     {
         std::string name = tkn.spelling;
         sourceKind data_source = ion_source(channel, name);
@@ -610,18 +623,23 @@ void Module::add_variables_to_symbols() {
         }
     };
 
-    // check for nonspecific current
+    // Nonspecific current variables are represented by an indexed variable
+    // with a 'current' data source. Assignments in the NrnCurrent block will
+    // later be rewritten so that these contributions are accumulated in `current_`
+    // (potentially saving some weight multiplications); at that point the
+    // data source for the nonspecific current variable will be reset to 'no_source'.
+
     if( neuron_block_.has_nonspecific_current() ) {
         auto const& i = neuron_block_.nonspecific_current;
-        update_ion_symbols(i, accessKind::write, ionKind::nonspecific);
+        create_indexed_variable(i.spelling, "", sourceKind::current, tok::plus, accessKind::write, "", i.location);
     }
 
     for(auto const& ion : neuron_block_.ions) {
         for(auto const& var : ion.read) {
-            update_ion_symbols(var, accessKind::read, ion.kind());
+            update_ion_symbols(var, accessKind::read, ion.name);
         }
         for(auto const& var : ion.write) {
-            update_ion_symbols(var, accessKind::write, ion.kind());
+            update_ion_symbols(var, accessKind::write, ion.name);
         }
     }
 
diff --git a/modcc/module.hpp b/modcc/module.hpp
index dbe8c1ff..4cf5e6ac 100644
--- a/modcc/module.hpp
+++ b/modcc/module.hpp
@@ -91,16 +91,16 @@ public:
     // Perform semantic analysis pass.
     bool semantic();
 
-    auto find_ion(ionKind k) -> decltype(ion_deps().begin()) {
+    auto find_ion(const std::string& ion_name) -> decltype(ion_deps().begin()) {
         auto& ions = neuron_block().ions;
         return std::find_if(
             ions.begin(), ions.end(),
-            [k](IonDep const& d) {return d.kind()==k;}
+            [&ion_name](IonDep const& d) {return d.name==ion_name;}
         );
     };
 
-    bool has_ion(ionKind k) {
-        return find_ion(k) != neuron_block().ions.end();
+    bool has_ion(const std::string& ion_name) {
+        return find_ion(ion_name) != neuron_block().ions.end();
     };
 
     bool is_linear() const { return linear_; }
diff --git a/modcc/parser.cpp b/modcc/parser.cpp
index 1598711a..d2fd3a02 100644
--- a/modcc/parser.cpp
+++ b/modcc/parser.cpp
@@ -304,16 +304,10 @@ void Parser::parse_neuron_block() {
                     get_token();
                     // check this is an identifier token
                     if(token_.type != tok::identifier) {
-                        error(pprintf("invalid name for an ion chanel '%'",
-                                      token_.spelling));
-                        return;
-                    }
-                    // check that the ion type is valid (insist on lower case?)
-                    if(!(token_.spelling == "k" || token_.spelling == "ca" || token_.spelling == "na")) {
-                        error(pprintf("invalid ion type % must be on eof 'k' 'ca' or 'na'",
-                                      yellow(token_.spelling)));
+                        error(pprintf("invalid name for an ion chanel '%'", token_.spelling));
                         return;
                     }
+
                     ion.name = token_.spelling;
                     get_token(); // consume the ion name
 
@@ -1464,7 +1458,7 @@ expression_ptr Parser::parse_conductance() {
     int line = location_.line;
     Location loc = location_; // solve location for expression
     std::string name;
-    ionKind channel;
+    std::string channel;
 
     get_token(); // consume the CONDUCTANCE keyword
 
@@ -1473,20 +1467,11 @@ expression_ptr Parser::parse_conductance() {
     name = token_.spelling; // save name of variable
     get_token(); // consume the variable identifier
 
-    if(token_.type != tok::useion) { // no ion channel was provided
-        // we set nonspecific not none because ionKind::none marks
-        // any variable that is not associated with an ion channel
-        channel = ionKind::nonspecific;
-    }
-    else {
+    if(token_.type == tok::useion) {
         get_token(); // consume the USEION keyword
         if(token_.type!=tok::identifier) goto conductance_statement_error;
 
-        if     (token_.spelling == "na") channel = ionKind::Na;
-        else if(token_.spelling == "ca") channel = ionKind::Ca;
-        else if(token_.spelling == "k")  channel = ionKind::K;
-        else goto conductance_statement_error;
-
+        channel = token_.spelling;
         get_token(); // consume the ion channel type
     }
     // check that the rest of the line was empty
diff --git a/modcc/printer/cprinter.cpp b/modcc/printer/cprinter.cpp
index 1c1ce229..5560ff22 100644
--- a/modcc/printer/cprinter.cpp
+++ b/modcc/printer/cprinter.cpp
@@ -216,7 +216,6 @@ std::string emit_cpp_source(const Module& module_, const printer_options& opt) {
     out <<
         "\n" << popindent <<
         "protected:\n" << indent <<
-        "using ionKind = ::arb::ionKind;\n\n"
         "std::size_t object_sizeof() const override { return sizeof(*this); }\n";
 
     io::separator sep("\n", ",\n");
@@ -280,14 +279,14 @@ std::string emit_cpp_source(const Module& module_, const printer_options& opt) {
 
         sep.reset();
         for (const auto& dep: ion_deps) {
-            out << sep << "{ionKind::" << dep.name << ", &" << ion_state_field(dep.name) << "}";
+            out << sep << "{\"" << dep.name << "\", &" << ion_state_field(dep.name) << "}";
         }
         out << popindent << "\n};" << popindent << "\n}\n";
 
         sep.reset();
         out << "mechanism_ion_index_table ion_index_table() override {\n" << indent << "return {" << indent;
         for (const auto& dep: ion_deps) {
-            out << sep << "{ionKind::" << dep.name << ", &" << ion_state_index(dep.name) << "}";
+            out << sep << "{\"" << dep.name << "\", &" << ion_state_index(dep.name) << "}";
         }
         out << popindent << "\n};" << popindent << "\n}\n";
     }
diff --git a/modcc/printer/cudaprinter.cpp b/modcc/printer/cudaprinter.cpp
index e84200f2..91f69c03 100644
--- a/modcc/printer/cudaprinter.cpp
+++ b/modcc/printer/cudaprinter.cpp
@@ -40,11 +40,11 @@ std::string make_ppack_name(const std::string& module_name) {
     return make_class_name(module_name)+"_pp_";
 }
 
-static std::string ion_state_field(std::string ion_name) {
+static std::string ion_state_field(const std::string& ion_name) {
     return "ion_"+ion_name+"_";
 }
 
-static std::string ion_state_index(std::string ion_name) {
+static std::string ion_state_index(const std::string& ion_name) {
     return "ion_"+ion_name+"_index_";
 }
 
@@ -117,7 +117,6 @@ std::string emit_cuda_cpp_source(const Module& module_, const printer_options& o
 
     out << popindent <<
         "protected:\n" << indent <<
-        "using ionKind = ::arb::ionKind;\n\n"
         "std::size_t object_sizeof() const override { return sizeof(*this); }\n"
         "::arb::gpu::mechanism_ppack_base* ppack_ptr() { return &pp_; }\n\n";
 
@@ -183,14 +182,14 @@ std::string emit_cuda_cpp_source(const Module& module_, const printer_options& o
 
         sep.reset();
         for (const auto& dep: ion_deps) {
-            out << sep << "{ionKind::" << dep.name << ", &pp_." << ion_state_field(dep.name) << "}";
+            out << sep << "{\"" << dep.name << "\", &pp_." << ion_state_field(dep.name) << "}";
         }
         out << popindent << "\n};" << popindent << "\n}\n";
 
         sep.reset();
         out << "mechanism_ion_index_table ion_index_table() override {\n" << indent << "return {" << indent;
         for (const auto& dep: ion_deps) {
-            out << sep << "{ionKind::" << dep.name << ", &pp_." << ion_state_index(dep.name) << "}";
+            out << sep << "{\"" << dep.name << "\", &pp_." << ion_state_index(dep.name) << "}";
         }
         out << popindent << "\n};" << popindent << "\n}\n";
     }
diff --git a/modcc/printer/infoprinter.cpp b/modcc/printer/infoprinter.cpp
index 7c69918e..5e9fe0b9 100644
--- a/modcc/printer/infoprinter.cpp
+++ b/modcc/printer/infoprinter.cpp
@@ -44,9 +44,10 @@ std::ostream& operator<<(std::ostream& out, const ion_dep_info& wrap) {
     const char* boolalpha[2] = {"false", "true"};
     const IonDep& ion = wrap.ion;
 
-    return out << "{ionKind::" << ion.name << ", {"
+    return out << "{\"" << ion.name << "\", {"
         << boolalpha[ion.writes_concentration_int()] << ", "
-        << boolalpha[ion.writes_concentration_ext()] << "}}";
+        << boolalpha[ion.writes_concentration_ext()] << ", "
+        << boolalpha[ion.writes_rev_potential()] << "}}";
 }
 
 std::string build_info_header(const Module& m, const printer_options& opt) {
@@ -73,7 +74,6 @@ std::string build_info_header(const Module& m, const printer_options& opt) {
         "\n"
         "inline const ::arb::mechanism_info& mechanism_" << name << "_info() {\n"
         << indent <<
-        "using ::arb::ionKind;\n"
         "using spec = ::arb::mechanism_field_spec;\n"
         "static mechanism_info info = {\n"
         << indent <<
diff --git a/modcc/printer/printerutil.cpp b/modcc/printer/printerutil.cpp
index f5f95cf5..ca42489c 100644
--- a/modcc/printer/printerutil.cpp
+++ b/modcc/printer/printerutil.cpp
@@ -118,7 +118,7 @@ indexed_variable_info decode_indexed_variable(IndexedVariable* sym) {
     std::string index_var = "node_index_";
 
     if (sym->is_ion()) {
-        ion_pfx = "ion_"+to_string(sym->ion_channel())+"_";
+        ion_pfx = "ion_"+sym->ion_channel()+"_";
         index_var = ion_pfx+"index_";
     }
 
diff --git a/modcc/symdiff.cpp b/modcc/symdiff.cpp
index f9f6a4bc..75a1b76a 100644
--- a/modcc/symdiff.cpp
+++ b/modcc/symdiff.cpp
@@ -8,6 +8,7 @@
 #include "error.hpp"
 #include "expression.hpp"
 #include "symdiff.hpp"
+#include "util.hpp"
 #include "visitor.hpp"
 
 class FindIdentifierVisitor: public Visitor {
diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp
index 0b9496a0..a4a1541f 100644
--- a/test/unit/test_fvm_layout.cpp
+++ b/test/unit/test_fvm_layout.cpp
@@ -324,12 +324,12 @@ TEST(fvm_layout, mech_index) {
     // There should be a K and Na ion channel associated with each
     // hh mechanism node.
 
-    ASSERT_EQ(1u, M.ions.count(ionKind::na));
-    ASSERT_EQ(1u, M.ions.count(ionKind::k));
-    EXPECT_EQ(0u, M.ions.count(ionKind::ca));
+    ASSERT_EQ(1u, M.ions.count("na"s));
+    ASSERT_EQ(1u, M.ions.count("k"s));
+    EXPECT_EQ(0u, M.ions.count("ca"s));
 
-    EXPECT_EQ(ivec({0,5}), M.ions.at(ionKind::na).cv);
-    EXPECT_EQ(ivec({0,5}), M.ions.at(ionKind::k).cv);
+    EXPECT_EQ(ivec({0,5}), M.ions.at("na"s).cv);
+    EXPECT_EQ(ivec({0,5}), M.ions.at("k"s).cv);
 }
 
 TEST(fvm_layout, coalescing_synapses) {
@@ -755,8 +755,8 @@ TEST(fvm_layout, ion_weights) {
         fvm_discretization D = fvm_discretize(cells);
         fvm_mechanism_data M = fvm_build_mechanism_data(global_default_catalogue(), cells, D);
 
-        ASSERT_EQ(1u, M.ions.count(ionKind::ca));
-        auto& ca = M.ions.at(ionKind::ca);
+        ASSERT_EQ(1u, M.ions.count("ca"s));
+        auto& ca = M.ions.at("ca"s);
 
         EXPECT_EQ(expected_ion_cv[run], ca.cv);
         EXPECT_TRUE(testing::seq_almost_eq<fvm_value_type>(expected_iconc_norm_area[run], ca.iconc_norm_area));
diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp
index cdc09371..59fa34d1 100644
--- a/test/unit/test_fvm_lowered.cpp
+++ b/test/unit/test_fvm_lowered.cpp
@@ -58,7 +58,7 @@ arb::mechanism* find_mechanism(fvm_cell& fvcell, const std::string& name) {
 
 using mechanism_global_table = std::vector<std::pair<const char*, arb::fvm_value_type*>>;
 using mechanism_field_table = std::vector<std::pair<const char*, arb::fvm_value_type**>>;
-using mechanism_ion_index_table = std::vector<std::pair<arb::ionKind, backend::iarray*>>;
+using mechanism_ion_index_table = std::vector<std::pair<const char*, backend::iarray*>>;
 
 ACCESS_BIND(\
     mechanism_global_table (arb::multicore::mechanism::*)(),\
@@ -534,7 +534,7 @@ TEST(fvm_lowered, weighted_write_ion) {
     fvcell.initialize({0}, cable1d_recipe(c), cell_to_intdom, targets, probe_map);
 
     auto& state = *(fvcell.*private_state_ptr).get();
-    auto& ion = state.ion_data.at(ionKind::ca);
+    auto& ion = state.ion_data.at("ca"s);
     ion.default_int_concentration = con_int;
     ion.default_ext_concentration = con_ext;
     ion.init_concentration();
@@ -553,7 +553,7 @@ TEST(fvm_lowered, weighted_write_ion) {
     ASSERT_TRUE(opt_cai_ptr);
     auto& test_ca_cai = *opt_cai_ptr.value();
 
-    auto opt_ca_index_ptr = util::value_by_key((test_ca->*private_ion_index_table_ptr)(), ionKind::ca);
+    auto opt_ca_index_ptr = util::value_by_key((test_ca->*private_ion_index_table_ptr)(), "ca"s);
     ASSERT_TRUE(opt_ca_index_ptr);
     auto& test_ca_ca_index = *opt_ca_index_ptr.value();
 
-- 
GitLab