https://github.com/akkartik/mu/blob/master/057immutable.cc

      1 //: Ingredients of a recipe are meant to be immutable unless they're also
      2 //: products. This layer will start enforcing this check.
      3 //:
      4 //: One hole for now: variables in surrounding spaces are implicitly mutable.
      5 //: [tag: todo]
      6
      7 :(scenario can_modify_ingredients_that_are_also_products)
      8 # mutable container
      9 def main [
     10   local-scope
     11   p:point <- merge 34, 35
     12   p <- foo p
     13 ]
     14 def foo p:point -> p:point [
     15   local-scope
     16   load-ingredients
     17   p <- put p, x:offset, 34
     18 ]
     19 $error: 0
     20
     21 :(scenario can_modify_ingredients_that_are_also_products_2)
     22 def main [
     23   local-scope
     24   p:&:point <- new point:type
     25   p <- foo p
     26 ]
     27 # mutable address to container
     28 def foo p:&:point -> p:&:point [
     29   local-scope
     30   load-ingredients
     31   *p <- put *p, x:offset, 34
     32 ]
     33 $error: 0
     34
     35 :(scenario can_modify_ingredients_that_are_also_products_3)
     36 def main [
     37   local-scope
     38   p:&:@:num <- new number:type, 3
     39   p <- foo p
     40 ]
     41 # mutable address
     42 def foo p:&:@:num -> p:&:@:num [
     43   local-scope
     44   load-ingredients
     45   *p <- put-index *p, 0, 34
     46 ]
     47 $error: 0
     48
     49 :(scenario ignore_literal_ingredients_for_immutability_checks)
     50 def main [
     51   local-scope
     52   p:&:d1 <- new d1:type
     53   q:num <- foo p
     54 ]
     55 def foo p:&:d1 -> q:num [
     56   local-scope
     57   load-ingredients
     58   x:&:d1 <- new d1:type
     59   *x <- put *x, p:offset, 34  # ignore this 'p'
     60   return 36
     61 ]
     62 container d1 [
     63   p:num
     64   q:num
     65 ]
     66 $error: 0
     67
     68 :(scenario cannot_modify_immutable_ingredients)
     69 % Hide_errors = true;
     70 def main [
     71   local-scope
     72   x:&:num <- new number:type
     73   foo x
     74 ]
     75 # immutable address to primitive
     76 def foo x:&:num [
     77   local-scope
     78   load-ingredients
     79   *x <- copy 34
     80 ]
     81 +error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product
     82
     83 :(scenario cannot_modify_immutable_containers)
     84 % Hide_errors = true;
     85 def main [
     86   local-scope
     87   x:point-number <- merge 34, 35, 36
     88   foo x
     89 ]
     90 # immutable container
     91 def foo x:point-number [
     92   local-scope
     93   load-ingredients
     94   # copy an element: ok
     95   y:point <- get x, xy:offset
     96   # modify the element: boom
     97   # This could be ok if y contains no addresses, but we're not going to try to be that smart.
     98   # It also makes the rules easier to reason about. If it's just an ingredient, just don't try to change it.
     99   y <- put y, x:offset, 37
    100 ]
    101 +error: foo: cannot modify 'y' in instruction 'y <- put y, x:offset, 37' because that would modify 'x' which is an ingredient of recipe foo but not also a product
    102
    103 :(scenario can_modify_immutable_pointers)
    104 def main [
    105   local-scope
    106   x:&:num <- new number:type
    107   foo x
    108 ]
    109 def foo x:&:num [
    110   local-scope
    111   load-ingredients
    112   # modify the address, not the payload
    113   x <- copy null
    114 ]
    115 $error: 0
    116
    117 :(scenario can_modify_immutable_pointers_but_not_their_payloads)
    118 % Hide_errors = true;
    119 def main [
    120   local-scope
    121   x:&:num <- new number:type
    122   foo x
    123 ]
    124 def foo x:&:num [
    125   local-scope
    126   load-ingredients
    127   # modify address; ok
    128   x <- new number:type
    129   # modify payload: boom
    130   # this could be ok, but we're not going to try to be that smart
    131   *x <- copy 34
    132 ]
    133 +error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product
    134
    135 :(scenario cannot_call_mutating_recipes_on_immutable_ingredients)
    136 % Hide_errors = true;
    137 def main [
    138   local-scope
    139   p:&:point <- new point:type
    140   foo p
    141 ]
    142 def foo p:&:point [
    143   local-scope
    144   load-ingredients
    145   bar p
    146 ]
    147 def bar p:&:point -> p:&:point [
    148   local-scope
    149   load-ingredients
    150   # p could be modified here, but it doesn't have to be, it's already marked
    151   # mutable in the header
    152 ]
    153 +error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product
    154
    155 :(scenario cannot_modify_copies_of_immutable_ingredients)
    156 % Hide_errors = true;
    157 def main [
    158   local-scope
    159   p:&:point <- new point:type
    160   foo p
    161 ]
    162 def foo p:&:point [
    163   local-scope
    164   load-ingredients
    165   q:&:point <- copy p
    166   *q <- put *q, x:offset, 34
    167 ]
    168 +error: foo: cannot modify 'q' in instruction '*q <- put *q, x:offset, 34' because that would modify p which is an ingredient of recipe foo but not also a product
    169
    170 :(scenario can_modify_copies_of_mutable_ingredients)
    171 def main [
    172   local-scope
    173   p:&:point <- new point:type
    174   foo p
    175 ]
    176 def foo p:&:point -> p:&:point [
    177   local-scope
    178   load-ingredients
    179   q:&:point <- copy p
    180   *q <- put *q, x:offset, 34
    181 ]
    182 $error: 0
    183
    184 :(scenario cannot_modify_address_inside_immutable_ingredients)
    185 % Hide_errors = true;
    186 container foo [
    187   x:&:@:num  # contains an address
    188 ]
    189 def main [
    190   # don't run anything
    191 ]
    192 def foo a:&:foo [
    193   local-scope
    194   load-ingredients
    195   x:&:@:num <- get *a, x:offset  # just a regular get of the container
    196   *x <- put-index *x, 0, 34  # but then a put-index on the result
    197 ]
    198 +error: foo: cannot modify 'x' in instruction '*x <- put-index *x, 0, 34' because that would modify a which is an ingredient of recipe foo but not also a product
    199
    200 :(scenario cannot_modify_address_inside_immutable_ingredients_2)
    201 container foo [
    202   x:&:@:num  # contains an address
    203 ]
    204 def main [
    205   # don't run anything
    206 ]
    207 def foo a:&:foo [
    208   local-scope
    209   load-ingredients
    210   b:foo <- merge null
    211   # modify b, completely unrelated to immutable ingredient a
    212   x:&:@:num <- get b, x:offset
    213   *x <- put-index *x, 0, 34
    214 ]
    215 $error: 0
    216
    217 :(scenario cannot_modify_address_inside_immutable_ingredients_3)
    218 % Hide_errors = true;
    219 def main [
    220   # don't run anything
    221 ]
    222 def foo a:&:@:&:num [
    223   local-scope
    224   load-ingredients
    225   x:&:num <- index *a, 0  # just a regular index of the array
    226   *x <- copy 34  # but then modify the result
    227 ]
    228 +error: foo: cannot modify 'x' in instruction '*x <- copy 34' because that would modify a which is an ingredient of recipe foo but not also a product
    229
    230 :(scenario cannot_modify_address_inside_immutable_ingredients_4)
    231 def main [
    232   # don't run anything
    233 ]
    234 def foo a:&:@:&:num [
    235   local-scope
    236   load-ingredients
    237   b:&:@:&:num <- new {(address number): type}, 3
    238   # modify b, completely unrelated to immutable ingredient a
    239   x:&:num <- index *b, 0
    240   *x <- copy 34
    241 ]
    242 $error: 0
    243
    244 :(scenario latter_ingredient_of_index_is_immutable)
    245 def main [
    246   # don't run anything
    247 ]
    248 def foo a:&:@:&:@:num, b:num -> a:&:@:&:@:num [
    249   local-scope
    250   load-ingredients
    251   x:&:@:num <- index *a, b
    252   *x <- put-index *x, 0, 34
    253 ]
    254 $error: 0
    255
    256 :(scenario can_traverse_immutable_ingredients)
    257 container test-list [
    258   next:&:test-list
    259 ]
    260 def main [
    261   local-scope
    262   p:&:test-list <- new test-list:type
    263   foo p
    264 ]
    265 def foo p:&:test-list [
    266   local-scope
    267   load-ingredients
    268   p2:&:test-list <- bar p
    269 ]
    270 def bar x:&:test-list -> y:&:test-list [
    271   local-scope
    272   load-ingredients
    273   y <- get *x, next:offset
    274 ]
    275 $error: 0
    276
    277 :(scenario treat_optional_ingredients_as_mutable)
    278 def main [
    279   k:&:num <- new number:type
    280   test k
    281 ]
    282 # recipe taking an immutable address ingredient
    283 def test k:&:num [
    284   local-scope
    285   load-ingredients
    286   foo k
    287 ]
    288 # ..calling a recipe with an optional address ingredient
    289 def foo -> [
    290   local-scope
    291   load-ingredients
    292   k:&:num, found?:bool <- next-ingredient
    293   # we don't further check k for immutability, but assume it's mutable
    294 ]
    295 $error: 0
    296
    297 :(scenario treat_optional_ingredients_as_mutable_2)
    298 % Hide_errors = true;
    299 def main [
    300   local-scope
    301   p:&:point <- new point:type
    302   foo p
    303 ]
    304 def foo p:&:point [
    305   local-scope
    306   load-ingredients
    307   bar p
    308 ]
    309 def bar [
    310   local-scope
    311   load-ingredients
    312   p:&:point <- next-ingredient  # optional ingredient; assumed to be mutable
    313 ]
    314 +error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product
    315
    316 //: when checking for immutable ingredients, remember to take space into account
    317 :(scenario check_space_of_reagents_in_immutability_checks)
    318 def main [
    319   a:space/names:new-closure <- new-closure
    320   b:&:num <- new number:type
    321   run-closure b:&:num, a:space
    322 ]
    323 def new-closure [
    324   local-scope
    325   x:&:num <- new number:type
    326   return default-space/names:new-closure
    327 ]
    328 def run-closure x:&:num, s:space/names:new-closure [
    329   local-scope
    330   load-ingredients
    331   0:space/names:new-closure <- copy s
    332   # different space; always mutable
    333   *x:&:num/space:1 <- copy 34
    334 ]
    335 $error: 0
    336
    337 :(before "End Transforms")
    338 Transform.push_back(check_immutable_ingredients);  // idempotent
    339
    340 :(code)
    341 void check_immutable_ingredients(const recipe_ordinal r) {
    342   // to ensure an address reagent isn't modified, it suffices to show that
    343   //   a) we never write to its contents directly,
    344   //   b) we never call 'put' or 'put-index' on it, and
    345   //   c) any non-primitive recipe calls in the body aren't returning it as a product
    346   const recipe& caller = get(Recipe, r);
    347   trace(9991, "transform") << "--- check mutability of ingredients in recipe " << caller.name << end();
    348   if (!caller.has_header) return;  // skip check for old-style recipes calling next-ingredient directly
    349   for (int i = 0;  i < SIZE(caller.ingredients);  ++i) {
    350     const reagent& current_ingredient = caller.ingredients.at(i);
    351     if (is_present_in_products(caller, current_ingredient.name)) continue;  // not expected to be immutable
    352     // End Immutable Ingredients Special-cases
    353     set<reagent, name_and_space_lt> immutable_vars;
    354     immutable_vars.insert(current_ingredient);
    355     for (int i = 0;  i < SIZE(caller.steps);  ++i) {
    356       const instruction& inst = caller.steps.at(i);
    357       check_immutable_ingredient_in_instruction(inst, immutable_vars, current_ingredient.name, caller);
    358       if (inst.operation == INDEX && SIZE(inst.ingredients) > 1 && inst.ingredients.at(1).name == current_ingredient.name) continue;
    359       update_aliases(inst, immutable_vars);
    360     }
    361   }
    362 }
    363
    364 void update_aliases(const instruction& inst, set<reagent, name_and_space_lt>& current_ingredient_and_aliases) {
    365   set<int> current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases);
    366   if (!contains_key(Recipe, inst.operation)) {
    367     // primitive recipe
    368     switch (inst.operation) {
    369       case COPY:
    370         for (set<int>::iterator p = current_ingredient_indices.begin();  p != current_ingredient_indices.end();  ++p)
    371           current_ingredient_and_aliases.insert(inst.products.at(*p).name);
    372         break;
    373       case GET:
    374       case INDEX:
    375       case MAYBE_CONVERT:
    376         // current_ingredient_indices can only have 0 or one value
    377         if (!current_ingredient_indices.empty() && !inst.products.empty()) {
    378           if (is_mu_address(inst.products.at(0)) || is_mu_container(inst.products.at(0)) || is_mu_exclusive_container(inst.products.at(0)))
    379             current_ingredient_and_aliases.insert(inst.products.at(0));
    380         }
    381         break;
    382       default: break;
    383     }
    384   }
    385   else {
    386     // defined recipe
    387     set<int> contained_in_product_indices = scan_contained_in_product_indices(inst, current_ingredient_indices);
    388     for (set<int>::iterator p = contained_in_product_indices.begin();  p != contained_in_product_indices.end();  ++p) {
    389       if (*p < SIZE(inst.products))
    390         current_ingredient_and_aliases.insert(inst.products.at(*p));
    391     }
    392   }
    393 }
    394
    395 set<int> scan_contained_in_product_indices(const instruction& inst, set<int>& ingredient_indices) {
    396   set<reagent, name_and_space_lt> selected_ingredients;
    397   const recipe& callee = get(Recipe, inst.operation);
    398   for (set<int>::iterator p = ingredient_indices.begin();  p != ingredient_indices.end();  ++p) {
    399     if (*p >= SIZE(callee.ingredients)) continue;  // optional immutable ingredient
    400     selected_ingredients.insert(callee.ingredients.at(*p));
    401   }
    402   set<int> result;
    403   for (int i = 0;  i < SIZE(callee.products);  ++i) {
    404     const reagent& current_product = callee.products.at(i);
    405     const string_tree* contained_in_name = property(current_product, "contained-in");
    406     if (contained_in_name && selected_ingredients.find(contained_in_name->value) != selected_ingredients.end())
    407       result.insert(i);
    408   }
    409   return result;
    410 }
    411
    412 bool is_mu_container(const reagent& r) {
    413   return is_mu_container(r.type);
    414 }
    415 bool is_mu_container(const type_tree* type) {
    416   if (!type) return false;
    417   if (!type->atom)
    418     return is_mu_container(get_base_type(type));
    419   if (type->value == 0) return false;
    420   if (!contains_key(Type, type->value)) return false;  // error raised elsewhere
    421   type_info& info = get(Type, type->value);
    422   return info.kind == CONTAINER;
    423 }
    424
    425 bool is_mu_exclusive_container(const reagent& r) {
    426   return is_mu_exclusive_container(r.type);
    427 }
    428 bool is_mu_exclusive_container(const type_tree* type) {
    429   if (!type) return false;
    430   if (!type->atom)
    431     return is_mu_exclusive_container(get_base_type(type));
    432   if (type->value == 0) return false;
    433   if (!contains_key(Type, type->value)) return false;  // error raised elsewhere
    434   type_info& info = get(Type, type->value);
    435   return info.kind == EXCLUSIVE_CONTAINER;
    436 }
    437
    438 :(before "End Types")
    439 // reagent comparison -- only in the context of a single recipe
    440 struct name_and_space_lt {
    441   bool operator()(const reagent& a, const reagent& b) const;
    442 };
    443 :(code)
    444 bool name_and_space_lt::operator()(const reagent& a, const reagent& b) const {
    445   int aspace = 0, bspace = 0;
    446   if (has_property(a, "space")) aspace = to_integer(property(a, "space")->value);
    447   if (has_property(b, "space")) bspace = to_integer(property(b, "space")->value);
    448   if (aspace != bspace) return aspace < bspace;
    449   return a.name < b.name;
    450 }
    451
    452 :(scenarios transform)
    453 :(scenario immutability_infects_contained_in_variables)
    454 % Hide_errors = true;
    455 container test-list [
    456   value:num
    457   next:&:test-list
    458 ]
    459 def main [
    460   local-scope
    461   p:&:test-list <- new test-list:type
    462   foo p
    463 ]
    464 def foo p:&:test-list [  # p is immutable
    465   local-scope
    466   load-ingredients
    467   p2:&:test-list <- test-next p  # p2 is immutable
    468   *p2 <- put *p2, value:offset, 34
    469 ]
    470 def test-next x:&:test-list -> y:&:test-list/contained-in:x [
    471   local-scope
    472   load-ingredients
    473   y <- get *x, next:offset
    474 ]
    475 +error: foo: cannot modify 'p2' in instruction '*p2 <- put *p2, value:offset, 34' because that would modify p which is an ingredient of recipe foo but not also a product
    476
    477 :(code)
    478 void check_immutable_ingredient_in_instruction(const instruction& inst, const set<reagent, name_and_space_lt>& current_ingredient_and_aliases, const string& original_ingredient_name, const recipe& caller) {
    479   // first check if the instruction is directly modifying something it shouldn't
    480   for (int i = 0;  i < SIZE(inst.products);  ++i) {
    481     if (has_property(inst.products.at(i), "lookup")
    482         && current_ingredient_and_aliases.find(inst.products.at(i)) != current_ingredient_and_aliases.end()) {
    483       string current_product_name = inst.products.at(i).name;
    484       if (current_product_name == original_ingredient_name)
    485         raise << maybe(caller.name) << "cannot modify '" << current_product_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end();
    486       else
    487         raise << maybe(caller.name) << "cannot modify '" << current_product_name << "' in instruction '" << to_original_string(inst) << "' because that would modify " << original_ingredient_name << " which is an ingredient of recipe " << caller.name << " but not also a product\n" << end();
    488       return;
    489     }
    490   }
    491   // check if there's any indirect modification going on
    492   set<int> current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases);
    493   if (current_ingredient_indices.empty()) return;  // ingredient not found in call
    494   for (set<int>::iterator p = current_ingredient_indices.begin();  p != current_ingredient_indices.end();  ++p) {
    495     const int current_ingredient_index = *p;
    496     reagent current_ingredient = inst.ingredients.at(current_ingredient_index);
    497     canonize_type(current_ingredient);
    498     const string& current_ingredient_name = current_ingredient.name;
    499     if (!contains_key(Recipe, inst.operation)) {
    500       // primitive recipe
    501       // we got here only because we got an instruction with an implicit product, and the instruction didn't explicitly spell it out
    502       //    put x, y:offset, z
    503       // instead of
    504       //    x <- put x, y:offset, z
    505       if (inst.operation == PUT || inst.operation == PUT_INDEX) {
    506         if (current_ingredient_index == 0) {
    507           if (current_ingredient_name == original_ingredient_name)
    508             raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end();
    509           else
    510             raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because that would modify '" << original_ingredient_name << "' which is an ingredient of recipe " << caller.name << " but not also a product\n" << end();
    511         }
    512       }
    513     }
    514     else {
    515       // defined recipe
    516       if (is_modified_in_recipe(inst.operation, current_ingredient_index, caller)) {
    517         if (current_ingredient_name == original_ingredient_name)
    518           raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end();
    519         else
    520           raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because that would modify '" << original_ingredient_name << "' which is an ingredient of recipe " << caller.name << " but not also a product\n" << end();
    521       }
    522     }
    523   }
    524 }
    525
    526 bool is_modified_in_recipe(const recipe_ordinal r, const int ingredient_index, const recipe& caller) {
    527   const recipe& callee = get(Recipe, r);
    528   if (!callee.has_header) {
    529     raise << maybe(caller.name) << "can't check mutability of ingredients in recipe " << callee.name << " because it uses 'next-ingredient' directly, rather than a recipe header.\n" << end();
    530     return true;
    531   }
    532   if (ingredient_index >= SIZE(callee.ingredients)) return false;  // optional immutable ingredient
    533   return is_present_in_products(callee, callee.ingredients.at(ingredient_index).name);
    534 }
    535
    536 bool is_present_in_products(const recipe& callee, const string& ingredient_name) {
    537   for (int i = 0;  i < SIZE(callee.products);  ++i) {
    538     if (callee.products.at(i).name == ingredient_name)
    539       return true;
    540   }
    541   return false;
    542 }
    543
    544 set<int> ingredient_indices(const instruction& inst, const set<reagent, name_and_space_lt>& ingredient_names) {
    545   set<int> result;
    546   for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
    547     if (is_literal(inst.ingredients.at(i))) continue;
    548     if (ingredient_names.find(inst.ingredients.at(i)) != ingredient_names.end())
    549       result.insert(i);
    550   }
    551   return result;
    552 }
    553
    554 //: Sometimes you want to pass in two addresses, one pointing inside the
    555 //: other. For example, you want to delete a node from a linked list. You
    556 //: can't pass both pointers back out, because if a caller tries to make both
    557 //: identical then you can't tell which value will be written on the way out.
    558 //:
    559 //: Experimental solution: just tell Mu that one points inside the other.
    560 //: This way we can return just one pointer as high up as necessary to capture
    561 //: all modifications performed by a recipe.
    562 //:
    563 //: We'll see if we end up wanting to abuse /contained-in for other reasons.
    564
    565 :(scenarios transform)
    566 :(scenario can_modify_contained_in_addresses)
    567 container test-list [
    568   value:num
    569   next:&:test-list
    570 ]
    571 def main [
    572   local-scope
    573   p:&:test-list <- new test-list:type
    574   foo p
    575 ]
    576 def foo p:&:test-list -> p:&:test-list [
    577   local-scope
    578   load-ingredients
    579   p2:&:test-list <- test-next p
    580   p <- test-remove p2, p
    581 ]
    582 def test-next x:&:test-list -> y:&:test-list [
    583   local-scope
    584   load-ingredients
    585   y <- get *x, next:offset
    586 ]
    587 def test-remove x:&:test-list/contained-in:from, from:&:test-list -> from:&:test-list [
    588   local-scope
    589   load-ingredients
    590   *x <- put *x, value:offset, 34  # can modify x
    591 ]
    592 $error: 0
    593
    594 :(before "End Immutable Ingredients Special-cases")
    595 if (has_property(current_ingredient, "contained-in")) {
    596   const string_tree* tmp = property(current_ingredient, "contained-in");
    597   if (!tmp->atom
    598       || (!is_present_in_ingredients(caller, tmp->value)
    599           && !is_present_in_products(caller, tmp->value))) {
    600     raise << maybe(caller.name) << "/contained-in can only point to another ingredient or product, but got '" << to_string(property(current_ingredient, "contained-in")) << "'\n" << end();
    601   }
    602   continue;
    603 }
    604
    605 :(scenario contained_in_product)
    606 container test-list [
    607   value:num
    608   next:&:test-list
    609 ]
    610 def foo x:&:test-list/contained-in:result -> result:&:test-list [
    611   local-scope
    612   load-ingredients
    613   result <- copy null
    614 ]
    615 $error: 0
    616
    617 :(scenario contained_in_is_mutable)
    618 container test-list [
    619   value:num
    620   next:&:test-list
    621 ]
    622 def foo x:&:test-list/contained-in:result -> result:&:test-list [
    623   local-scope
    624   load-ingredients
    625   result <- copy x
    626   put *x, value:offset, 34
    627 ]
    628 $error: 0