https://github.com/akkartik/mu/blob/master/053recipe_header.cc

      1 //: Advanced notation for the common/easy case where a recipe takes some fixed
      2 //: number of ingredients and yields some fixed number of products.
      3
      4 :(scenario recipe_with_header)
      5 def main [
      6   1:num/raw <- add2 3, 5
      7 ]
      8 def add2 x:num, y:num -> z:num [
      9   local-scope
     10   load-ingredients
     11   z:num <- add x, y
     12   return z
     13 ]
     14 +mem: storing 8 in location 1
     15
     16 //: When loading recipes save any header.
     17
     18 :(before "End recipe Fields")
     19 bool has_header;
     20 vector<reagent> ingredients;
     21 vector<reagent> products;
     22 :(before "End recipe Constructor")
     23 has_header = false;
     24
     25 :(before "End Recipe Refinements")
     26 if (in.peek() != '[') {
     27   trace("parse") << "recipe has a header; parsing" << end();
     28   load_recipe_header(in, result);
     29 }
     30
     31 :(code)
     32 void load_recipe_header(istream& in, recipe& result) {
     33   result.has_header = true;
     34   while (has_data(in) && in.peek() != '[' && in.peek() != '\n') {
     35     string s = next_word(in);
     36     if (s.empty()) {
     37       assert(!has_data(in));
     38       raise << "incomplete recipe header at end of file (0)\n" << end();
     39       return;
     40     }
     41     if (s == "<-")
     42       raise << "recipe " << result.name << " should say '->' and not '<-'\n" << end();
     43     if (s == "->") break;
     44     result.ingredients.push_back(reagent(s));
     45     trace("parse") << "header ingredient: " << result.ingredients.back().original_string << end();
     46     skip_whitespace_but_not_newline(in);
     47   }
     48   while (has_data(in) && in.peek() != '[' && in.peek() != '\n') {
     49     string s = next_word(in);
     50     if (s.empty()) {
     51       assert(!has_data(in));
     52       raise << "incomplete recipe header at end of file (1)\n" << end();
     53       return;
     54     }
     55     result.products.push_back(reagent(s));
     56     trace("parse") << "header product: " << result.products.back().original_string << end();
     57     skip_whitespace_but_not_newline(in);
     58   }
     59   // End Load Recipe Header(result)
     60 }
     61
     62 :(scenario recipe_handles_stray_comma)
     63 def main [
     64   1:num/raw <- add2 3, 5
     65 ]
     66 def add2 x:num, y:num -> z:num, [
     67   local-scope
     68   load-ingredients
     69   z:num <- add x, y
     70   return z
     71 ]
     72 +mem: storing 8 in location 1
     73
     74 :(scenario recipe_handles_stray_comma_2)
     75 def main [
     76   foo
     77 ]
     78 def foo, [
     79   1:num/raw <- add 2, 2
     80 ]
     81 def bar [
     82   1:num/raw <- add 2, 3
     83 ]
     84 +mem: storing 4 in location 1
     85
     86 :(scenario recipe_handles_wrong_arrow)
     87 % Hide_errors = true;
     88 def foo a:num <- b:num [
     89 ]
     90 +error: recipe foo should say '->' and not '<-'
     91
     92 :(scenario recipe_handles_missing_bracket)
     93 % Hide_errors = true;
     94 def main
     95 ]
     96 +error: main: recipe body must begin with '['
     97
     98 :(scenario recipe_handles_missing_bracket_2)
     99 % Hide_errors = true;
    100 def main
    101   local-scope
    102   {
    103   }
    104 ]
    105 # doesn't overflow line when reading header
    106 -parse: header ingredient: local-scope
    107 +error: main: recipe body must begin with '['
    108
    109 :(scenario recipe_handles_missing_bracket_3)
    110 % Hide_errors = true;
    111 def main  # comment
    112   local-scope
    113   {
    114   }
    115 ]
    116 # doesn't overflow line when reading header
    117 -parse: header ingredient: local-scope
    118 +error: main: recipe body must begin with '['
    119
    120 :(after "Begin debug_string(recipe x)")
    121 out << "ingredients:\n";
    122 for (int i = 0;  i < SIZE(x.ingredients);  ++i)
    123   out << "  " << debug_string(x.ingredients.at(i)) << '\n';
    124 out << "products:\n";
    125 for (int i = 0;  i < SIZE(x.products);  ++i)
    126   out << "  " << debug_string(x.products.at(i)) << '\n';
    127
    128 //: If a recipe never mentions any ingredients or products, assume it has a header.
    129
    130 :(scenario recipe_without_ingredients_or_products_has_header)
    131 def test [
    132   1:num <- copy 34
    133 ]
    134 +parse: recipe test has a header
    135
    136 :(before "End Recipe Body(result)")
    137 if (!result.has_header) {
    138   result.has_header = true;
    139   for (int i = 0;  i < SIZE(result.steps);  ++i) {
    140     const instruction& inst = result.steps.at(i);
    141     if ((inst.name == "reply" && !inst.ingredients.empty())
    142         || (inst.name == "return" && !inst.ingredients.empty())
    143         || inst.name == "next-ingredient"
    144         || inst.name == "ingredient"
    145         || inst.name == "rewind-ingredients") {
    146       result.has_header = false;
    147       break;
    148     }
    149   }
    150 }
    151 if (result.has_header) {
    152   trace("parse") << "recipe " << result.name << " has a header" << end();
    153 }
    154
    155 //: Support type abbreviations in headers.
    156
    157 :(scenario type_abbreviations_in_recipe_headers)
    158 def main [
    159   local-scope
    160   a:text <- foo
    161   1:char/raw <- index *a, 0
    162 ]
    163 def foo -> a:text [  # 'text' is an abbreviation
    164   local-scope
    165   load-ingredients
    166   a <- new [abc]
    167 ]
    168 +mem: storing 97 in location 1
    169
    170 :(before "End Expand Type Abbreviations(caller)")
    171 for (long int i = 0;  i < SIZE(caller.ingredients);  ++i)
    172   expand_type_abbreviations(caller.ingredients.at(i).type);
    173 for (long int i = 0;  i < SIZE(caller.products);  ++i)
    174   expand_type_abbreviations(caller.products.at(i).type);
    175
    176 //: Rewrite 'load-ingredients' to instructions to create all reagents in the header.
    177
    178 :(before "End Rewrite Instruction(curr, recipe result)")
    179 if (curr.name == "load-ingredients" || curr.name == "load-inputs") {
    180   curr.clear();
    181   recipe_ordinal op = get(Recipe_ordinal, "next-ingredient-without-typechecking");
    182   for (int i = 0;  i < SIZE(result.ingredients);  ++i) {
    183     curr.operation = op;
    184     curr.name = "next-ingredient-without-typechecking";
    185     curr.products.push_back(result.ingredients.at(i));
    186     result.steps.push_back(curr);
    187     curr.clear();
    188   }
    189 }
    190 if (curr.name == "next-ingredient-without-typechecking") {
    191   raise << maybe(result.name) << "never call 'next-ingredient-without-typechecking' directly\n" << end();
    192   curr.clear();
    193 }
    194
    195 //: internal version of next-ingredient; don't call this directly
    196 :(before "End Primitive Recipe Declarations")
    197 NEXT_INGREDIENT_WITHOUT_TYPECHECKING,
    198 :(before "End Primitive Recipe Numbers")
    199 put(Recipe_ordinal, "next-ingredient-without-typechecking", NEXT_INGREDIENT_WITHOUT_TYPECHECKING);
    200 :(before "End Primitive Recipe Checks")
    201 case NEXT_INGREDIENT_WITHOUT_TYPECHECKING: {
    202   break;
    203 }
    204 :(before "End Primitive Recipe Implementations")
    205 case NEXT_INGREDIENT_WITHOUT_TYPECHECKING: {
    206   assert(!Current_routine->calls.empty());
    207   if (current_call().next_ingredient_to_process < SIZE(current_call().ingredient_atoms)) {
    208     products.push_back(
    209         current_call().ingredient_atoms.at(current_call().next_ingredient_to_process));
    210     assert(SIZE(products) == 1);  products.resize(2);  // push a new vector
    211     products.at(1).push_back(1);
    212     ++current_call().next_ingredient_to_process;
    213   }
    214   else {
    215     products.resize(2);
    216     // pad the first product with sufficient zeros to match its type
    217     products.at(0).resize(size_of(current_instruction().products.at(0)));
    218     products.at(1).push_back(0);
    219   }
    220   break;
    221 }
    222
    223 //: more useful error messages if someone forgets 'load-ingredients'
    224
    225 :(scenario load_ingredients_missing_error)
    226 % Hide_errors = true;
    227 def foo a:num [
    228   local-scope
    229   b:num <- add a:num, 1
    230 ]
    231 +error: foo: tried to read ingredient 'a' in 'b:num <- add a:num, 1' but it hasn't been written to yet
    232 +error:   did you forget 'load-ingredients'?
    233
    234 :(after "use-before-set Error")
    235 if (is_present_in_ingredients(caller, ingredient.name))
    236   raise << "  did you forget 'load-ingredients'?\n" << end();
    237
    238 :(scenario load_ingredients_missing_error_2)
    239 % Hide_errors = true;
    240 def foo a:num [
    241   local-scope
    242   b:num <- add a, 1
    243 ]
    244 +error: foo: missing type for 'a' in 'b:num <- add a, 1'
    245 +error:   did you forget 'load-ingredients'?
    246
    247 :(after "missing-type Error 1")
    248 if (is_present_in_ingredients(get(Recipe, get(Recipe_ordinal, recipe_name)), x.name))
    249   raise << "  did you forget 'load-ingredients'?\n" << end();
    250
    251 :(code)
    252 bool is_present_in_ingredients(const recipe& callee, const string& ingredient_name) {
    253   for (int i = 0;  i < SIZE(callee.ingredients);  ++i) {
    254     if (callee.ingredients.at(i).name == ingredient_name)
    255       return true;
    256   }
    257   return false;
    258 }
    259
    260 //:: Check all calls against headers.
    261
    262 :(scenario show_clear_error_on_bad_call)
    263 % Hide_errors = true;
    264 def main [
    265   1:num <- foo 34
    266 ]
    267 def foo x:point -> y:num [
    268   local-scope
    269   load-ingredients
    270   return 35
    271 ]
    272 +error: main: ingredient 0 has the wrong type at '1:num <- foo 34'
    273
    274 :(scenario show_clear_error_on_bad_call_2)
    275 % Hide_errors = true;
    276 def main [
    277   1:point <- foo 34
    278 ]
    279 def foo x:num -> y:num [
    280   local-scope
    281   load-ingredients
    282   return x
    283 ]
    284 +error: main: product 0 has the wrong type at '1:point <- foo 34'
    285
    286 :(after "Transform.push_back(check_instruction)")
    287 Transform.push_back(check_calls_against_header);  // idempotent
    288 :(code)
    289 void check_calls_against_header(const recipe_ordinal r) {
    290   const recipe& caller = get(Recipe, r);
    291   trace(9991, "transform") << "--- type-check calls inside recipe " << caller.name << end();
    292   for (int i = 0;  i < SIZE(caller.steps);  ++i) {
    293     const instruction& inst = caller.steps.at(i);
    294     if (is_primitive(inst.operation)) continue;
    295     const recipe& callee = get(Recipe, inst.operation);
    296     if (!callee.has_header) continue;
    297     for (long int i = 0;  i < min(SIZE(inst.ingredients), SIZE(callee.ingredients));  ++i) {
    298       // ingredients coerced from call to callee
    299       if (!types_coercible(callee.ingredients.at(i), inst.ingredients.at(i))) {
    300         raise << maybe(caller.name) << "ingredient " << i << " has the wrong type at '" << to_original_string(inst) << "'\n" << end();
    301         raise << "  ['" << to_string(callee.ingredients.at(i).type) << "' vs '" << to_string(inst.ingredients.at(i).type) << "']\n" << end();
    302       }
    303     }
    304     for (long int i = 0;  i < min(SIZE(inst.products), SIZE(callee.products));  ++i) {
    305       if (is_dummy(inst.products.at(i))) continue;
    306       // products coerced from callee to call
    307       if (!types_coercible(inst.products.at(i), callee.products.at(i))) {
    308         raise << maybe(caller.name) << "product " << i << " has the wrong type at '" << to_original_string(inst) << "'\n" << end();
    309         raise << "  ['" << to_string(inst.products.at(i).type) << "' vs '" << to_string(callee.products.at(i).type) << "']\n" << end();
    310       }
    311     }
    312   }
    313 }
    314
    315 //:: Check types going in and out of all recipes with headers.
    316
    317 :(scenarios transform)
    318 :(scenario recipe_headers_are_checked)
    319 % Hide_errors = true;
    320 def add2 x:num, y:num -> z:num [
    321   local-scope
    322   load-ingredients
    323   z:&:num <- copy 0/unsafe
    324   return z
    325 ]
    326 +error: add2: replied with the wrong type at 'return z'
    327
    328 :(before "End Checks")
    329 Transform.push_back(check_return_instructions_against_header);  // idempotent
    330
    331 :(code)
    332 void check_return_instructions_against_header(const recipe_ordinal r) {
    333   const recipe& caller_recipe = get(Recipe, r);
    334   if (!caller_recipe.has_header) return;
    335   trace(9991, "transform") << "--- checking return instructions against header for " << caller_recipe.name << end();
    336   for (int i = 0;  i < SIZE(caller_recipe.steps);  ++i) {
    337     const instruction& inst = caller_recipe.steps.at(i);
    338     if (inst.name != "reply" && inst.name != "return") continue;
    339     if (SIZE(caller_recipe.products) != SIZE(inst.ingredients)) {
    340       raise << maybe(caller_recipe.name) << "replied with the wrong number of products at '" << to_original_string(inst) << "'\n" << end();
    341       continue;
    342     }
    343     for (int i = 0;  i < SIZE(caller_recipe.products);  ++i) {
    344       if (!types_match(caller_recipe.products.at(i), inst.ingredients.at(i)))
    345         raise << maybe(caller_recipe.name) << "replied with the wrong type at '" << to_original_string(inst) << "'\n" << end();
    346     }
    347   }
    348 }
    349
    350 :(scenario recipe_headers_are_checked_2)
    351 % Hide_errors = true;
    352 def add2 x:num, y:num [
    353   local-scope
    354   load-ingredients
    355   z:&:num <- copy 0/unsafe
    356   return z
    357 ]
    358 +error: add2: replied with the wrong number of products at 'return z'
    359
    360 :(scenario recipe_headers_are_checked_against_pre_transformed_instructions)
    361 % Hide_errors = true;
    362 def foo -> x:num [
    363   local-scope
    364   x:num <- copy 0
    365   z:bool <- copy false
    366   return-if z, z
    367 ]
    368 +error: foo: replied with the wrong type at 'return-if z, z'
    369
    370 :(scenario recipe_headers_check_for_duplicate_names)
    371 % Hide_errors = true;
    372 def foo x:num, x:num -> z:num [
    373   local-scope
    374   load-ingredients
    375   return z
    376 ]
    377 +error: foo: 'x' can't repeat in the ingredients
    378
    379 :(scenario recipe_headers_check_for_duplicate_names_2)
    380 % Hide_errors = true;
    381 def foo x:num, x:num [  # no result
    382   local-scope
    383   load-ingredients
    384 ]
    385 +error: foo: 'x' can't repeat in the ingredients
    386
    387 :(scenario recipe_headers_check_for_missing_types)
    388 % Hide_errors = true;
    389 def main [
    390   foo 0
    391 ]
    392 def foo a [  # no type for 'a'
    393 ]
    394 +error: foo: ingredient 'a' has no type
    395
    396 :(before "End recipe Fields")
    397 map<string, int> ingredient_index;
    398
    399 :(after "Begin Instruction Modifying Transforms")
    400 Transform.push_back(check_header_ingredients);  // idempotent
    401
    402 :(code)
    403 void check_header_ingredients(const recipe_ordinal r) {
    404   recipe& caller_recipe = get(Recipe, r);
    405   caller_recipe.ingredient_index.clear();
    406   trace(9991, "transform") << "--- checking return instructions against header for " << caller_recipe.name << end();
    407   for (int i = 0;  i < SIZE(caller_recipe.ingredients);  ++i) {
    408     if (caller_recipe.ingredients.at(i).type == NULL)
    409       raise << maybe(caller_recipe.name) << "ingredient '" << caller_recipe.ingredients.at(i).name << "' has no type\n" << end();
    410     if (contains_key(caller_recipe.ingredient_index, caller_recipe.ingredients.at(i).name))
    411       raise << maybe(caller_recipe.name) << "'" << caller_recipe.ingredients.at(i).name << "' can't repeat in the ingredients\n" << end();
    412     put(caller_recipe.ingredient_index, caller_recipe.ingredients.at(i).name, i);
    413   }
    414 }
    415
    416 //: Deduce types from the header if possible.
    417
    418 :(scenarios run)
    419 :(scenario deduce_instruction_types_from_recipe_header)
    420 def main [
    421   1:num/raw <- add2 3, 5
    422 ]
    423 def add2 x:num, y:num -> z:num [
    424   local-scope
    425   load-ingredients
    426   z <- add x, y  # no type for z
    427   return z
    428 ]
    429 +mem: storing 8 in location 1
    430
    431 :(after "Begin Type Modifying Transforms")
    432 Transform.push_back(deduce_types_from_header);  // idempotent
    433
    434 :(code)
    435 void deduce_types_from_header(const recipe_ordinal r) {
    436   recipe& caller_recipe = get(Recipe, r);
    437   if (caller_recipe.products.empty()) return;
    438   trace(9991, "transform") << "--- deduce types from header for " << caller_recipe.name << end();
    439   map<string, const type_tree*> header_type;
    440   for (int i = 0;  i < SIZE(caller_recipe.ingredients);  ++i) {
    441     if (!caller_recipe.ingredients.at(i).type) continue;  // error handled elsewhere
    442     put(header_type, caller_recipe.ingredients.at(i).name, caller_recipe.ingredients.at(i).type);
    443     trace(9993, "transform") << "type of " << caller_recipe.ingredients.at(i).name << " is " << names_to_string(caller_recipe.ingredients.at(i).type) << end();
    444   }
    445   for (int i = 0;  i < SIZE(caller_recipe.products);  ++i) {
    446     if (!caller_recipe.products.at(i).type) continue;  // error handled elsewhere
    447     put(header_type, caller_recipe.products.at(i).name, caller_recipe.products.at(i).type);
    448     trace(9993, "transform") << "type of " << caller_recipe.products.at(i).name << " is " << names_to_string(caller_recipe.products.at(i).type) << end();
    449   }
    450   for (int i = 0;  i < SIZE(caller_recipe.steps);  ++i) {
    451     instruction& inst = caller_recipe.steps.at(i);
    452     trace(9992, "transform") << "instruction: " << to_string(inst) << end();
    453     for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
    454       if (inst.ingredients.at(i).type) continue;
    455       if (header_type.find(inst.ingredients.at(i).name) == header_type.end())
    456         continue;
    457       if (!contains_key(header_type, inst.ingredients.at(i).name)) continue;  // error handled elsewhere
    458       inst.ingredients.at(i).type = new type_tree(*get(header_type, inst.ingredients.at(i).name));
    459       trace(9993, "transform") << "type of " << inst.ingredients.at(i).name << " is " << names_to_string(inst.ingredients.at(i).type) << end();
    460     }
    461     for (int i = 0;  i < SIZE(inst.products);  ++i) {
    462       trace(9993, "transform") << "  product: " << to_string(inst.products.at(i)) << end();
    463       if (inst.products.at(i).type) continue;
    464       if (header_type.find(inst.products.at(i).name) == header_type.end())
    465         continue;
    466       if (!contains_key(header_type, inst.products.at(i).name)) continue;  // error handled elsewhere
    467       inst.products.at(i).type = new type_tree(*get(header_type, inst.products.at(i).name));
    468       trace(9993, "transform") << "type of " << inst.products.at(i).name << " is " << names_to_string(inst.products.at(i).type) << end();
    469     }
    470   }
    471 }
    472
    473 //: One final convenience: no need to say what to return if the information is
    474 //: in the header.
    475
    476 :(scenario return_based_on_header)
    477 def main [
    478   1:num/raw <- add2 3, 5
    479 ]
    480 def add2 x:num, y:num -> z:num [
    481   local-scope
    482   load-ingredients
    483   z <- add x, y
    484   return
    485 ]
    486 +mem: storing 8 in location 1
    487
    488 :(after "Transform.push_back(check_header_ingredients)")
    489 Transform.push_back(fill_in_return_ingredients);  // idempotent
    490
    491 :(code)
    492 void fill_in_return_ingredients(const recipe_ordinal r) {
    493   recipe& caller_recipe = get(Recipe, r);
    494   trace(9991, "transform") << "--- fill in return ingredients from header for recipe " << caller_recipe.name << end();
    495   if (!caller_recipe.has_header) return;
    496   for (int i = 0;  i < SIZE(caller_recipe.steps);  ++i) {
    497     instruction& inst = caller_recipe.steps.at(i);
    498     if (inst.name == "reply" || inst.name == "return")
    499       add_header_products(inst, caller_recipe);
    500   }
    501   // fall through return
    502   if (!caller_recipe.steps.empty()) {
    503     const instruction& final_instruction = caller_recipe.steps.at(SIZE(caller_recipe.steps)-1);
    504     if (final_instruction.name == "reply" || final_instruction.name == "return")
    505       return;
    506   }
    507   instruction inst;
    508   inst.name = "return";
    509   add_header_products(inst, caller_recipe);
    510   caller_recipe.steps.push_back(inst);
    511 }
    512
    513 void add_header_products(instruction& inst, const recipe& caller_recipe) {
    514   assert(inst.name == "reply" || inst.name == "return");
    515   // collect any products with the same names as ingredients
    516   for (int i = 0;  i < SIZE(caller_recipe.products);  ++i) {
    517     // if the ingredient is missing, add it from the header
    518     if (SIZE(inst.ingredients) == i)
    519       inst.ingredients.push_back(caller_recipe.products.at(i));
    520     // if it's missing /same_as_ingredient, try to fill it in
    521     if (contains_key(caller_recipe.ingredient_index, caller_recipe.products.at(i).name) && !has_property(inst.ingredients.at(i), "same_as_ingredient")) {
    522       ostringstream same_as_ingredient;
    523       same_as_ingredient << get(caller_recipe.ingredient_index, caller_recipe.products.at(i).name);
    524       inst.ingredients.at(i).properties.push_back(pair<string, string_tree*>("same-as-ingredient", new string_tree(same_as_ingredient.str())));
    525     }
    526   }
    527 }
    528
    529 :(scenario explicit_return_ignores_header)
    530 def main [
    531   1:num/raw, 2:num/raw <- add2 3, 5
    532 ]
    533 def add2 a:num, b:num -> y:num, z:num [
    534   local-scope
    535   load-ingredients
    536   y <- add a, b
    537   z <- subtract a, b
    538   return a, z
    539 ]
    540 +mem: storing 3 in location 1
    541 +mem: storing -2 in location 2
    542
    543 :(scenario return_on_fallthrough_based_on_header)
    544 def main [
    545   1:num/raw <- add2 3, 5
    546 ]
    547 def add2 x:num, y:num -> z:num [
    548   local-scope
    549   load-ingredients
    550   z <- add x, y
    551 ]
    552 +transform: instruction: return {z: "number"}
    553 +mem: storing 8 in location 1
    554
    555 :(scenario return_on_fallthrough_already_exists)
    556 def main [
    557   1:num/raw <- add2 3, 5
    558 ]
    559 def add2 x:num, y:num -> z:num [
    560   local-scope
    561   load-ingredients
    562   z <- add x, y  # no type for z
    563   return z
    564 ]
    565 +transform: instruction: return {z: ()}
    566 -transform: instruction: return z:num
    567 +mem: storing 8 in location 1
    568
    569 :(scenario return_causes_error_in_empty_recipe)
    570 % Hide_errors = true;
    571 def foo -> x:num [
    572 ]
    573 +error: foo: tried to read ingredient 'x' in 'return x:num' but it hasn't been written to yet
    574
    575 :(scenario return_after_conditional_return_based_on_header)
    576 def main [
    577   1:num/raw <- add2 3, 5
    578 ]
    579 def add2 x:num, y:num -> z:num [
    580   local-scope
    581   load-ingredients
    582   z <- add x, y  # no type for z
    583   return-if false, 34
    584 ]
    585 +mem: storing 8 in location 1
    586
    587 :(scenario recipe_headers_perform_same_ingredient_check)
    588 % Hide_errors = true;
    589 def main [
    590   1:num <- copy 34
    591   2:num <- copy 34
    592   3:num <- add2 1:num, 2:num
    593 ]
    594 def add2 x:num, y:num -> x:num [
    595   local-scope
    596   load-ingredients
    597 ]
    598 +error: main: '3:num <- add2 1:num, 2:num' should write to '1:num' rather than '3:num'
    599
    600 //: One special-case is recipe 'main'. Make sure it's only ever taking in text
    601 //: ingredients, and returning a single number.
    602
    603 :(scenario recipe_header_ingredients_constrained_for_main)
    604 % Hide_errors = true;
    605 def main x:num [
    606 ]
    607 +error: ingredients of recipe 'main' must all be text (address:array:character)
    608
    609 :(scenario recipe_header_products_constrained_for_main)
    610 % Hide_errors = true;
    611 def main -> x:text [
    612 ]
    613 +error: recipe 'main' must return at most a single product, a number
    614
    615 :(scenario recipe_header_products_constrained_for_main_2)
    616 % Hide_errors = true;
    617 def main -> x:num, y:num [
    618 ]
    619 +error: recipe 'main' must return at most a single product, a number
    620
    621 :(after "Transform.push_back(expand_type_abbreviations)")
    622 Transform.push_back(check_recipe_header_constraints);
    623 :(code)
    624 void check_recipe_header_constraints(const recipe_ordinal r) {
    625   const recipe& caller = get(Recipe, r);
    626   if (caller.name != "main") return;
    627   trace(9992, "transform") << "check recipe header constraints for recipe " << caller.name << end();
    628   if (!caller.has_header) return;
    629   reagent/*local*/ expected_ingredient("x:address:array:character");
    630   for (int i = 0; i < SIZE(caller.ingredients); ++i) {
    631     if (!types_strictly_match(expected_ingredient, caller.ingredients.at(i))) {
    632       raise << "ingredients of recipe 'main' must all be text (address:array:character)\n" << end();
    633       break;
    634     }
    635   }
    636   int nprod = SIZE(caller.products);
    637   reagent/*local*/ expected_product("x:number");
    638   if (nprod > 1
    639       || (nprod == 1 && !types_strictly_match(expected_product, caller.products.at(0)))) {
    640     raise << "recipe 'main' must return at most a single product, a number\n" << end();
    641   }
    642 }