//: Ingredients of a recipe are meant to be immutable unless they're also //: products. This layer will start enforcing this check. //: //: One hole for now: variables in surrounding spaces are implicitly mutable. //: [tag: todo] void test_can_modify_ingredients_that_are_also_products() { run( // mutable container "def main [\n" " local-scope\n" " p:point <- merge 34, 35\n" " p <- foo p\n" "]\n" "def foo p:point -> p:point [\n" " local-scope\n" " load-ingredients\n" " p <- put p, x:offset, 34\n" "]\n" ); CHECK_TRACE_COUNT("error", 0); } void test_can_modify_ingredients_that_are_also_products_2() { run( "def main [\n" " local-scope\n" " p:&:point <- new point:type\n" " p <- foo p\n" "]\n" // mutable address to container "def foo p:&:point -> p:&:point [\n" " local-scope\n" " load-ingredients\n" " *p <- put *p, x:offset, 34\n" "]\n" ); CHECK_TRACE_COUNT("error", 0); } void test_can_modify_ingredients_that_are_also_products_3() { run( "def main [\n" " local-scope\n" " p:&:@:num <- new number:type, 3\n" " p <- foo p\n" "]\n" // mutable address "def foo p:&:@:num -> p:&:@:num [\n" " local-scope\n" " load-ingredients\n" " *p <- put-index *p, 0, 34\n" "]\n" ); CHECK_TRACE_COUNT("error", 0); } void test_ignore_literal_ingredients_for_immutability_checks() { run( "def main [\n" " local-scope\n" " p:&:d1 <- new d1:type\n" " q:num <- foo p\n" "]\n" "def foo p:&:d1 -> q:num [\n" " local-scope\n" " load-ingredients\n" " x:&:d1 <- new d1:type\n" " *x <- put *x, p:offset, 34\n" // ignore this 'p' " return 36\n" "]\n" "container d1 [\n" " p:num\n" " q:num\n" "]\n" ); CHECK_TRACE_COUNT("error", 0); } void test_cannot_modify_immutable_ingredients() { Hide_errors = true; run( "def main [\n" " local-scope\n" " x:&:num <- new number:type\n" " foo x\n" "]\n" // immutable address to primitive "def foo x:&:num [\n" " local-scope\n" " load-ingredients\n" " *x <- copy 34\n" "]\n" ); CHECK_TRACE_CONTENTS( "error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product\n" ); } void test_cannot_modify_immutable_containers() { Hide_errors = true; run( "def main [\n" " local-scope\n" " x:point-number <- merge 34, 35, 36\n" " foo x\n" "]\n" // immutable container "def foo x:point-number [\n" " local-scope\n" " load-ingredients\n" // copy an element: ok " y:point <- get x, xy:offset\n" // modify the element: boom // This could be ok if y contains no addresses, but we're not going to try to be that smart. // It also makes the rules easier to reason about. If it's just an ingredient, just don't try to change it. " y <- put y, x:offset, 37\n" "]\n" ); CHECK_TRACE_CONTENTS( "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\n" ); } void test_can_modify_immutable_pointers() { run( "def main [\n" " local-scope\n" " x:&:num <- new number:type\n" " foo x\n" "]\n" "def foo x:&:num [\n" " local-scope\n" " load-ingredients\n" // modify the address, not the payload " x <- copy null\n" "]\n" ); CHECK_TRACE_COUNT("error", 0); } void test_can_modify_immutable_pointers_but_not_their_payloads() { Hide_errors = true; run( "def main [\n" " local-scope\n" " x:&:num <- new number:type\n" " foo x\n" "]\n" "def foo x:&:num [\n" " local-scope\n" " load-ingredients\n" // modify address: ok " x <- new number:type\n" // modify payload: boom // this could be ok, but we're not going to try to be that smart " *x <- copy 34\n" "]\n" ); CHECK_TRACE_CONTENTS( "error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product\n" ); } void test_cannot_call_mutating_recipes_on_immutable_ingredients() { Hide_errors = true; run( "def main [\n" " local-scope\n" " p:&:point <- new point:type\n" " foo p\n" "]\n" "def foo p:&:point [\n" " local-scope\n" " load-ingredients\n" " bar p\n" "]\n" "def bar p:&:point -> p:&:point [\n" " local-scope\n" " load-ingredients\n" // p could be modified here, but it doesn't have to be; it's already // marked mutable in the header "]\n" ); CHECK_TRACE_CONTENTS( "error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product\n" ); } void test_cannot_modify_copies_of_immutable_ingredients() { Hide_errors = true; run( "def main [\n" " local-scope\n" " p:&:point <- new point:type\n" " foo p\n" "]\n" "def foo p:&:point [\n" " local-scope\n" " load-ingredients\n" " q:&:point <- copy p\n" " *q <- put *q, x:offset, 34\n" "]\n" ); CHECK_TRACE_CONTENTS( "error
//: Let's raise errors when students use real hardware in any recipes besides
//: 'main'. Part of the goal is to teach them testing hygiene and dependency
//: injection.
//:
//: This is easy to sidestep, it's for feedback rather than safety.

:(before "End Globals")
vector<type_tree*> Real_hardware_types;
:(before "Begin transform_all")
setup_real_hardware_types();
:(before "End transform_all")
teardown_real_hardware_types();
:(code)
void setup_real_hardware_types() {
  Real_hardware_types.push_back(parse_type("address:screen"));
  Real_hardware_types.push_back(parse_type("address:console"));
  Real_hardware_types.push_back(parse_type("address:resources"));
}
type_tree* parse_type(string s) {
  reagent x("x:"+s);
  type_tree* result = x.type;
  x.type = NULL;  // don't deallocate on return
  return result;
}
void teardown_real_hardware_types() {
  for (int i = 0;  i < SIZE(Real_hardware_types);  ++i)
    delete Real_hardware_types.at(i);
  Real_hardware_types.clear();
}

:(before "End Checks")
Transform.push_back(check_for_misuse_of_real_hardware);
:(code)
void check_for_misuse_of_real_hardware(const recipe_ordinal r) {
  const recipe& caller = get(Recipe, r);
  if (caller.name == "main") return;
  if (starts_with(caller.name, "scenario_")) return;
  trace(101, "transform") << "--- check if recipe " << caller.name << " has any dependency-injection mistakes" << end();
  for (int index = 0;  index < SIZE(caller.steps);  ++index) {
    const instruction& inst = caller.steps.at(index);
    if (is_primitive(inst.operation)) continue;
    for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
      const reagent& ing = inst.ingredients.at(i);
      if (!is_literal(ing) || ing.name != "0") continue;
      const recipe& callee = get(Recipe, inst.operation);
      if (!callee.has_header) continue;
      if (i >= SIZE(callee.ingredients)) continue;
      const reagent& expected_ing = callee.ingredients.at(i);
      for (int j = 0;  j < SIZE(Real_hardware_types);  ++j) {
        if (*Real_hardware_types.at(j) == *expected_ing