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