User Guide
parser.cpp
Go to the documentation of this file.
1 /*
2  * Copyright 2023 Blue Brain Project, EPFL.
3  * See the top-level LICENSE file for details.
4  *
5  * SPDX-License-Identifier: Apache-2.0
6  */
7 
8 #include <string>
9 #include <utility>
10 
11 #include <catch2/catch_test_macros.hpp>
12 #include <catch2/matchers/catch_matchers_string.hpp>
13 
14 #include "ast/program.hpp"
15 #include "lexer/modtoken.hpp"
16 #include "parser/diffeq_driver.hpp"
17 #include "parser/nmodl_driver.hpp"
20 #include "utils/common_utils.hpp"
23 
24 
25 using namespace nmodl::test_utils;
26 //=============================================================================
27 // Parser tests
28 //=============================================================================
29 
30 bool is_valid_construct(const std::string& construct) {
32  return driver.parse_string(construct) != nullptr;
33 }
34 
35 
36 SCENARIO("NMODL can accept CR as return char for one line comment", "[parser]") {
37  GIVEN("A comment defined with CR as return char") {
38  WHEN("parsing") {
39  THEN("success") {
40  REQUIRE(is_valid_construct(R"(: see you next line
41 PROCEDURE foo() {
42  })"));
43  }
44  }
45  }
46 }
47 
48 SCENARIO("NMODL can define macros using DEFINE keyword", "[parser]") {
49  GIVEN("A valid macro definition") {
50  WHEN("DEFINE NSTEP 6") {
51  THEN("parser accepts without an error") {
52  REQUIRE(is_valid_construct("DEFINE NSTEP 6"));
53  }
54  }
55  WHEN("DEFINE NSTEP 6") {
56  THEN("parser accepts without an error") {
57  REQUIRE(is_valid_construct("DEFINE NSTEP 6"));
58  }
59  }
60  }
61 
62  GIVEN("A macro with nested definition is not supported") {
63  WHEN("DEFINE SIX 6 DEFINE NSTEP SIX") {
64  THEN("parser throws an error") {
65  REQUIRE_THROWS_WITH(is_valid_construct("DEFINE SIX 6 DEFINE NSTEP SIX"),
66  Catch::Matchers::ContainsSubstring("unexpected INVALID_TOKEN"));
67  }
68  }
69  }
70 
71  GIVEN("A invalid macro definition with float value") {
72  WHEN("DEFINE NSTEP 6.0") {
73  THEN("parser throws an exception") {
74  REQUIRE_THROWS_WITH(is_valid_construct("DEFINE NSTEP 6.0"),
75  Catch::Matchers::ContainsSubstring("unexpected REAL"));
76  }
77  }
78  }
79 
80  GIVEN("A invalid macro definition with name and without value") {
81  WHEN("DEFINE NSTEP") {
82  THEN("parser throws an exception") {
83  REQUIRE_THROWS_WITH(is_valid_construct("DEFINE NSTEP"),
84  Catch::Matchers::ContainsSubstring("expecting INTEGER"));
85  }
86  }
87  }
88 
89  GIVEN("A invalid macro definition with name and value as a name") {
90  WHEN("DEFINE NSTEP SIX") {
91  THEN("parser throws an exception") {
92  REQUIRE_THROWS_WITH(is_valid_construct("DEFINE NSTEP SIX"),
93  Catch::Matchers::ContainsSubstring("expecting INTEGER"));
94  }
95  }
96  }
97 
98  GIVEN("A invalid macro definition without name but with value") {
99  WHEN("DEFINE 6") {
100  THEN("parser throws an exception") {
101  REQUIRE_THROWS_WITH(is_valid_construct("DEFINE 6"),
102  Catch::Matchers::ContainsSubstring("expecting NAME"));
103  }
104  }
105  }
106 }
107 
108 SCENARIO("Macros can be used anywhere in the mod file") {
109  std::string nmodl_text = R"(
110  DEFINE NSTEP 6
111  PARAMETER {
112  amp[NSTEP] (mV)
113  }
114  )";
115  WHEN("macro is used in parameter definition") {
116  THEN("parser accepts without an error") {
117  REQUIRE(is_valid_construct(nmodl_text));
118  }
119  }
120 }
121 
122 SCENARIO("NMODL parser accepts empty unit specification") {
123  std::string nmodl_text = R"(
124  FUNCTION ssCB(kdf(), kds()) (mM) {
125 
126  }
127  )";
128  WHEN("FUNCTION is defined with empty unit") {
129  THEN("parser accepts without an error") {
130  REQUIRE(is_valid_construct(nmodl_text));
131  }
132  }
133 }
134 
135 SCENARIO("NMODL parser running number of valid NMODL constructs") {
136  for (const auto& construct: nmodl_valid_constructs) {
137  auto test_case = construct.second;
138  GIVEN(test_case.name) {
139  THEN("Parser successfully parses : " + test_case.input) {
140  REQUIRE(is_valid_construct(test_case.input));
141  }
142  }
143  }
144 }
145 
146 SCENARIO("NMODL parser running number of invalid NMODL constructs") {
147  for (const auto& construct: nmodl_invalid_constructs) {
148  auto test_case = construct.second;
149  GIVEN(test_case.name) {
150  THEN("Parser throws an exception while parsing : " + test_case.input) {
151  REQUIRE_THROWS(is_valid_construct(test_case.input));
152  }
153  }
154  }
155 }
156 
157 //=============================================================================
158 // Ensure that the parser can handle invalid INCLUDE constructs
159 //=============================================================================
160 
161 SCENARIO("Check that the parser doesn't crash when passing invalid INCLUDE constructs") {
162  GIVEN("An empty filename") {
163  REQUIRE_THROWS_WITH(is_valid_construct("INCLUDE \"\""),
164  Catch::Matchers::ContainsSubstring("empty filename"));
165  }
166 
167  GIVEN("An missing included file") {
168  REQUIRE_THROWS_WITH(is_valid_construct("INCLUDE \"unknown.file\""),
169  Catch::Matchers::ContainsSubstring(
170  "can not open file : \"unknown.file\""));
171  }
172 
173  GIVEN("An invalid included file") {
174  REQUIRE_THROWS_WITH(is_valid_construct("INCLUDE \"included.file\""),
175  Catch::Matchers::ContainsSubstring("unexpected End of file"));
176  }
177 }
178 
179 SCENARIO("NEURON block can add CURIE information", "[parser][represents]") {
180  GIVEN("A valid CURIE information statement") {
181  THEN("parser accepts without an error") {
182  REQUIRE(is_valid_construct("NEURON { REPRESENTS NCIT:C17008 }"));
183  REQUIRE(is_valid_construct("NEURON { REPRESENTS [NCIT:C17008] }"));
184  }
185  }
186 
187  GIVEN("Incomplete CURIE information statement") {
188  THEN("parser throws an error") {
189  REQUIRE_THROWS_WITH(is_valid_construct("NEURON { REPRESENTS }"),
190  Catch::Matchers::ContainsSubstring("Lexer Error"));
191  REQUIRE_THROWS_WITH(is_valid_construct("NEURON { REPRESENTS NCIT}"),
192  Catch::Matchers::ContainsSubstring("Lexer Error"));
193  }
194  }
195 }
196 
197 
198 SCENARIO("Check parents in valid NMODL constructs") {
200  for (const auto& construct: nmodl_valid_constructs) {
201  // parse the string and get the ast
202  const auto ast = driver.parse_string(construct.second.input);
203  GIVEN(construct.second.name) {
204  THEN("Check the parents in : " + construct.second.input) {
205  // check the parents
206  REQUIRE(!nmodl::visitor::test::CheckParentVisitor().check_ast(*ast));
207  }
208  }
209  }
210 }
211 
212 //=============================================================================
213 // Differential Equation Parser tests
214 //=============================================================================
215 
216 std::string solve_construct(const std::string& equation, std::string method) {
217  auto solution = nmodl::parser::DiffeqDriver::solve(equation, std::move(method));
218  return solution;
219 }
220 
221 SCENARIO("Legacy differential equation solver") {
222  GIVEN("A differential equation") {
223  int counter = 0;
224  for (const auto& test_case: diff_eq_constructs) {
225  auto prefix = "." + std::to_string(counter);
226  WHEN(prefix + " EQUATION = " + test_case.equation + " METHOD = " + test_case.method) {
227  THEN(prefix + " SOLUTION = " + test_case.solution) {
228  auto expected_result = test_case.solution;
229  auto result = solve_construct(test_case.equation, test_case.method);
230  REQUIRE(result == expected_result);
231  }
232  }
233  counter++;
234  }
235  }
236 }
237 
238 
239 //=============================================================================
240 // Check if parsed NEURON block has correct token information
241 //=============================================================================
242 
243 void parse_neuron_block_string(const std::string& name, nmodl::ModToken& value) {
245  driver.parse_string(name);
246 
247  const auto& ast_program = driver.get_ast();
248  const auto& neuron_blocks = nmodl::collect_nodes(*ast_program->get_shared_ptr(),
249  {nmodl::ast::AstNodeType::NEURON_BLOCK});
250  value = *(neuron_blocks.front()->get_token());
251 }
252 
253 SCENARIO("Check if a NEURON block is parsed with correct location info in its token") {
254  GIVEN("A single NEURON block") {
255  nmodl::ModToken value;
256 
257  std::stringstream ss;
258  std::string neuron_block = R"(
259  NEURON {
260  SUFFIX it
261  USEION ca READ cai,cao WRITE ica
262  RANGE gcabar, m_inf, tau_m, alph1, alph2, KK, shift
263  }
264  )";
265  parse_neuron_block_string(reindent_text(neuron_block), value);
266  ss << value;
267  REQUIRE(ss.str() == " NEURON at [1.1-5.1] type 296");
268  }
269 }
test_utils.hpp
nmodl::parser::NmodlDriver
Class that binds all pieces together for parsing nmodl file.
Definition: nmodl_driver.hpp:67
nmodl::test_utils::nmodl_valid_constructs
const std::map< std::string, NmodlTestCase > nmodl_valid_constructs
Definition: nmodl_constructs.cpp:181
parse_neuron_block_string
void parse_neuron_block_string(const std::string &name, nmodl::ModToken &value)
Definition: parser.cpp:243
nmodl::test_utils::reindent_text
std::string reindent_text(const std::string &text, int indent_level)
Reindent nmodl text for text-to-text comparison.
Definition: test_utils.cpp:53
nmodl::visitor::test::CheckParentVisitor
Visitor for checking parents of ast nodes
Definition: checkparent_visitor.hpp:45
visitor_utils.hpp
Utility functions for visitors implementation.
SCENARIO
SCENARIO("NMODL can accept CR as return char for one line comment", "[parser]")
Definition: parser.cpp:36
program.hpp
Auto generated AST classes declaration.
nmodl::test_utils
custom type to represent nmodl construct for testing
Definition: nmodl_constructs.cpp:12
driver
nmodl::parser::UnitDriver driver
Definition: parser.cpp:28
nmodl::test_utils::diff_eq_constructs
const std::vector< DiffEqTestCase > diff_eq_constructs
Definition: nmodl_constructs.cpp:1309
solve_construct
std::string solve_construct(const std::string &equation, std::string method)
Definition: parser.cpp:216
nmodl::parser::DiffeqDriver::solve
static std::string solve(const std::string &equation, std::string method, bool debug=false)
solve equation using provided method
Definition: diffeq_driver.cpp:38
nmodl::test_utils::nmodl_invalid_constructs
const std::map< std::string, NmodlTestCase > nmodl_invalid_constructs
Guidelines for adding nmodl text constructs.
Definition: nmodl_constructs.cpp:63
nmodl::symtab::syminfo::to_string
std::string to_string(const T &obj)
Definition: symbol_properties.hpp:282
nmodl::parser::UnitDriver::parse_string
bool parse_string(const std::string &input)
parser Units provided as string (used for testing)
Definition: unit_driver.cpp:40
diffeq_driver.hpp
is_valid_construct
bool is_valid_construct(const std::string &construct)
Definition: parser.cpp:30
nmodl::collect_nodes
std::vector< std::shared_ptr< const ast::Ast > > collect_nodes(const ast::Ast &node, const std::vector< ast::AstNodeType > &types)
traverse node recursively and collect nodes of given types
Definition: visitor_utils.cpp:206
modtoken.hpp
checkparent_visitor.hpp
Visitor for checking parents of ast nodes
nmodl_driver.hpp
nmodl_constructs.hpp
nmodl::ModToken
Represent token returned by scanner.
Definition: modtoken.hpp:50
common_utils.hpp
Common utility functions for file/dir manipulation.