https://github.com/akkartik/mu/blob/master/011load.cc

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