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