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