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->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   // End next_instruction(curr)
163   return true;
164 }
165 
166 // can return empty string -- only if 'in' has no more data
167 string next_word(istream& in) {
168   skip_whitespace_but_not_newline(in);
169   // End next_word Special-cases
170   ostringstream out;
171   slurp_word(in, out);
172   skip_whitespace_and_comments_but_not_newline(in);
173   string result = out.str();
174   if (result != "[" && ends_with(result, '['))
175     raise << "insert a space before '[' in '" << result << "'\n" << end();
176   return result;
177 }
178 
179 bool is_label_word(const string& word) {
180   if (word.empty()) return false;  // error raised elsewhere
181   return !isalnum(word.at(0)) && string("$_*@&,=-[]()").find(word.at(0)) == string::npos;
182 }
183 
184 bool ends_with(const string& s, const char c) {
185   if (s.empty()) return false;
186   return *s.rbegin() == c;
187 }
188 
189 :(before "End Globals")
190 // word boundaries
191 extern const string Terminators("(){}");
192 :(code)
193 void slurp_word(istream& in, ostream& out) {
194   char c;
195   if (has_data(in) && Terminators.find(in.peek()) != string::npos) {
196     in >> c;
197     out << c;
198     return;
199   }
200   while (in >> c) {
201     if (isspace(c) || Terminators.find(c) != string::npos || Ignore.find(c) != string::npos) {
202       in.putback(c);
203       break;
204     }
205     out << c;
206   }
207 }
208 
209 void skip_whitespace_and_comments(istream& in) {
210   while (true) {
211     if (!has_data(in)) break;
212     if (isspace(in.peek())) in.get();
213     else if (Ignore.find(in.peek()) != string::npos) in.get();
214     else if (in.peek() == '#') skip_comment(in);
215     else break;
216   }
217 }
218 
219 // confusing; move to the next line only to skip a comment, but never otherwise
220 void skip_whitespace_and_comments_but_not_newline(istream& in) {
221   while (true) {
222     if (!has_data(in)) break;
223     if (in.peek() == '\n') break;
224     if (isspace(in.peek())) in.get();
225     else if (Ignore.find(in.peek()) != string::npos) in.get();
226     else if (in.peek() == '#') skip_comment(in);
227     else break;
228   }
229 }
230 
231 void skip_comment(istream& in) {
232   if (has_data(in) && in.peek() == '#') {
233     in.get();
234     while (has_data(in) && in.peek() != '\n') in.get();
235   }
236 }
237 
238 :(scenario recipe_instead_of_def)
239 recipe main [
240   1:number <- copy 23
241 ]
242 +parse: instruction: copy
243 +parse:   ingredient: {23: "literal"}
244 +parse:   product: {1: "number"}
245 
246 :(scenario parse_comment_outside_recipe)
247 # this comment will be dropped by the tangler, so we need a dummy recipe to stop that
248 def f1 [
249 ]
250 # this comment will go through to 'load'
251 def main [
252   1:number <- copy 23
253 ]
254 +parse: instruction: copy
255 +parse:   ingredient: {23: "literal"}
256 +parse:   product: {1: "number"}
257 
258 :(scenario parse_comment_amongst_instruction)
259 def main [
260   # comment
261   1:number <- copy 23
262 ]
263 +parse: instruction: copy
264 +parse:   ingredient: {23: "literal"}
265 +parse:   product: {1: "number"}
266 
267 :(scenario parse_comment_amongst_instruction_2)
268 def main [
269   # comment
270   1:number <- copy 23
271   # comment
272 ]
273 +parse: instruction: copy
274 +parse:   ingredient: {23: "literal"}
275 +parse:   product: {1: "number"}
276 
277 :(scenario parse_comment_amongst_instruction_3)
278 def main [
279   1:number <- copy 23
280   # comment
281   2:number <- copy 23
282 ]
283 +parse: instruction: copy
284 +parse:   ingredient: {23: "literal"}
285 +parse:   product: {1: "number"}
286 +parse: instruction: copy
287 +parse:   ingredient: {23: "literal"}
288 +parse:   product: {2: "number"}
289 
290 :(scenario parse_comment_after_instruction)
291 def main [
292   1:number <- copy 23  # comment
293 ]
294 +parse: instruction: copy
295 +parse:   ingredient: {23: "literal"}
296 +parse:   product: {1: "number"}
297 
298 :(scenario parse_label)
299 def main [
300   +foo
301 ]
302 +parse: label: +foo
303 
304 :(scenario parse_dollar_as_recipe_name)
305 def main [
306   $foo
307 ]
308 +parse: instruction: $foo
309 
310 :(scenario parse_multiple_properties)
311 def main [
312   1:number <- copy 23/foo:bar:baz
313 ]
314 +parse: instruction: copy
315 +parse:   ingredient: {23: "literal", "foo": ("bar" "baz")}
316 +parse:   product: {1: "number"}
317 
318 :(scenario parse_multiple_products)
319 def main [
320   1:number, 2:number <- copy 23
321 ]
322 +parse: instruction: copy
323 +parse:   ingredient: {23: "literal"}
324 +parse:   product: {1: "number"}
325 +parse:   product: {2: "number"}
326 
327 :(scenario parse_multiple_ingredients)
328 def main [
329   1:number, 2:number <- copy 23, 4:number
330 ]
331 +parse: instruction: copy
332 +parse:   ingredient: {23: "literal"}
333 +parse:   ingredient: {4: "number"}
334 +parse:   product: {1: "number"}
335 +parse:   product: {2: "number"}
336 
337 :(scenario parse_multiple_types)
338 def main [
339   1:number, 2:address:number <- copy 23, 4:number
340 ]
341 +parse: instruction: copy
342 +parse:   ingredient: {23: "literal"}
343 +parse:   ingredient: {4: "number"}
344 +parse:   product: {1: "number"}
345 +parse:   product: {2: ("address" "number")}
346 
347 :(scenario parse_properties)
348 def main [
349   1:address:number/lookup <- copy 23
350 ]
351 +parse:   product: {1: ("address" "number"), "lookup": ()}
352 
353 //: this test we can't represent with a scenario
354 :(code)
355 void test_parse_comment_terminated_by_eof() {
356   load("recipe main [\n"
357        "  a:number <- copy 34\n"
358        "]\n"
359        "# abc");  // no newline after comment
360   cerr << ".";  // termination = success
361 }
362 
363 :(scenario warn_on_missing_space_before_bracket)
364 % Hide_errors = true;
365 def main[
366   1:number <- copy 23
367 ]
368 +error: insert a space before '[' in 'main['
369 
370 //: Warn if a recipe gets redefined, because large codebases can accidentally
371 //: step on their own toes. But there'll be many occasions later where
372 //: we'll want to disable the errors.
373 :(before "End Globals")
374 bool Disable_redefine_checks = false;
375 :(before "End Reset")
376 Disable_redefine_checks = false;
377 :(code)
378 bool should_check_for_redefine(const string& recipe_name) {
379   if (Disable_redefine_checks) return false;
380   return true;
381 }
382 
383 :(scenario forbid_redefining_recipes)
384 % Hide_errors = true;
385 def main [
386   1:number <- copy 23
387 ]
388 def main [
389   1:number <- copy 24
390 ]
391 +error: redefining recipe main
392 
393 :(scenario permit_forcibly_redefining_recipes)
394 def main [
395   1:number <- copy 23
396 ]
397 def! main [
398   1:number <- copy 24
399 ]
400 -error: redefining recipe main
401 $error: 0
402 
403 :(code)
404 // for debugging
405 void show_rest_of_stream(istream& in) {
406   cerr << '^';
407   char c;
408   while (in >> c)
409     cerr << c;
410   cerr << "$\n";
411   exit(0);
412 }