about summary refs log tree commit diff stats
path: root/060immutable.cc
blob: 9acd74e3a46afa9e9549709d55220e91d2cdb045 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//: Addresses passed into of a recipe are meant to be immutable unless they're
//: also products. This layer will start enforcing this check.

:(scenario can_modify_value_ingredients)
% Hide_warnings = true;
recipe main [
  local-scope
  p:address:point <- new point:type
  foo *p
]
recipe foo p:point [
  local-scope
  load-ingredients
  x:address:number <- get-address p, x:offset
  *x <- copy 34
]
$warn: 0

:(scenario can_modify_ingredients_that_are_also_products)
% Hide_warnings = true;
recipe main [
  local-scope
  p:address:point <- new point:type
  p <- foo p
]
recipe foo p:address:point -> p:address:point [
  local-scope
  load-ingredients
  x:address:number <- get-address *p, x:offset
  *x <- copy 34
]
$warn: 0

:(scenario cannot_take_address_inside_immutable_ingredients)
% Hide_warnings = true;
recipe main [
  local-scope
  p:address:point <- new point:type
  foo p
]
recipe foo p:address:point [
  local-scope
  load-ingredients
  x:address:number <- get-address *p, x:offset
  *x <- copy 34
]
+warn: foo: cannot modify ingredient p after instruction 'x:address:number <- get-address *p, x:offset' because it's not also a product of foo

:(scenario cannot_call_mutating_recipes_on_immutable_ingredients)
% Hide_warnings = true;
recipe main [
  local-scope
  p:address:point <- new point:type
  foo p
]
recipe foo p:address:point [
  local-scope
  load-ingredients
  bar p
]
recipe bar p:address:point -> p:address:point [
  local-scope
  load-ingredients
  x:address:number <- get-address *p, x:offset
  *x <- copy 34
]
+warn: foo: cannot modify ingredient p at instruction 'bar p' because it's not also a product of foo

:(scenario can_traverse_immutable_ingredients)
% Hide_warnings = true;
container test-list [
  next:address:test-list
]
recipe main [
  local-scope
  p:address:test-list <- new test-list:type
  foo p
]
recipe foo p:address:test-list [
  local-scope
  load-ingredients
  p2:address:test-list <- bar p
]
recipe bar x:address:test-list -> y:address:test-list [
  local-scope
  load-ingredients
  y <- get *x, next:offset
]
$warn: 0

:(before "End Transforms")
Transform.push_back(check_immutable_ingredients);  // idempotent

:(code)
void check_immutable_ingredients(recipe_ordinal r) {
  // to ensure a reagent isn't modified, it suffices to show that we never
  // call get-address or index-address with it, and that any non-primitive
  // recipe calls in the body aren't returning it as a product.
  const recipe& caller = get(Recipe, r);
  if (!caller.has_header) return;  // skip check for old-style recipes calling next-ingredient directly
  for (long long int i = 0; i < SIZE(caller.ingredients); ++i) {
    const reagent& current_ingredient = caller.ingredients.at(i);
    if (!is_mu_address(current_ingredient)) continue;  // will be copied
    if (is_present_in_products(caller, current_ingredient.name)) continue;  // not expected to be immutable
    // End Immutable Ingredients Special-cases
    for (long long int i = 0; i < SIZE(caller.steps); ++i) {
      check_immutable_ingredient_in_instruction(caller.steps.at(i), current_ingredient.name, caller);
    }
  }
}

void check_immutable_ingredient_in_instruction(const instruction& inst, const string& current_ingredient_name, const recipe& caller) {
  long long int current_ingredient_index = find_ingredient_index(inst, current_ingredient_name);
  if (current_ingredient_index == -1) return;  // ingredient not found in call
  reagent current_ingredient = inst.ingredients.at(current_ingredient_index);
  canonize_type(current_ingredient);
  if (!contains_key(Recipe, inst.operation)) {
    // primitive recipe
    if (inst.operation == GET_ADDRESS || inst.operation == INDEX_ADDRESS)
      raise << maybe(caller.name) << "cannot modify ingredient " << current_ingredient_name << " after instruction '" << inst.to_string() << "' because it's not also a product of " << caller.name << '\n' << end();
  }
  else {
    // defined recipe
    if (!is_mu_address(current_ingredient)) return;  // making a copy is ok
    if (is_modified_in_recipe(inst.operation, current_ingredient_index, caller))
      raise << maybe(caller.name) << "cannot modify ingredient " << current_ingredient_name << " at instruction '" << inst.to_string() << "' because it's not also a product of " << caller.name << '\n' << end();
  }
}

bool is_modified_in_recipe(recipe_ordinal r, long long int ingredient_index, const recipe& caller) {
  const recipe& callee = get(Recipe, r);
  if (!callee.has_header) {
    raise << maybe(caller.name) << "can't check mutability of ingredients in " << callee.name << " because it uses 'next-ingredient' directly, rather than a recipe header.\n" << end();
    return true;
  }
  return is_present_in_products(callee, callee.ingredients.at(ingredient_index).name);
}

bool is_present_in_products(const recipe& callee, const string& ingredient_name) {
  for (long long int i = 0; i < SIZE(callee.products); ++i) {
    if (callee.products.at(i).name == ingredient_name)
      return true;
  }
  return false;
}

bool is_present_in_ingredients(const recipe& callee, const string& ingredient_name) {
  for (long long int i = 0; i < SIZE(callee.ingredients); ++i) {
    if (callee.ingredients.at(i).name == ingredient_name)
      return true;
  }
  return false;
}

long long int find_ingredient_index(const instruction& inst, const string& ingredient_name) {
  for (long long int i = 0; i < SIZE(inst.ingredients); ++i) {
    if (inst.ingredients.at(i).name == ingredient_name)
      return i;
  }
  return -1;
}

//: Sometimes you want to pass in two addresses, one pointing inside the
//: other. For example, you want to delete a node from a linked list. You
//: can't pass both pointers back out, because if a caller tries to make both
//: identical then you can't tell which value will be written on the way out.
//:
//: Experimental solution: just tell mu that one points inside the other.
//: This way we can return just one pointer as high up as necessary to capture
//: all modifications performed by a recipe.
//:
//: We'll see if we end up wanting to abuse /contained-in for other reasons.

:(scenarios transform)
:(scenario can_modify_contained_in_addresses)
#% Hide_warnings = true;
container test-list [
  next:address:test-list
]
recipe main [
  local-scope
  p:address:test-list <- new test-list:type
  foo p
]
recipe foo p:address:test-list -> p:address:test-list [
  local-scope
  load-ingredients
  p2:address:test-list <- test-next p
  p <- test-remove p2, p
]
recipe test-next x:address:test-list -> y:address:test-list [
  local-scope
  load-ingredients
  y <- get *x, next:offset
]
recipe test-remove x:address:test-list/contained-in:from, from:address:test-list -> from:address:test-list [
  local-scope
  load-ingredients
  x2:address:address:test-list <- get-address *x, next:offset  # pretend modification
]
$warn: 0

:(before "End Immutable Ingredients Special-cases")
if (has_property(current_ingredient, "contained-in")) {
  const string_tree* tmp = property(current_ingredient, "contained-in");
  if (tmp->left || tmp->right
      || !is_present_in_ingredients(caller, tmp->value)
      || !is_present_in_products(caller, tmp->value))
    raise_error << maybe(caller.name) << "contained-in can only point to another ingredient+product, but got " << debug_string(property(current_ingredient, "contained-in")) << '\n' << end();
  continue;
}