1 //: Phase 1 of running Mu code: load it from a textual representation.
  2 //:
  3 //: The process of running Mu code:
  4 //:   load -> transform -> run
  5 
  6 :(scenarios load)  // use 'load' instead of 'run' in all scenarios in this layer
  7 :(scenario first_recipe)
  8 def main [
  9   1:number <- copy 23
 10 ]
 11 +parse: instruction: copy
 12 +parse:   ingredient: {23: "literal"}
 13 +parse:   product: {1: "number"}
 14 
 15 :(code)
 16 vector<recipe_ordinal> load(string form) {
 17   istringstream in(form);
 18   in >> std::noskipws;
 19   return load(in);
 20 }
 21 
 22 vector<recipe_ordinal> load(istream& in) {
 23   in >> std::noskipws;
 24   vector<recipe_ordinal> result;
 25   while (has_data(in)) {
 26     skip_whitespace_and_comments(in);
 27     if (!has_data(in)) break;
 28     string command = next_word(in);
 29     if (command.empty()) {
 30       assert(!has_data(in));
 31       break;
 32     }
 33     // Command Handlers
 34     if (command == "recipe" || command == "def") {
 35       recipe_ordinal r = slurp_recipe(in);
 36       if (r > 0) result.push_back(r);
 37     }
 38     else if (command == "recipe!" || command == "def!") {
 39       Disable_redefine_checks = true;
 40       recipe_ordinal r = slurp_recipe(in);
 41       if (r > 0) result.push_back(r);
 42       Disable_redefine_checks = false;
 43     }
 44     // End Command Handlers
 45     else {
 46       raise << "unknown top-level command: " << command << '\n' << end();
 47     }
 48   }
 49   return result;
 50 }
 51 
 52 // return the recipe ordinal slurped, or -1 if it failed
 53 int slurp_recipe(istream& in) {
 54   recipe result;
 55   result.name = next_word(in);
 56   if (result.name.empty()) {
 57     assert(!has_data(in));
 58     raise << "file ended with 'recipe'\n" << end();
 59     return -1;
 60   }
 61   // End Load Recipe Name
 62   skip_whitespace_but_not_newline(in);
 63   // End Recipe Refinements
 64   if (result.name.empty())
 65     raise << "empty result.name\n" << end();
 66   trace(9991, "parse") << "--- defining " << result.name << end();
 67   if (!contains_key(Recipe_ordinal, result.name))
 68     put(Recipe_ordinal, result.name, Next_recipe_ordinal++);
 69   if (Recipe.find(get(Recipe_ordinal, result.name)) != Recipe.end()) {
 70     trace(9991, "parse") << "already exists" << end();
 71     if (should_check_for_redefine(result.name))
 72       raise << "redefining recipe " << result.name << "\n" << end();
 73     Recipe.erase(get(Recipe_ordinal, result.name));
 74   }
 75   slurp_body(in, result);
 76   // End Recipe Body(result)
 77   put(Recipe, get(Recipe_ordinal, result.name), result);
 78   return get(Recipe_ordinal, result.name);
 79 }
 80 
 81 void slurp_body(istream& in, recipe& result) {
 82   in >> std::noskipws;
 83   skip_whitespace_but_not_newline(in);
 84   if (in.get() != '[')
 85     raise << result.name << ": recipe body must begin with '['\n" << end();
 86   skip_whitespace_and_comments(in);  // permit trailing comment after '['
 87   instruction curr;
 88   while (next_instruction(in, &curr)) {
 89     curr.original_string = to_original_string(curr);
 90     // End Rewrite Instruction(curr, recipe result)
 91     trace(9992, "load") << "after rewriting: " << to_string(curr) << end();
 92     if (!curr.is_empty()) result.steps.push_back(curr);
 93   }
 94 }
 95 
 96 bool next_instruction(istream& in, instruction* curr) {
 97   curr->clear();
 98   skip_whitespace_and_comments(in);
 99   if (!has_data(in)) {
100     raise << "incomplete recipe at end of file (0)\n" << end();
101     return false;
102   }
103 
104   vector<string> words;
105   while (has_data(in) && in.peek() != '\n') {
106     skip_whitespace_but_not_newline(in);
107     if (!has_data(in)) {
108       raise << "incomplete recipe at end of file (1)\n" << end();
109       return false;
110     }
111     string word = next_word(in);
112     if (word.empty()) {
113       assert(!has_data(in));
114       raise << "incomplete recipe at end of file (2)\n" << end();
115       return false;
116     }
117     words.push_back(word);
118     skip_whitespace_but_not_newline(in);
119   }
120   skip_whitespace_and_comments(in);
121   if (SIZE(words) == 1 && words.at(0) == "]")
122     return false;  // end of recipe
123 
124   if (SIZE(words) == 1 && is_label_word(words.at(0))) {
125     curr->is_label = true;
126     curr->label = words.at(0);
127     trace(9993, "parse") << "label: " << curr->label << end();
128     if (!has_data(in)) {
129       raise << "incomplete recipe at end of file (3)\n" << end();
130       return false;
131     }
132     return true;
133   }
134 
135   vector<string>::iterator p = words.begin();
136   if (find(words.begin(), words.end(), "<-") != words.end()) {
137     for (;  *p != "<-";  ++p)
138       curr->products.push_back(reagent(*p));
139     ++p;  // skip <-
140   }
141 
142   if (p == words.end()) {
143     raise << "instruction prematurely ended with '<-'\n" << end();
144     return false;
145   }
146   curr->old_name = curr->name = *p;  ++p;
147   // curr->operation will be set at transform time
148 
149   for (;  p != words.end();  ++p)
150     curr->ingredients.push_back(reagent(*p));
151 
152   trace(9993, "parse") << "instruction: " << curr->name << end();
153   trace(9993, "parse") << "  number of ingredients: " << SIZE(curr->ingredients) << end();
154   for (vector<reagent>::iterator p = curr->ingredients.begin();  p != curr->ingredients.end();  ++p)
155     trace(9993, "parse") << "  ingredient: " << to_string(*p) << end();
156   for (vector<reagent>::iterator p = curr->products.begin();  p != curr->products.end();  ++p)
157     trace(9993, "parse") << "  product: " << to_string(*p) << end();
158   if (!has_data(in)) {
159     raise << "9: unbalanced '[' for recipe\n" << end();
160     return false;
161   }
162   return true;
163 }
164 
165 // can return empty string -- only if `in` has no more data
166 string next_word(istream& in) {
167   skip_whitespace_but_not_newline(in);
168   // End next_word Special-cases
169   ostringstream out;
170   slurp_word(in, out);
171   skip_whitespace_and_comments_but_not_newline(in);
172   string result = out.str();
173   if (result != "[" && ends_with(result, '['))
174     raise << "insert a space before '[' in '" << result << "'\n" << end();
175   return result;
176 }
177 
178 bool is_label_word(const string& word) {
179   if (word.empty()) return false;  // error raised elsewhere
180   return !isalnum(word.at(0)) && string("$_*@&,=-[]()").find(word.at(0)) == string::npos;
181 }
182 
183 bool ends_with(const string& s, const char c) {
184   if (s.empty()) return false;
185   return *s.rbegin() == c;
186 }
187 
188 :(before "End Globals")
189 // word boundaries
190 extern const string Terminators("(){}");
191 :(code)
192 void slurp_word(istream& in, ostream& out) {
193   char c;
194   if (has_data(in) && Terminators.find(in.peek()) != string::npos) {
195     in >> c;
196     out << c;
197     return;
198   }
199   while (in >> c) {
200     if (isspace(c) || Terminators.find(c) != string::npos || Ignore.find(c) != string::npos) {
201       in.putback(c);
202       break;
203     }
204     out << c;
205   }
206 }
207 
208 void skip_whitespace_and_comments(istream& in) {
209   while (true) {
210     if (!has_data(in)) break;
211     if (isspace(in.peek())) in.get();
212     else if (Ignore.find(in.peek()) != string::npos) in.get();
213     else if (in.peek() == '#') skip_comment(in);
214     else break;
215   }
216 }
217 
218 // confusing; move to the next line only to skip a comment, but never otherwise
219 void skip_whitespace_and_comments_but_not_newline(istream& in) {
220   while (true) {
221     if (!has_data(in)) break;
222     if (in.peek() == '\n') break;
223     if (isspace(in.peek())) in.get();
224     else if (Ignore.find(in.peek()) != string::npos) in.get();
225     else if (in.peek() == '#') skip_comment(in);
226     else break;
227   }
228 }
229 
230 void skip_comment(istream& in) {
231   if (has_data(in) && in.peek() == '#') {
232     in.get();
233     while (has_data(in) && in.peek() != '\n') in.get();
234   }
235 }
236 
237 :(scenario recipe_instead_of_def)
238 recipe main [
239   1:number <- copy 23
240 ]
241 +parse: instruction: copy
242 +parse:   ingredient: {23: "literal"}
243 +parse:   product: {1: "number"}
244 
245 :(scenario parse_comment_outside_recipe)
246 # this comment will be dropped by the tangler, so we need a dummy recipe to stop that
247 def f1 [
248 ]
249 # this comment will go through to 'load'
250 def main [
251   1:number <- copy 23
252 ]
253 +parse: instruction: copy
254 +parse:   ingredient: {23: "literal"}
255 +parse:   product: {1: "number"}
256 
257 :(scenario parse_comment_amongst_instruction)
258 def main [
259   # comment
260   1:number <- copy 23
261 ]
262 +parse: instruction: copy
263 +parse:   ingredient: {23: "literal"}
264 +parse:   product: {1: "number"}
265 
266 :(scenario parse_comment_amongst_instruction_2)
267 def main [
268   # comment
269   1:number <- copy 23
270   # comment
271 ]
272 +parse: instruction: copy
273 +parse:   ingredient: {23: "literal"}
274 +parse:   product: {1: "number"}
275 
276 :(scenario parse_comment_amongst_instruction_3)
277 def main [
278   1:number <- copy 23
279   # comment
280   2:number <- copy 23
281 ]
282 +parse: instruction: copy
283 +parse:   ingredient: {23: "literal"}
284 +parse:   product: {1: "number"}
285 +parse: instruction: copy
286 +parse:   ingredient: {23: "literal"}
287 +parse:   product: {2: "number"}
288 
289 :(scenario parse_comment_after_instruction)
290 def main [
291   1:number <- copy 23  # comment
292 ]
293 +parse: instruction: copy
294 +parse:   ingredient: {23: "literal"}
295 +parse:   product: {1: "number"}
296 
297 :(scenario parse_label)
298 def main [
299   +foo
300 ]
301 +parse: label: +foo
302 
303 :(scenario parse_dollar_as_recipe_name)
304 def main [
305   $foo
306 ]
307 +parse: instruction: $foo
308 
309 :(scenario parse_multiple_properties)
310 def main [
311   1:number <- copy 23/foo:bar:baz
312 ]
313 +parse: instruction: copy
314 +parse:   ingredient: {23: "literal", "foo": ("bar" "baz")}
315 +parse:   product: {1: "number"}
316 
317 :(scenario parse_multiple_products)
318 def main [
319   1:number, 2:number <- copy 23
320 ]
321 +parse: instruction: copy
322 +parse:   ingredient: {23: "literal"}
323 +parse:   product: {1: "number"}
324 +parse:   product: {2: "number"}
325 
326 :(scenario parse_multiple_ingredients)
327 def main [
328   1:number, 2:number <- copy 23, 4:number
329 ]
330 +parse: instruction: copy
331 +parse:   ingredient: {23: "literal"}
332 +parse:   ingredient: {4: "number"}
333 +parse:   product: {1: "number"}
334 +parse:   product: {2: "number"}
335 
336 :(scenario parse_multiple_types)
337 def main [
338   1:number, 2:address:number <- copy 23, 4:number
339 ]
340 +parse: instruction: copy
341 +parse:   ingredient: {23: "literal"}
342 +parse:   ingredient: {4: "number"}
343 +parse:   product: {1: "number"}
344 +parse:   product: {2: ("address" "number")}
345 
346 :(scenario parse_properties)
347 def main [
348   1:address:number/lookup <- copy 23
349 ]
350 +parse:   product: {1: ("address" "number"), "lookup": ()}
351 
352 //: this test we can't represent with a scenario
353 :(code)
354 void test_parse_comment_terminated_by_eof() {
355   load("recipe main [\n"
356        "  a:number <- copy 34\n"
357        "]\n"
358        "# abc");  // no newline after comment
359   cerr << ".";  // termination = success
360 }
361 
362 :(scenario warn_on_missing_space_before_bracket)
363 % Hide_errors = true;
364 def main[
365   1:number <- copy 23
366 ]
367 +error: insert a space before '[' in 'main['
368 
369 //: Warn if a recipe gets redefined, because large codebases can accidentally
370 //: step on their own toes. But there'll be many occasions later where
371 //: we'll want to disable the errors.
372 :(before "End Globals")
373 bool Disable_redefine_checks = false;
374 :(before "End Setup")
375 Disable_redefine_checks = false;
376 :(code)
377 bool should_check_for_redefine(const string& recipe_name) {
378   if (Disable_redefine_checks) return false;
379   return true;
380 }
381 
382 :(scenario forbid_redefining_recipes)
383 % Hide_errors = true;
384 def main [
385   1:number <- copy 23
386 ]
387 def main [
388   1:number <- copy 24
389 ]
390 +error: redefining recipe main
391 
392 :(scenario permit_forcibly_redefining_recipes)
393 def main [
394   1:number <- copy 23
395 ]
396 def! main [
397   1:number <- copy 24
398 ]
399 -error: redefining recipe main
400 $error: 0
401 
402 :(code)
403 // for debugging
404 void show_rest_of_stream(istream& in) {
405   cerr << '^';
406   char c;
407   while (in >> c)
408     cerr << c;
409   cerr << "$\n";
410   exit(0);
411 }