https://github.com/akkartik/mu1/blob/master/074wait.cc
  1 //: Routines can be put in a 'waiting' state, from which it will be ready to
  2 //: run again when a specific memory location changes its value. This is Mu's
  3 //: basic technique for orchestrating the order in which different routines
  4 //: operate.
  5 
  6 :(scenario wait_for_location)
  7 def f1 [
  8   10:num <- copy 34
  9   start-running f2
 10   20:location <- copy 10/unsafe
 11   wait-for-reset-then-set 20:location
 12   # wait for f2 to run and reset location 1
 13   30:num <- copy 10:num
 14 ]
 15 def f2 [
 16   10:location <- copy 0/unsafe
 17 ]
 18 +schedule: f1
 19 +run: waiting for location 10 to reset
 20 +schedule: f2
 21 +schedule: waking up routine 1
 22 +schedule: f1
 23 +mem: storing 1 in location 30
 24 
 25 //: define the new state that all routines can be in
 26 
 27 :(before "End routine States")
 28 WAITING,
 29 :(before "End routine Fields")
 30 // only if state == WAITING
 31 int waiting_on_location;
 32 :(before "End routine Constructor")
 33 waiting_on_location = 0;
 34 
 35 :(before "End Mu Test Teardown")
 36 if (Passed && any_routines_waiting())
 37   raise << Current_scenario->name << ": deadlock!\n" << end();
 38 :(before "End Run Routine")
 39 if (any_routines_waiting()) {
 40   raise << "deadlock!\n" << end();
 41   dump_waiting_routines();
 42 }
 43 :(before "End Test Teardown")
 44 if (Passed && any_routines_with_error())
 45   raise << "some routines died with errors\n" << end();
 46 :(code)
 47 bool any_routines_waiting() {
 48   for (int i = 0;  i < SIZE(Routines);  ++i) {
 49     if (Routines.at(i)->state == WAITING)
 50       return true;
 51   }
 52   return false;
 53 }
 54 void dump_waiting_routines() {
 55   for (int i = 0;  i < SIZE(Routines);  ++i) {
 56     if (Routines.at(i)->state == WAITING)
 57       cerr << i << ": " << routine_label(Routines.at(i)) << '\n';
 58   }
 59 }
 60 
 61 :(scenario wait_for_location_can_deadlock)
 62 % Hide_errors = true;
 63 def main [
 64   10:num <- copy 1
 65   20:location <- copy 10/unsafe
 66   wait-for-reset-then-set 20:location
 67 ]
 68 +error: deadlock!
 69 
 70 //: Primitive recipe to put routines in that state.
 71 //: This primitive is also known elsewhere as compare-and-set (CAS). Used to
 72 //: build locks.
 73 
 74 :(before "End Primitive Recipe Declarations")
 75 WAIT_FOR_RESET_THEN_SET,
 76 :(before "End Primitive Recipe Numbers")
 77 put(Recipe_ordinal, "wait-for-reset-then-set", WAIT_FOR_RESET_THEN_SET);
 78 :(before "End Primitive Recipe Checks")
 79 case WAIT_FOR_RESET_THEN_SET: {
 80   if (SIZE(inst.ingredients) != 1) {
 81     raise << maybe(get(Recipe, r).name) << "'wait-for-reset-then-set' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
 82     break;
 83   }
 84   if (!is_mu_location(inst.ingredients.at(0))) {
 85     raise << maybe(get(Recipe, r).name) << "'wait-for-reset-then-set' requires a location ingredient, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
 86   }
 87   break;
 88 }
 89 :(before "End Primitive Recipe Implementations")
 90 case WAIT_FOR_RESET_THEN_SET: {
 91   int loc = static_cast<int>(ingredients.at(0).at(0));
 92   trace(9998, "run") << "wait: *" << loc << " = " << get_or_insert(Memory, loc) << end();
 93   if (get_or_insert(Memory, loc) == 0) {
 94     trace(9998, "run") << "location " << loc << " is already 0; setting" << end();
 95     put(Memory, loc, 1);
 96     break;
 97   }
 98   trace(9998, "run") << "waiting for location " << loc << " to reset" << end();
 99   Current_routine->state = WAITING;
100   Current_routine->waiting_on_location = loc;
101   break;
102 }
103 
104 //: Counterpart to unlock a lock.
105 :(before "End Primitive Recipe Declarations")
106 RESET,
107 :(before "End Primitive Recipe Numbers")
108 put(Recipe_ordinal, "reset", RESET);
109 :(before "End Primitive Recipe Checks")
110 case RESET: {
111   if (SIZE(inst.ingredients) != 1) {
112     raise << maybe(get(Recipe, r).name) << "'reset' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
113     break;
114   }
115   if (!is_mu_location(inst.ingredients.at(0))) {
116     raise << maybe(get(Recipe, r).name) << "'reset' requires a location ingredient, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
117   }
118   break;
119 }
120 :(before "End Primitive Recipe Implementations")
121 case RESET: {
122   int loc = static_cast<int>(ingredients.at(0).at(0));
123   put(Memory, loc, 0);
124   trace(9998, "run") << "reset: *" << loc << " = " << get_or_insert(Memory, loc) << end();
125   break;
126 }
127 
128 //: scheduler tweak to get routines out of that state
129 
130 :(before "End Scheduler State Transitions")
131 for (int i = 0;  i < SIZE(Routines);  ++i) {
132   if (Routines.at(i)->state != WAITING) continue;
133   int loc = Routines.at(i)->waiting_on_location;
134   if (loc && get_or_insert(Memory, loc) == 0) {
135     trace("schedule") << "waking up routine " << Routines.at(i)->id << end();
136     put(Memory, loc, 1);
137     Routines.at(i)->state = RUNNING;
138     Routines.at(i)->waiting_on_location = 0;
139   }
140 }
141 
142 //: Primitive to help compute locations to wait on.
143 //: Only supports elements immediately inside containers; no arrays or
144 //: containers within containers yet.
145 
146 :(scenario get_location)
147 def main [
148   12:num <- copy 34
149   13:num <- copy 35
150   15:location <- get-location 12:point, 1:offset
151 ]
152 +mem: storing 13 in location 15
153 
154 :(before "End Primitive Recipe Declarations")
155 GET_LOCATION,
156 :(before "End Primitive Recipe Numbers")
157 put(Recipe_ordinal, "get-location", GET_LOCATION);
158 :(before "End Primitive Recipe Checks")
159 case GET_LOCATION: {
160   if (SIZE(inst.ingredients) != 2) {
161     raise << maybe(get(Recipe, r).name) << "'get-location' expects exactly 2 ingredients in '" << to_original_string(inst) << "'\n" << end();
162     break;
163   }
164   reagent/*copy*/ base = inst.ingredients.at(0);
165   if (!canonize_type(base)) break;
166   if (!base.type) {
167     raise << maybe(get(Recipe, r).name) << "first ingredient of 'get-location' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
168     break;
169   }
170   const type_tree* base_root_type = base.type->atom ? base.type : base.type->left;
171   if (!base_root_type->atom || base_root_type->value == 0 || !contains_key(Type, base_root_type->value) || get(Type, base_root_type->value).kind != CONTAINER) {
172     raise << maybe(get(Recipe, r).name) << "first ingredient of 'get-location' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
173     break;
174   }
175   type_ordinal base_type = base.type->value;
176   const reagent& offset = inst.ingredients.at(1);
177   if (!is_literal(offset) || !is_mu_scalar(offset)) {
178     raise << maybe(get(Recipe, r).name) << "second ingredient of 'get-location' should have type 'offset', but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
179     break;
180   }
181   int offset_value = 0;
182   //: later layers will permit non-integer offsets
183   if (is_integer(offset.name)) {
184     offset_value = to_integer(offset.name);
185     if (offset_value < 0 || offset_value >= SIZE(get(Type, base_type).elements)) {
186       raise << maybe(get(Recipe, r).name) << "invalid offset " << offset_value << " for '" << get(Type, base_type).name << "'\n" << end();
187       break;
188     }
189   }
190   else {
191     offset_value = offset.value;
192   }
193   if (inst.products.empty()) break;
194   if (!is_mu_location(inst.products.at(0))) {
195     raise << maybe(get(Recipe, r).name) << "'get-location " << base.original_string << ", " << offset.original_string << "' should write to type location but '" << inst.products.at(0).name << "' has type '" << names_to_string_without_quotes(inst.products.at(0).type) << "'\n" << end();
196     break;
197   }
198   break;
199 }
200 :(before "End Primitive Recipe Implementations")
201 case GET_LOCATION: {
202   reagent/*copy*/ base = current_instruction().ingredients.at(0);
203   canonize(base);
204   int base_address = base.value;
205   if (base_address == 0) {
206     raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
207     break;
208   }
209   const type_tree* base_type = get_base_type(base.type);
210   int offset = ingredients.at(1).at(0);
211   if (offset < 0 || offset >= SIZE(get(Type, base_type->value).elements)) break;  // copied from Check above
212   int result = base_address;
213   for (int i = 0;  i < offset;  ++i)
214     result += size_of(element_type(base.type, i));
215   trace(9998, "run") << "address to copy is " << result << end();
216   products.resize(1);
217   products.at(0).push_back(result);
218   break;
219 }
220 
221 :(code)
222 bool is_mu_location(reagent/*copy*/ x) {
223   if (!canonize_type(x)) return false;
224   if (!x.type) return false;
225   if (!x.type->atom) return false;
226   return x.type->value == get(Type_ordinal, "location");
227 }
228 
229 :(scenario get_location_out_of_bounds)
230 % Hide_errors = true;
231 def main [
232   12:num <- copy 34
233   13:num <- copy 35
234   14:num <- copy 36
235   get-location 12:point-number/raw, 2:offset  # point-number occupies 3 locations but has only 2 fields; out of bounds
236 ]
237 +error: main: invalid offset 2 for 'point-number'
238 
239 :(scenario get_location_out_of_bounds_2)
240 % Hide_errors = true;
241 def main [
242   12:num <- copy 34
243   13:num <- copy 35
244   14:num <- copy 36
245   get-location 12:point-number/raw, -1:offset
246 ]
247 +error: main: invalid offset -1 for 'point-number'
248 
249 :(scenario get_location_product_type_mismatch)
250 % Hide_errors = true;
251 container boolbool [
252   x:bool
253   y:bool
254 ]
255 def main [
256   12:bool <- copy 1
257   13:bool <- copy 0
258   15:bool <- get-location 12:boolbool, 1:offset
259 ]
260 +error: main: 'get-location 12:boolbool, 1:offset' should write to type location but '15' has type 'boolean'
261 
262 :(scenario get_location_indirect)
263 # 'get-location' can read from container address
264 def main [
265   1:num/alloc-id, 2:num <- copy 0, 10
266   10:num/alloc-id, 11:num/x, 12:num/y <- copy 0, 34, 35
267   20:location <- get-location 1:&:point/lookup, 0:offset
268 ]
269 +mem: storing 11 in location 20
270 
271 :(scenario get_location_indirect_2)
272 def main [
273   1:num/alloc-id, 2:num <- copy 0, 10
274   10:num/alloc-id, 11:num/x, 12:num/y <- copy 0, 34, 35
275   4:num/alloc-id, 5:num <- copy 0, 20
276   4:&:location/lookup <- get-location 1:&:point/lookup, 0:offset
277 ]
278 +mem: storing 11 in location 21
279 
280 //: allow waiting on a routine to complete
281 
282 :(scenario wait_for_routine)
283 def f1 [
284   # add a few routines to run
285   1:num/routine <- start-running f2
286   2:num/routine <- start-running f3
287   wait-for-routine 1:num/routine
288   # now wait for f2 to *complete* and modify location 13 before using its value
289   20:num <- copy 13:num
290 ]
291 def f2 [
292   10:num <- copy 0  # just padding
293   switch  # simulate a block; routine f1 shouldn't restart at this point
294   13:num <- copy 34
295 ]
296 def f3 [
297   # padding routine just to help simulate the block in f2 using 'switch'
298   11:num <- copy 0
299   12:num <- copy 0
300 ]
301 +schedule: f1
302 +run: waiting for routine 2
303 +schedule: f2
304 +schedule: f3
305 +schedule: f2
306 +schedule: waking up routine 1
307 +schedule: f1
308 # if we got the synchronization wrong we'd be storing 0 in location 20
309 +mem: storing 34 in location 20
310 
311 :(before "End routine Fields")
312 // only if state == WAITING
313 int waiting_on_routine;
314 :(before "End routine Constructor")
315 waiting_on_routine = 0;
316 
317 :(before "End Primitive Recipe Declarations")
318 WAIT_FOR_ROUTINE,
319 :(before "End Primitive Recipe Numbers")
320 put(Recipe_ordinal, "wait-for-routine", WAIT_FOR_ROUTINE);
321 :(before "End Primitive Recipe Checks")
322 case WAIT_FOR_ROUTINE: {
323   if (SIZE(inst.ingredients) != 1) {
324     raise << maybe(get(Recipe, r).name) << "'wait-for-routine' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
325     break;
326   }
327   if (!is_mu_number(inst.ingredients.at(0))) {
328     raise << maybe(get(Recipe, r).name) << "first ingredient of 'wait-for-routine' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
329     break;
330   }
331   break;
332 }
333 :(before "End Primitive Recipe Implementations")
334 case WAIT_FOR_ROUTINE: {
335   if (ingredients.at(0).at(0) == Current_routine->id) {
336     raise << maybe(current_recipe_name()) << "routine can't wait for itself! '" << to_original_string(current_instruction()) << "'\n" << end();
337     break;
338   }
339   Current_routine->state = WAITING;
340   Current_routine->waiting_on_routine = ingredients.at(0).at(0);
341   trace(9998, "run") << "waiting for routine " << ingredients.at(0).at(0) << end();
342   break;
343 }
344 
345 :(before "End Scheduler State Transitions")
346 // Wake up any routines waiting for other routines to complete.
347 // Important: this must come after the scheduler loop above giving routines
348 // waiting for locations to change a chance to wake up.
349 for (int i = 0;  i < SIZE(Routines);  ++i) {
350   if (Routines.at(i)->state != WAITING) continue;
351   routine* waiter = Routines.at(i);
352   if (!waiter->waiting_on_routine) continue;
353   int id = waiter->waiting_on_routine;
354   assert(id != waiter->id);  // routine can't wait on itself
355   for (int j = 0;  j < SIZE(Routines);  ++j) {
356     const routine* waitee = Routines.at(j);
357     if (waitee->id == id && waitee->state != RUNNING && waitee->state != WAITING) {
358       // routine is COMPLETED or DISCONTINUED
359       trace("schedule") << "waking up routine " << waiter->id << end();
360       waiter->state = RUNNING;
361       waiter->waiting_on_routine = 0;
362     }
363   }
364 }
365 
366 //: yield voluntarily to let some other routine run
367 
368 :(before "End Primitive Recipe Declarations")
369 SWITCH,
370 :(before "End Primitive Recipe Numbers")
371 put(Recipe_ordinal, "switch", SWITCH);
372 :(before "End Primitive Recipe Checks")
373 case SWITCH: {
374   break;
375 }
376 :(before "End Primitive Recipe Implementations")
377 case SWITCH: {
378   ++current_step_index();
379   goto stop_running_current_routine;
380 }
381 
382 :(scenario switch_preempts_current_routine)
383 def f1 [
384   start-running f2
385   1:num <- copy 34
386   switch
387   3:num <- copy 36
388 ]
389 def f2 [
390   2:num <- copy 35
391 ]
392 +mem: storing 34 in location 1
393 # context switch
394 +mem: storing 35 in location 2
395 # back to original thread
396 +mem: storing 36 in location 3
397 
398 //:: helpers for manipulating routines in tests
399 //:
400 //: Managing arbitrary scenarios requires the ability to:
401 //:   a) check if a routine is blocked
402 //:   b) restart a blocked routine ('restart')
403 //:
404 //: A routine is blocked either if it's waiting or if it explicitly signals
405 //: that it's blocked (even as it periodically wakes up and polls for some
406 //: event).
407 //:
408 //: Signalling blockedness might well be a huge hack. But Mu doesn't have Unix
409 //: signals to avoid polling with, because signals are also pretty hacky.
410 
411 :(before "End routine Fields")
412 bool blocked;
413 :(before "End routine Constructor")
414 blocked = false;
415 
416 :(before "End Primitive Recipe Declarations")
417 CURRENT_ROUTINE_IS_BLOCKED,
418 :(before "End Primitive Recipe Numbers")
419 put(Recipe_ordinal, "current-routine-is-blocked", CURRENT_ROUTINE_IS_BLOCKED);
420 :(before "End Primitive Recipe Checks")
421 case CURRENT_ROUTINE_IS_BLOCKED: {
422   if (!inst.ingredients.empty()) {
423     raise << maybe(get(Recipe, r).name) << "'current-routine-is-blocked' should have no ingredients, but got '" << to_original_string(inst) << "'\n" << end();
424     break;
425   }
426   break;
427 }
428 :(before "End Primitive Recipe Implementations")
429 case CURRENT_ROUTINE_IS_BLOCKED: {
430   Current_routine->blocked = true;
431   break;
432 }
433 
434 :(before "End Primitive Recipe Declarations")
435 CURRENT_ROUTINE_IS_UNBLOCKED,
436 :(before "End Primitive Recipe Numbers")
437 put(Recipe_ordinal, "current-routine-is-unblocked", CURRENT_ROUTINE_IS_UNBLOCKED);
438 :(before "End Primitive Recipe Checks")
439 case CURRENT_ROUTINE_IS_UNBLOCKED: {
440   if (!inst.ingredients.empty()) {
441     raise << maybe(get(Recipe, r).name) << "'current-routine-is-unblocked' should have no ingredients, but got '" << to_original_string(inst) << "'\n" << end();
442     break;
443   }
444   break;
445 }
446 :(before "End Primitive Recipe Implementations")
447 case CURRENT_ROUTINE_IS_UNBLOCKED: {
448   Current_routine->blocked = false;
449   break;
450 }
451 
452 //: also allow waiting on a routine to block
453 //: (just for tests; use wait_for_routine above wherever possible)
454 
455 :(scenario wait_for_routine_to_block)
456 def f1 [
457   1:num/routine <- start-running f2
458   wait-for-routine-to-block 1:num/routine
459   # now wait for f2 to run and modify location 10 before using its value
460   11:num <- copy 10:num
461 ]
462 def f2 [
463   10:num <- copy 34
464 ]
465 +schedule: f1
466 +run: waiting for routine 2 to block
467 +schedule: f2
468 +schedule: waking up routine 1 because routine 2 is blocked
469 +schedule: f1
470 # if we got the synchronization wrong we'd be storing 0 in location 11
471 +mem: storing 34 in location 11
472 
473 :(before "End routine Fields")
474 // only if state == WAITING
475 int waiting_on_routine_to_block;
476 :(before "End routine Constructor")
477 waiting_on_routine_to_block = 0;
478 
479 :(before "End Primitive Recipe Declarations")
480 WAIT_FOR_ROUTINE_TO_BLOCK,
481 :(before "End Primitive Recipe Numbers")
482 put(Recipe_ordinal, "wait-for-routine-to-block", WAIT_FOR_ROUTINE_TO_BLOCK);
483 :(before "End Primitive Recipe Checks")
484 case WAIT_FOR_ROUTINE_TO_BLOCK: {
485   if (SIZE(inst.ingredients) != 1) {
486     raise << maybe(get(Recipe, r).name) << "'wait-for-routine-to-block' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
487     break;
488   }
489   if (!is_mu_number(inst.ingredients.at(0))) {
490     raise << maybe(get(Recipe, r).name) << "first ingredient of 'wait-for-routine-to-block' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
491     break;
492   }
493   break;
494 }
495 :(before "End Primitive Recipe Implementations")
496 case WAIT_FOR_ROUTINE_TO_BLOCK: {
497   if (ingredients.at(0).at(0) == Current_routine->id) {
498     raise << maybe(current_recipe_name()) << "routine can't wait for itself! '" << to_original_string(current_instruction()) << "'\n" << end();
499     break;
500   }
501   Current_routine->state = WAITING;
502   Current_routine->waiting_on_routine_to_block = ingredients.at(0).at(0);
503   trace(9998, "run") << "waiting for routine " << ingredients.at(0).at(0) << " to block" << end();
504   break;
505 }
506 
507 :(before "End Scheduler State Transitions")
508 // Wake up any routines waiting for other routines to stop running.
509 for (int i = 0;  i < SIZE(Routines);  ++i) {
510   if (Routines.at(i)->state != WAITING) continue;
511   routine* waiter = Routines.at(i);
512   if (!waiter->waiting_on_routine_to_block) continue;
513   int id = waiter->waiting_on_routine_to_block;
514   assert(id != waiter->id);  // routine can't wait on itself
515   for (int j = 0;  j < SIZE(Routines);  ++j) {
516     const routine* waitee = Routines.at(j);
517     if (waitee->id != id) continue;
518     if (waitee->state != RUNNING || waitee->blocked) {
519       trace("schedule") << "waking up routine " << waiter->id << " because routine " << waitee->id << " is blocked" << end();
520       waiter->state = RUNNING;
521       waiter->waiting_on_routine_to_block = 0;
522     }
523   }
524 }
525 
526 //: helper for restarting blocking routines in tests
527 
528 :(before "End Primitive Recipe Declarations")
529 RESTART,
530 :(before "End Primitive Recipe Numbers")
531 put(Recipe_ordinal, "restart", RESTART);
532 :(before "End Primitive Recipe Checks")
533 case RESTART: {
534   if (SIZE(inst.ingredients) != 1) {
535     raise << maybe(get(Recipe, r).name) << "'restart' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
536     break;
537   }
538   if (!is_mu_number(inst.ingredients.at(0))) {
539     raise << maybe(get(Recipe, r).name) << "first ingredient of 'restart' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
540     break;
541   }
542   break;
543 }
544 :(before "End Primitive Recipe Implementations")
545 case RESTART: {
546   int id = ingredients.at(0).at(0);
547   for (int i = 0;  i < SIZE(Routines);  ++i) {
548     if (Routines.at(i)->id == id) {
549       if (Routines.at(i)->state == WAITING)
550         Routines.at(i)->state = RUNNING;
551       Routines.at(i)->blocked = false;
552       break;
553     }
554   }
555   break;
556 }
557 
558 :(scenario cannot_restart_completed_routine)
559 % Scheduling_interval = 1;
560 def main [
561   local-scope
562   r:num/routine-id <- start-running f
563   x:num <- copy 0  # wait for f to be scheduled
564   # r is COMPLETED by this point
565   restart r  # should have no effect
566   x:num <- copy 0  # give f time to be scheduled (though it shouldn't be)
567 ]
568 def f [
569   1:num/raw <- copy 1
570 ]
571 # shouldn't crash
572 
573 :(scenario restart_blocked_routine)
574 % Scheduling_interval = 1;
575 def main [
576   local-scope
577   r:num/routine-id <- start-running f
578   wait-for-routine-to-block r  # get past the block in f below
579   restart r
580   wait-for-routine-to-block r  # should run f to completion
581 ]
582 # function with one block
583 def f [
584   current-routine-is-blocked
585   # 8 instructions of padding, many more than 'main' above
586   1:num <- add 1:num, 1
587   1:num <- add 1:num, 1
588   1:num <- add 1:num, 1
589   1:num <- add 1:num, 1
590   1:num <- add 1:num, 1
591   1:num <- add 1:num, 1
592   1:num <- add 1:num, 1
593   1:num <- add 1:num, 1
594   1:num <- add 1:num, 1
595 ]
596 # make sure all of f ran
597 +mem: storing 8 in location 1