1 //: Helper for various programming environments: run arbitrary Mu code and
  2 //: return some result in text form.
  3 
  4 :(scenario run_interactive_code)
  5 def main [
  6   1:num <- copy 0
  7   2:text <- new [1:num/raw <- copy 34]
  8   run-sandboxed 2:text
  9   3:num <- copy 1:num
 10 ]
 11 +mem: storing 34 in location 3
 12 
 13 :(scenario run_interactive_empty)
 14 def main [
 15   1:text <- copy 0/unsafe
 16   2:text <- run-sandboxed 1:text
 17 ]
 18 # result is null
 19 +mem: storing 0 in location 2
 20 
 21 //: As the name suggests, 'run-sandboxed' will prevent certain operations that
 22 //: regular Mu code can perform.
 23 :(before "End Globals")
 24 bool Sandbox_mode = false;
 25 //: for starters, users can't override 'main' when the environment is running
 26 :(before "End Load Recipe Name")
 27 if (Sandbox_mode && result.name == "main") {
 28   slurp_balanced_bracket(in);
 29   return -1;
 30 }
 31 
 32 //: run code in 'interactive mode', i.e. with errors off and return:
 33 //:   stringified output in case we want to print it to screen
 34 //:   any errors encountered
 35 //:   simulated screen any prints went to
 36 //:   any 'app' layer traces generated
 37 :(before "End Primitive Recipe Declarations")
 38 RUN_SANDBOXED,
 39 :(before "End Primitive Recipe Numbers")
 40 put(Recipe_ordinal, "run-sandboxed", RUN_SANDBOXED);
 41 :(before "End Primitive Recipe Checks")
 42 case RUN_SANDBOXED: {
 43   if (SIZE(inst.ingredients) != 1) {
 44   ¦ raise << maybe(get(Recipe, r).name) << "'run-sandboxed' requires exactly one ingredient, but got '" << inst.original_string << "'\n" << end();
 45   ¦ break;
 46   }
 47   if (!is_mu_text(inst.ingredients.at(0))) {
 48   ¦ raise << maybe(get(Recipe, r).name) << "first ingredient of 'run-sandboxed' should be a string, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
 49   ¦ break;
 50   }
 51   break;
 52 }
 53 :(before "End Primitive Recipe Implementations")
 54 case RUN_SANDBOXED: {
 55   bool new_code_pushed_to_stack = run_interactive(ingredients.at(0).at(0));
 56   if (!new_code_pushed_to_stack) {
 57   ¦ products.resize(5);
 58   ¦ products.at(0).push_back(0);
 59   ¦ products.at(1).push_back(trace_error_contents());
 60   ¦ products.at(2).push_back(0);
 61   ¦ products.at(3).push_back(trace_app_contents());
 62   ¦ products.at(4).push_back(1);  // completed
 63   ¦ run_code_end();
 64   ¦ break;  // done with this instruction
 65   }
 66   else {
 67   ¦ continue;  // not done with caller; don't increment current_step_index()
 68   }
 69 }
 70 
 71 //: To show results in the sandbox Mu uses a hack: it saves the products
 72 //: returned by each instruction while Track_most_recent_products is true, and
 73 //: keeps the most recent such result around so that it can be returned as the
 74 //: result of a sandbox.
 75 
 76 :(before "End Globals")
 77 bool Track_most_recent_products = false;
 78 int Call_depth_to_track_most_recent_products_at = 0;
 79 string Most_recent_products;
 80 :(before "End Setup")
 81 Track_most_recent_products = false;
 82 Call_depth_to_track_most_recent_products_at = 0;
 83 Most_recent_products = "";
 84 
 85 :(before "End Globals")
 86 trace_stream* Save_trace_stream = NULL;
 87 string Save_trace_file;
 88 :(code)
 89 // reads a string, tries to call it as code (treating it as a test), saving
 90 // all errors.
 91 // returns true if successfully called (no errors found during load and transform)
 92 bool run_interactive(int address) {
 93   assert(contains_key(Recipe_ordinal, "interactive") && get(Recipe_ordinal, "interactive") != 0);
 94   // try to sandbox the run as best you can
 95   // todo: test this
 96   if (!Current_scenario) {
 97   ¦ for (int i = 1; i < Reserved_for_tests; ++i)
 98   ¦ ¦ Memory.erase(i);
 99   }
100   string command = trim(strip_comments(read_mu_text(address)));
101   Name[get(Recipe_ordinal, "interactive")].clear();
102   run_code_begin(/*should_stash_snapshots*/true);
103   if (command.empty()) return false;
104   // don't kill the current routine on parse errors
105   routine* save_current_routine = Current_routine;
106   Current_routine = NULL;
107   // call run(string) but without the scheduling
108   load(string("recipe! interactive [\n") +
109   ¦ ¦ ¦ ¦ "new-default-space\n" +  // disable automatic abandon so tests can see changes
110   ¦ ¦ ¦ ¦ "screen:&:screen <- next-ingredient\n" +
111   ¦ ¦ ¦ ¦ "$start-tracking-products\n" +
112   ¦ ¦ ¦ ¦ command + "\n" +
113   ¦ ¦ ¦ ¦ "$stop-tracking-products\n" +
114   ¦ ¦ ¦ ¦ "return screen\n" +
115   ¦ ¦ ¦"]\n");
116   transform_all();
117   Current_routine = save_current_routine;
118   if (trace_count("error") > 0) return false;
119   // now call 'sandbox' which will run 'interactive' in a separate routine,
120   // and wait for it
121   if (Save_trace_stream) {
122   ¦ ++Save_trace_stream->callstack_depth;
123   ¦ trace(9999, "trace") << "run-sandboxed: incrementing callstack depth to " << Save_trace_stream->callstack_depth << end();
124   ¦ assert(Save_trace_stream->callstack_depth < 9000);  // 9998-101 plus cushion
125   }
126   Current_routine->calls.push_front(call(get(Recipe_ordinal, "sandbox")));
127   return true;
128 }
129 
130 //: Carefully update all state to exactly how it was -- including snapshots.
131 
132 :(before "End Globals")
133 bool Run_profiler_stash = false;
134 map<string, recipe_ordinal> Recipe_ordinal_snapshot_stash;
135 map<recipe_ordinal, recipe> Recipe_snapshot_stash;
136 map<string, type_ordinal> Type_ordinal_snapshot_stash;
137 map<type_ordinal, type_info> Type_snapshot_stash;
138 map<recipe_ordinal, map<string, int> > Name_snapshot_stash;
139 map<string, vector<recipe_ordinal> > Recipe_variants_snapshot_stash;
140 map<string, type_tree*> Type_abbreviations_snapshot_stash;
141 vector<scenario> Scenarios_snapshot_stash;
142 set<string> Scenario_names_snapshot_stash;
143 
144 :(code)
145 void run_code_begin(bool should_stash_snapshots) {
146   // stuff to undo later, in run_code_end()
147   Hide_errors = true;
148   Disable_redefine_checks = true;
149   Run_profiler_stash = Run_profiler;
150   Run_profiler = false;
151   if (should_stash_snapshots)
152   ¦ stash_snapshots();
153   Save_trace_stream = Trace_stream;
154   Trace_stream = new trace_stream;
155   Trace_stream->collect_depth = App_depth;
156 }
157 
158 void run_code_end() {
159   Hide_errors = false;
160   Disable_redefine_checks = false;
161   Run_profiler = Run_profiler_stash;
162   Run_profiler_stash = false;
163 //?   ofstream fout("sandbox.log");
164 //?   fout << Trace_stream->readable_contents("");
165 //?   fout.close();
166   delete Trace_stream;
167   Trace_stream = Save_trace_stream;
168   Save_trace_stream = NULL;
169   Save_trace_file.clear();
170   Recipe.erase(get(Recipe_ordinal, "interactive"));  // keep past sandboxes from inserting errors
171   if (!Recipe_snapshot_stash.empty())
172   ¦ unstash_snapshots();
173 }
174 
175 // keep sync'd with save_snapshots and restore_snapshots
176 void stash_snapshots() {
177   assert(Recipe_ordinal_snapshot_stash.empty());
178   Recipe_ordinal_snapshot_stash = Recipe_ordinal_snapshot;
179   assert(Recipe_snapshot_stash.empty());
180   Recipe_snapshot_stash = Recipe_snapshot;
181   assert(Type_ordinal_snapshot_stash.empty());
182   Type_ordinal_snapshot_stash = Type_ordinal_snapshot;
183   assert(Type_snapshot_stash.empty());
184   Type_snapshot_stash = Type_snapshot;
185   assert(Name_snapshot_stash.empty());
186   Name_snapshot_stash = Name_snapshot;
187   assert(Recipe_variants_snapshot_stash.empty());
188   Recipe_variants_snapshot_stash = Recipe_variants_snapshot;
189   assert(Type_abbreviations_snapshot_stash.empty());
190   Type_abbreviations_snapshot_stash = Type_abbreviations_snapshot;
191   assert(Scenarios_snapshot_stash.empty());
192   Scenarios_snapshot_stash = Scenarios_snapshot;
193   assert(Scenario_names_snapshot_stash.empty());
194   Scenario_names_snapshot_stash = Scenario_names_snapshot;
195   save_snapshots();
196 }
197 void unstash_snapshots() {
198   restore_snapshots();
199   Recipe_ordinal_snapshot = Recipe_ordinal_snapshot_stash;  Recipe_ordinal_snapshot_stash.clear();
200   Recipe_snapshot = Recipe_snapshot_stash;  Recipe_snapshot_stash.clear();
201   Type_ordinal_snapshot = Type_ordinal_snapshot_stash;  Type_ordinal_snapshot_stash.clear();
202   Type_snapshot = Type_snapshot_stash;  Type_snapshot_stash.clear();
203   Name_snapshot = Name_snapshot_stash;  Name_snapshot_stash.clear();
204   Recipe_variants_snapshot = Recipe_variants_snapshot_stash;  Recipe_variants_snapshot_stash.clear();
205   Type_abbreviations_snapshot = Type_abbreviations_snapshot_stash;  Type_abbreviations_snapshot_stash.clear();
206   Scenarios_snapshot = Scenarios_snapshot_stash;  Scenarios_snapshot_stash.clear();
207   Scenario_names_snapshot = Scenario_names_snapshot_stash;  Scenario_names_snapshot_stash.clear();
208 }
209 
210 :(before "End Load Recipes")
211 load(string(
212 "recipe interactive [\n") +  // just a dummy version to initialize the Recipe_ordinal and so on
213 "]\n" +
214 "recipe sandbox [\n" +
215   "local-scope\n" +
216   "screen:&:screen <- new-fake-screen 30, 5\n" +
217   "routine-id:num <- start-running interactive, screen\n" +
218   "limit-time routine-id, 100000/instructions\n" +
219   "wait-for-routine routine-id\n" +
220   "instructions-run:num <- number-of-instructions routine-id\n" +
221   "stash instructions-run [instructions run]\n" +
222   "sandbox-state:num <- routine-state routine-id\n" +
223   "completed?:bool <- equal sandbox-state, 1/completed\n" +
224   "output:text <- $most-recent-products\n" +
225   "errors:text <- save-errors\n" +
226   "stashes:text <- save-app-trace\n" +
227   "$cleanup-run-sandboxed\n" +
228   "return output, errors, screen, stashes, completed?\n" +
229 "]\n");
230 
231 //: adjust errors in the sandbox
232 :(after "string maybe(string s)")
233   if (s == "interactive") return "";
234 
235 :(scenario run_interactive_comments)
236 def main [
237   1:text <- new [# ab
238 add 2, 2]
239   2:text <- run-sandboxed 1:text
240   3:@:char <- copy *2:text
241 ]
242 +mem: storing 52 in location 4
243 
244 :(before "End Primitive Recipe Declarations")
245 _START_TRACKING_PRODUCTS,
246 :(before "End Primitive Recipe Numbers")
247 put(Recipe_ordinal, "$start-tracking-products", _START_TRACKING_PRODUCTS);
248 :(before "End Primitive Recipe Checks")
249 case _START_TRACKING_PRODUCTS: {
250   break;
251 }
252 :(before "End Primitive Recipe Implementations")
253 case _START_TRACKING_PRODUCTS: {
254   Track_most_recent_products = true;
255   Call_depth_to_track_most_recent_products_at = SIZE(Current_routine->calls);
256   break;
257 }
258 
259 :(before "End Primitive Recipe Declarations")
260 _STOP_TRACKING_PRODUCTS,
261 :(before "End Primitive Recipe Numbers")
262 put(Recipe_ordinal, "$stop-tracking-products", _STOP_TRACKING_PRODUCTS);
263 :(before "End Primitive Recipe Checks")
264 case _STOP_TRACKING_PRODUCTS: {
265   break;
266 }
267 :(before "End Primitive Recipe Implementations")
268 case _STOP_TRACKING_PRODUCTS: {
269   Track_most_recent_products = false;
270   break;
271 }
272 
273 :(before "End Primitive Recipe Declarations")
274 _MOST_RECENT_PRODUCTS,
275 :(before "End Primitive Recipe Numbers")
276 put(Recipe_ordinal, "$most-recent-products", _MOST_RECENT_PRODUCTS);
277 :(before "End Primitive Recipe Checks")
278 case _MOST_RECENT_PRODUCTS: {
279   break;
280 }
281 :(before "End Primitive Recipe Implementations")
282 case _MOST_RECENT_PRODUCTS: {
283   products.resize(1);
284   products.at(0).push_back(new_mu_text(Most_recent_products));
285   break;
286 }
287 
288 :(before "End Primitive Recipe Declarations")
289 SAVE_ERRORS,
290 :(before "End Primitive Recipe Numbers")
291 put(Recipe_ordinal, "save-errors", SAVE_ERRORS);
292 :(before "End Primitive Recipe Checks")
293 case SAVE_ERRORS: {
294   break;
295 }
296 :(before "End Primitive Recipe Implementations")
297 case SAVE_ERRORS: {
298   products.resize(1);
299   products.at(0).push_back(trace_error_contents());
300   break;
301 }
302 
303 :(before "End Primitive Recipe Declarations")
304 SAVE_APP_TRACE,
305 :(before "End Primitive Recipe Numbers")
306 put(Recipe_ordinal, "save-app-trace", SAVE_APP_TRACE);
307 :(before "End Primitive Recipe Checks")
308 case SAVE_APP_TRACE: {
309   break;
310 }
311 :(before "End Primitive Recipe Implementations")
312 case SAVE_APP_TRACE: {
313   products.resize(1);
314   products.at(0).push_back(trace_app_contents());
315   break;
316 }
317 
318 :(before "End Primitive Recipe Declarations")
319 _CLEANUP_RUN_SANDBOXED,
320 :(before "End Primitive Recipe Numbers")
321 put(Recipe_ordinal, "$cleanup-run-sandboxed", _CLEANUP_RUN_SANDBOXED);
322 :(before "End Primitive Recipe Checks")
323 case _CLEANUP_RUN_SANDBOXED: {
324   break;
325 }
326 :(before "End Primitive Recipe Implementations")
327 case _CLEANUP_RUN_SANDBOXED: {
328   run_code_end();
329   break;
330 }
331 
332 :(scenario "run_interactive_converts_result_to_text")
333 def main [
334   # try to interactively add 2 and 2
335   1:text <- new [add 2, 2]
336   2:text <- run-sandboxed 1:text
337   10:@:char <- copy *2:text
338 ]
339 # first letter in the output should be '4' in unicode
340 +mem: storing 52 in location 11
341 
342 :(scenario "run_interactive_ignores_products_in_nested_functions")
343 def main [
344   1:text <- new [foo]
345   2:text <- run-sandboxed 1:text
346   10:@:char <- copy *2:text
347 ]
348 def foo [
349   20:num <- copy 1234
350   {
351   ¦ break
352   ¦ reply 5678
353   }
354 ]
355 # no product should have been tracked
356 +mem: storing 0 in location 10
357 
358 :(scenario "run_interactive_ignores_products_in_previous_instructions")
359 def main [
360   1:text <- new [
361   ¦ add 1, 1  # generates a product
362   ¦ foo]  # no products
363   2:text <- run-sandboxed 1:text
364   10:@:char <- copy *2:text
365 ]
366 def foo [
367   20:num <- copy 1234
368   {
369   ¦ break
370   ¦ reply 5678
371   }
372 ]
373 # no product should have been tracked
374 +mem: storing 0 in location 10
375 
376 :(scenario "run_interactive_remembers_products_before_final_label")
377 def main [
378   1:text <- new [
379   ¦ add 1, 1  # generates a product
380   ¦ +foo]  # no products
381   2:text <- run-sandboxed 1:text
382   10:@:char <- copy *2:text
383 ]
384 def foo [
385   20:num <- copy 1234
386   {
387   ¦ break
388   ¦ reply 5678
389   }
390 ]
391 # product tracked
392 +mem: storing 50 in location 11
393 
394 :(scenario "run_interactive_returns_text")
395 def main [
396   # try to interactively add 2 and 2
397   1:text <- new [
398   ¦ x:text <- new [a]
399   ¦ y:text <- new [b]
400   ¦ z:text <- append x:text, y:text
401   ]
402   2:text <- run-sandboxed 1:text
403   10:@:char <- copy *2:text
404 ]
405 # output contains "ab"
406 +mem: storing 97 in location 11
407 +mem: storing 98 in location 12
408 
409 :(scenario "run_interactive_returns_errors")
410 def main [
411   # run a command that generates an error
412   1:text <- new [x:num <- copy 34
413 get x:num, foo:offset]
414   2:text, 3:text <- run-sandboxed 1:text
415   10:@:char <- copy *3:text
416 ]
417 # error should be "unknown element foo in container number"
418 +mem: storing 117 in location 11
419 +mem: storing 110 in location 12
420 +mem: storing 107 in location 13
421 +mem: storing 110 in location 14
422 # ...
423 
424 :(scenario run_interactive_with_comment)
425 def main [
426   # 2 instructions, with a comment after the first
427   1:&:@:num <- new [a:num <- copy 0  # abc
428 b:num <- copy 0
429 ]
430   2:text, 3:text <- run-sandboxed 1:text
431 ]
432 # no errors
433 +mem: storing 0 in location 3
434 
435 :(after "Running One Instruction")
436 if (Track_most_recent_products && SIZE(Current_routine->calls) == Call_depth_to_track_most_recent_products_at
437   ¦ && !current_instruction().is_label
438   ¦ && current_instruction().name != "$stop-tracking-products") {
439   Most_recent_products = "";
440 }
441 :(before "End Running One Instruction")
442 if (Track_most_recent_products && SIZE(Current_routine->calls) == Call_depth_to_track_most_recent_products_at) {
443   Most_recent_products = track_most_recent_products(current_instruction(), products);
444 }
445 :(code)
446 string track_most_recent_products(const instruction& instruction, const vector<vector<double> >& products) {
447   ostringstream out;
448   for (int i = 0; i < SIZE(products); ++i) {
449   ¦ // A sandbox can print a string result, but only if it is actually saved
450   ¦ // to a variable in the sandbox, because otherwise the results are
451   ¦ // reclaimed before the sandbox sees them. So you get these interactions
452   ¦ // in the sandbox:
453   ¦ //
454   ¦ //    new [abc]
455   ¦ //    => <address>
456   ¦ //
457   ¦ //    x:text <- new [abc]
458   ¦ //    => abc
459   ¦ if (i < SIZE(instruction.products)) {
460   ¦ ¦ if (is_mu_text(instruction.products.at(i))) {
461   ¦ ¦ ¦ if (!scalar(products.at(i))) continue;  // error handled elsewhere
462   ¦ ¦ ¦ out << read_mu_text(products.at(i).at(0)) << '\n';
463   ¦ ¦ ¦ continue;
464   ¦ ¦ }
465   ¦ }
466   ¦ for (int j = 0; j < SIZE(products.at(i)); ++j)
467   ¦ ¦ out << no_scientific(products.at(i).at(j)) << ' ';
468   ¦ out << '\n';
469   }
470   return out.str();
471 }
472 
473 :(code)
474 string strip_comments(string in) {
475   ostringstream result;
476   for (int i = 0; i < SIZE(in); ++i) {
477   ¦ if (in.at(i) != '#') {
478   ¦ ¦ result << in.at(i);
479   ¦ }
480   ¦ else {
481   ¦ ¦ while (i+1 < SIZE(in) && in.at(i+1) != '\n')
482   ¦ ¦ ¦ ++i;
483   ¦ }
484   }
485   return result.str();
486 }
487 
488 int stringified_value_of_location(int address) {
489   // convert to string
490   ostringstream out;
491   out << no_scientific(get_or_insert(Memory, address));
492   return new_mu_text(out.str());
493 }
494 
495 int trace_error_contents() {
496   if (!Trace_stream) return 0;
497   ostringstream out;
498   for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
499   ¦ if (p->label != "error") continue;
500   ¦ out << p->contents;
501   ¦ if (*--p->contents.end() != '\n') out << '\n';
502   }
503   string result = out.str();
504   truncate(result);
505   if (result.empty()) return 0;
506   return new_mu_text(result);
507 }
508 
509 int trace_app_contents() {
510   if (!Trace_stream) return 0;
511   ostringstream out;
512   for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
513   ¦ if (p->depth != App_depth) continue;
514   ¦ out << p->contents;
515   ¦ if (*--p->contents.end() != '\n') out << '\n';
516   }
517   string result = out.str();
518   if (result.empty()) return 0;
519   truncate(result);
520   return new_mu_text(result);
521 }
522 
523 void truncate(string& x) {
524   if (SIZE(x) > 1024) {
525   ¦ x.erase(1024);
526   ¦ *x.rbegin() = '\n';
527   ¦ *++x.rbegin() = '.';
528   ¦ *++++x.rbegin() = '.';
529   }
530 }
531 
532 //: simpler version of run-sandboxed: doesn't do any running, just loads
533 //: recipes and reports errors.
534 
535 :(before "End Primitive Recipe Declarations")
536 RELOAD,
537 :(before "End Primitive Recipe Numbers")
538 put(Recipe_ordinal, "reload", RELOAD);
539 :(before "End Primitive Recipe Checks")
540 case RELOAD: {
541   if (SIZE(inst.ingredients) != 1) {
542   ¦ raise << maybe(get(Recipe, r).name) << "'reload' requires exactly one ingredient, but got '" << inst.original_string << "'\n" << end();
543   ¦ break;
544   }
545   if (!is_mu_text(inst.ingredients.at(0))) {
546   ¦ raise << maybe(get(Recipe, r).name) << "first ingredient of 'reload' should be a string, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
547   ¦ break;
548   }
549   break;
550 }
551 :(before "End Primitive Recipe Implementations")
552 case RELOAD: {
553   restore_non_recipe_snapshots();
554   string code = read_mu_text(ingredients.at(0).at(0));
555   run_code_begin(/*should_stash_snapshots*/false);
556   routine* save_current_routine = Current_routine;
557   Current_routine = NULL;
558   Sandbox_mode = true;
559   vector<recipe_ordinal> recipes_reloaded = load(code);
560   transform_all();
561   Trace_stream->newline();  // flush trace
562   Sandbox_mode = false;
563   Current_routine = save_current_routine;
564   products.resize(1);
565   products.at(0).push_back(trace_error_contents());
566   run_code_end();  // wait until we're done with the trace contents
567   break;
568 }
569 
570 :(scenario reload_continues_past_error)
571 def main [
572   local-scope
573   x:text <- new [recipe foo [
574   get 1234:num, foo:offset
575 ]]
576   reload x
577   1:num/raw <- copy 34
578 ]
579 +mem: storing 34 in location 1
580 
581 :(scenario reload_can_repeatedly_load_container_definitions)
582 # define a container and try to create it (merge requires knowing container size)
583 def main [
584   local-scope
585   x:text <- new [
586   ¦ container foo [
587   ¦ ¦ x:num
588   ¦ ¦ y:num
589   ¦ ]
590   ¦ recipe bar [
591   ¦ ¦ local-scope
592   ¦ ¦ x:foo <- merge 34, 35
593   ¦ ]
594   ]
595   # save warning addresses in locations of type 'number' to avoid spurious changes to them due to 'abandon'
596   1:num/raw <- reload x
597   2:num/raw <- reload x
598 ]
599 # no errors on either load
600 +mem: storing 0 in location 1
601 +mem: storing 0 in location 2