1 //: Clean syntax to manipulate and check the screen in scenarios.
  2 //: Instructions 'assume-screen' and 'screen-should-contain' implicitly create
  3 //: a variable called 'screen' that is accessible to later instructions in the
  4 //: scenario. 'screen-should-contain' can check unicode characters in the fake
  5 //: screen
  6 
  7 //: first make sure we don't mangle these functions in other transforms
  8 :(before "End initialize_transform_rewrite_literal_string_to_text()")
  9 recipes_taking_literal_strings.insert("screen-should-contain");
 10 recipes_taking_literal_strings.insert("screen-should-contain-in-color");
 11 
 12 :(scenarios run_mu_scenario)
 13 :(scenario screen_in_scenario)
 14 scenario screen-in-scenario [
 15   local-scope
 16   assume-screen 5/width, 3/height
 17   run [
 18   ¦ a:char <- copy 97/a
 19   ¦ screen:&:screen <- print screen:&:screen, a
 20   ]
 21   screen-should-contain [
 22   #  01234
 23   ¦ .a    .
 24   ¦ .     .
 25   ¦ .     .
 26   ]
 27 ]
 28 # checks are inside scenario
 29 
 30 :(scenario screen_in_scenario_unicode)
 31 # screen-should-contain can check unicode characters in the fake screen
 32 scenario screen-in-scenario-unicode [
 33   local-scope
 34   assume-screen 5/width, 3/height
 35   run [
 36   ¦ lambda:char <- copy 955/greek-small-lambda
 37   ¦ screen:&:screen <- print screen:&:screen, lambda
 38   ¦ a:char <- copy 97/a
 39   ¦ screen:&:screen <- print screen:&:screen, a
 40   ]
 41   screen-should-contain [
 42   #  01234
 43   ¦ .λa   .
 44   ¦ .     .
 45   ¦ .     .
 46   ]
 47 ]
 48 # checks are inside scenario
 49 
 50 :(scenario screen_in_scenario_color)
 51 scenario screen-in-scenario-color [
 52   local-scope
 53   assume-screen 5/width, 3/height
 54   run [
 55   ¦ lambda:char <- copy 955/greek-small-lambda
 56   ¦ screen:&:screen <- print screen:&:screen, lambda, 1/red
 57   ¦ a:char <- copy 97/a
 58   ¦ screen:&:screen <- print screen:&:screen, a, 7/white
 59   ]
 60   # screen-should-contain shows everything
 61   screen-should-contain [
 62   #  01234
 63   ¦ .λa   .
 64   ¦ .     .
 65   ¦ .     .
 66   ]
 67   # screen-should-contain-in-color filters out everything except the given
 68   # color, all you see is the 'a' in white.
 69   screen-should-contain-in-color 7/white, [
 70   #  01234
 71   ¦ . a   .
 72   ¦ .     .
 73   ¦ .     .
 74   ]
 75   # ..and the λ in red.
 76   screen-should-contain-in-color 1/red, [
 77   #  01234
 78   ¦ .λ    .
 79   ¦ .     .
 80   ¦ .     .
 81   ]
 82 ]
 83 # checks are inside scenario
 84 
 85 :(scenario screen_in_scenario_error)
 86 % Scenario_testing_scenario = true;
 87 % Hide_errors = true;
 88 scenario screen-in-scenario-error [
 89   local-scope
 90   assume-screen 5/width, 3/height
 91   run [
 92   ¦ a:char <- copy 97/a
 93   ¦ screen:&:screen <- print screen:&:screen, a
 94   ]
 95   screen-should-contain [
 96   #  01234
 97   ¦ .b    .
 98   ¦ .     .
 99   ¦ .     .
100   ]
101 ]
102 +error: F - screen-in-scenario-error: expected screen location (0, 0) to contain 98 ('b') instead of 97 ('a')
103 
104 :(scenario screen_in_scenario_color_error)
105 % Scenario_testing_scenario = true;
106 % Hide_errors = true;
107 # screen-should-contain can check unicode characters in the fake screen
108 scenario screen-in-scenario-color-error [
109   local-scope
110   assume-screen 5/width, 3/height
111   run [
112   ¦ a:char <- copy 97/a
113   ¦ screen:&:screen <- print screen:&:screen, a, 1/red
114   ]
115   screen-should-contain-in-color 2/green, [
116   #  01234
117   ¦ .a    .
118   ¦ .     .
119   ¦ .     .
120   ]
121 ]
122 +error: F - screen-in-scenario-color-error: expected screen location (0, 0) to contain 'a' in color 2 instead of 1
123 
124 :(scenarios run)
125 :(scenario convert_names_does_not_fail_when_mixing_special_names_and_numeric_locations)
126 % Scenario_testing_scenario = true;
127 def main [
128   screen:num <- copy 1:num
129 ]
130 -error: mixing variable names and numeric addresses in main
131 $error: 0
132 :(scenarios run_mu_scenario)
133 
134 //: It's easier to implement assume-screen and other similar scenario-only
135 //: primitives if they always write to a fixed location. So we'll assign a
136 //: single fixed location for the per-scenario screen, keyboard, file system,
137 //: etc. Carve space for these fixed locations out of the reserved-for-test
138 //: locations.
139 
140 :(before "End Globals")
141 extern const int Max_variables_in_scenarios = Reserved_for_tests-100;
142 int Next_predefined_global_for_scenarios = Max_variables_in_scenarios;
143 :(before "End Reset")
144 assert(Next_predefined_global_for_scenarios < Reserved_for_tests);
145 
146 :(before "End Globals")
147 // Scenario Globals.
148 extern const int SCREEN = Next_predefined_global_for_scenarios++;
149 // End Scenario Globals.
150 
151 //: give 'screen' a fixed location in scenarios
152 :(before "End Special Scenario Variable Names(r)")
153 Name[r]["screen"] = SCREEN;
154 //: make 'screen' always a raw location in scenarios
155 :(before "End is_special_name Special-cases")
156 if (s == "screen") return true;
157 
158 :(before "End Rewrite Instruction(curr, recipe result)")
159 // rewrite 'assume-screen width, height' to
160 // 'screen:&:screen <- new-fake-screen width, height'
161 if (curr.name == "assume-screen") {
162   curr.name = "new-fake-screen";
163   if (!curr.products.empty()) {
164   ¦ raise << result.name << ": 'assume-screen' has no products\n" << end();
165   }
166   else if (!starts_with(result.name, "scenario_")) {
167   ¦ raise << result.name << ": 'assume-screen' can't be called here, only in scenarios\n" << end();
168   }
169   else {
170   ¦ assert(curr.products.empty());
171   ¦ curr.products.push_back(reagent("screen:&:screen/raw"));
172   ¦ curr.products.at(0).set_value(SCREEN);
173   }
174 }
175 
176 :(scenario assume_screen_shows_up_in_errors)
177 % Hide_errors = true;
178 scenario assume-screen-shows-up-in-errors [
179   assume-screen width, 5
180 ]
181 +error: assume-screen-shows-up-in-errors: missing type for 'width' in 'assume-screen width, 5'
182 
183 //: screen-should-contain is a regular instruction
184 :(before "End Primitive Recipe Declarations")
185 SCREEN_SHOULD_CONTAIN,
186 :(before "End Primitive Recipe Numbers")
187 put(Recipe_ordinal, "screen-should-contain", SCREEN_SHOULD_CONTAIN);
188 :(before "End Primitive Recipe Checks")
189 case SCREEN_SHOULD_CONTAIN: {
190   if (SIZE(inst.ingredients) != 1) {
191   ¦ raise << maybe(get(Recipe, r).name) << "'screen-should-contain' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
192   ¦ break;
193   }
194   if (!is_literal_text(inst.ingredients.at(0))) {
195   ¦ raise << maybe(get(Recipe, r).name) << "first ingredient of 'screen-should-contain' should be a literal string, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
196   ¦ break;
197   }
198   break;
199 }
200 :(before "End Primitive Recipe Implementations")
201 case SCREEN_SHOULD_CONTAIN: {
202   if (!Passed) break;
203   assert(scalar(ingredients.at(0)));
204   check_screen(current_instruction().ingredients.at(0).name, -1);
205   break;
206 }
207 
208 :(before "End Primitive Recipe Declarations")
209 SCREEN_SHOULD_CONTAIN_IN_COLOR,
210 :(before "End Primitive Recipe Numbers")
211 put(Recipe_ordinal, "screen-should-contain-in-color", SCREEN_SHOULD_CONTAIN_IN_COLOR);
212 :(before "End Primitive Recipe Checks")
213 case SCREEN_SHOULD_CONTAIN_IN_COLOR: {
214   if (SIZE(inst.ingredients) != 2) {
215   ¦ raise << maybe(get(Recipe, r).name) << "'screen-should-contain-in-color' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
216   ¦ break;
217   }
218   if (!is_mu_number(inst.ingredients.at(0))) {
219   ¦ raise << maybe(get(Recipe, r).name) << "first ingredient of 'screen-should-contain-in-color' should be a number (color code), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
220   ¦ break;
221   }
222   if (!is_literal_text(inst.ingredients.at(1))) {
223   ¦ raise << maybe(get(Recipe, r).name) << "second ingredient of 'screen-should-contain-in-color' should be a literal string, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
224   ¦ break;
225   }
226   break;
227 }
228 :(before "End Primitive Recipe Implementations")
229 case SCREEN_SHOULD_CONTAIN_IN_COLOR: {
230   if (!Passed) break;
231   assert(scalar(ingredients.at(0)));
232   assert(scalar(ingredients.at(1)));
233   check_screen(current_instruction().ingredients.at(1).name, ingredients.at(0).at(0));
234   break;
235 }
236 
237 :(before "End Types")
238 // scan an array of characters in a unicode-aware, bounds-checked manner
239 struct raw_string_stream {
240   int index;
241   const int max;
242   const char* buf;
243 
244   raw_string_stream(const string&);
245   uint32_t get();  // unicode codepoint
246   uint32_t peek();  // unicode codepoint
247   bool at_end() const;
248   void skip_whitespace_and_comments();
249 };
250 
251 :(code)
252 void check_screen(const string& expected_contents, const int color) {
253   int screen_location = get_or_insert(Memory, SCREEN)+/*skip refcount*/1;
254   int data_offset = find_element_name(get(Type_ordinal, "screen"), "data", "");
255   assert(data_offset >= 0);
256   int screen_data_location = screen_location+data_offset;  // type: address:array:character
257   int screen_data_start = get_or_insert(Memory, screen_data_location) + /*skip refcount*/1;  // type: array:character
258   int width_offset = find_element_name(get(Type_ordinal, "screen"), "num-columns", "");
259   int screen_width = get_or_insert(Memory, screen_location+width_offset);
260   int height_offset = find_element_name(get(Type_ordinal, "screen"), "num-rows", "");
261   int screen_height = get_or_insert(Memory, screen_location+height_offset);
262   raw_string_stream cursor(expected_contents);
263   // todo: too-long expected_contents should fail
264   int top_index_offset = find_element_name(get(Type_ordinal, "screen"), "top-idx", "");
265   int top_index = get_or_insert(Memory, screen_location+top_index_offset);
266   for (int i=0, row=top_index/screen_width;  i < screen_height;  ++i, row=(row+1)%screen_height) {
267   ¦ cursor.skip_whitespace_and_comments();
268   ¦ if (cursor.at_end()) break;
269   ¦ if (cursor.get() != '.') {
270   ¦ ¦ raise << maybe(current_recipe_name()) << "each row of the expected screen should start with a '.'\n" << end();
271   ¦ ¦ if (!Scenario_testing_scenario) Passed = false;
272   ¦ ¦ return;
273   ¦ }
274   ¦ int addr = screen_data_start+/*length*/1+row*screen_width* /*size of screen-cell*/2;
275   ¦ for (int column = 0;  column < screen_width;  ++column, addr+= /*size of screen-cell*/2) {
276   ¦ ¦ const int cell_color_offset = 1;
277   ¦ ¦ uint32_t curr = cursor.get();
278   ¦ ¦ if (get_or_insert(Memory, addr) == 0 && isspace(curr)) continue;
279   ¦ ¦ if (curr == ' ' && color != -1 && color != get_or_insert(Memory, addr+cell_color_offset)) {
280   ¦ ¦ ¦ // filter out other colors
281   ¦ ¦ ¦ continue;
282   ¦ ¦ }
283   ¦ ¦ if (get_or_insert(Memory, addr) != 0 && get_or_insert(Memory, addr) == curr) {
284   ¦ ¦ ¦ if (color == -1 || color == get_or_insert(Memory, addr+cell_color_offset)) continue;
285   ¦ ¦ ¦ // contents match but color is off
286   ¦ ¦ ¦ if (!Hide_errors) cerr << '\n';
287   ¦ ¦ ¦ raise << "F - " << maybe(current_recipe_name()) << "expected screen location (" << row << ", " << column << ") to contain '" << unicode_character_at(addr) << "' in color " << color << " instead of " << no_scientific(get_or_insert(Memory, addr+cell_color_offset)) << "\n" << end();
288   ¦ ¦ ¦ if (!Hide_errors) dump_screen();
289   ¦ ¦ ¦ if (!Scenario_testing_scenario) Passed = false;
290   ¦ ¦ ¦ return;
291   ¦ ¦ }
292 
293   ¦ ¦ // really a mismatch
294   ¦ ¦ // can't print multi-byte unicode characters in errors just yet. not very useful for debugging anyway.
295   ¦ ¦ char expected_pretty[10] = {0};
296   ¦ ¦ if (curr < 256 && !iscntrl(curr)) {
297   ¦ ¦ ¦ // " ('<curr>')"
298   ¦ ¦ ¦ expected_pretty[0] = ' ', expected_pretty[1] = '(', expected_pretty[2] = '\'', expected_pretty[3] = static_cast<unsigned char>(curr), expected_pretty[4] = '\'', expected_pretty[5] = ')', expected_pretty[6] = '\0';
299   ¦ ¦ }
300   ¦ ¦ char actual_pretty[10] = {0};
301   ¦ ¦ if (get_or_insert(Memory, addr) < 256 && !iscntrl(get_or_insert(Memory, addr))) {
302   ¦ ¦ ¦ // " ('<curr>')"
303   ¦ ¦ ¦ actual_pretty[0] = ' ', actual_pretty[1] = '(', actual_pretty[2] = '\'', actual_pretty[3] = static_cast<unsigned char>(get_or_insert(Memory, addr)), actual_pretty[4] = '\'', actual_pretty[5] = ')', actual_pretty[6] = '\0';
304   ¦ ¦ }
305 
306   ¦ ¦ ostringstream color_phrase;
307   ¦ ¦ if (color != -1) color_phrase << " in color " << color;
308   ¦ ¦ if (!Hide_errors) cerr << '\n';
309   ¦ ¦ raise << "F - " << maybe(current_recipe_name()) << "expected screen location (" << row << ", " << column << ") to contain " << curr << expected_pretty << color_phrase.str() << " instead of " << no_scientific(get_or_insert(Memory, addr)) << actual_pretty << '\n' << end();
310   ¦ ¦ if (!Hide_errors) dump_screen();
311   ¦ ¦ if (!Scenario_testing_scenario) Passed = false;
312   ¦ ¦ return;
313   ¦ }
314   ¦ if (cursor.get() != '.') {
315   ¦ ¦ raise << maybe(current_recipe_name()) << "row " << row << " of the expected screen is too long\n" << end();
316   ¦ ¦ if (!Scenario_testing_scenario) Passed = false;
317   ¦ ¦ return;
318   ¦ }
319   }
320   cursor.skip_whitespace_and_comments();
321   if (!cursor.at_end()) {
322   ¦ raise << maybe(current_recipe_name()) << "expected screen has too many rows\n" << end();
323   ¦ Passed = false;
324   }
325 }
326 
327 const char* unicode_character_at(int addr) {
328   int unicode_code_point = static_cast<int>(get_or_insert(Memory, addr));
329   return to_unicode(unicode_code_point);
330 }
331 
332 raw_string_stream::raw_string_stream(const string& backing) :index(0), max(SIZE(backing)), buf(backing.c_str()) {}
333 
334 bool raw_string_stream::at_end() const {
335   if (index >= max) return true;
336   if (tb_utf8_char_length(buf[index]) > max-index) {
337   ¦ raise << "unicode string seems corrupted at index "<< index << " character " << static_cast<int>(buf[index]) << '\n' << end();
338   ¦ return true;
339   }
340   return false;
341 }
342 
343 uint32_t raw_string_stream::get() {
344   assert(index < max);  // caller must check bounds before calling 'get'
345   uint32_t result = 0;
346   int length = tb_utf8_char_to_unicode(&result, &buf[index]);
347   assert(length != TB_EOF);
348   index += length;
349   return result;
350 }
351 
352 uint32_t raw_string_stream::peek() {
353   assert(index < max);  // caller must check bounds before calling 'get'
354   uint32_t result = 0;
355   int length = tb_utf8_char_to_unicode(&result, &buf[index]);
356   assert(length != TB_EOF);
357   return result;
358 }
359 
360 void raw_string_stream::skip_whitespace_and_comments() {
361   while (!at_end()) {
362   ¦ if (isspace(peek())) get();
363   ¦ else if (peek() == '#') {
364   ¦ ¦ // skip comment
365   ¦ ¦ get();
366   ¦ ¦ while (peek() != '\n') get();  // implicitly also handles CRLF
367   ¦ }
368   ¦ else break;
369   }
370 }
371 
372 :(before "End Primitive Recipe Declarations")
373 _DUMP_SCREEN,
374 :(before "End Primitive Recipe Numbers")
375 put(Recipe_ordinal, "$dump-screen", _DUMP_SCREEN);
376 :(before "End Primitive Recipe Checks")
377 case _DUMP_SCREEN: {
378   break;
379 }
380 :(before "End Primitive Recipe Implementations")
381 case _DUMP_SCREEN: {
382   dump_screen();
383   break;
384 }
385 
386 :(code)
387 void dump_screen() {
388   int screen_location = get_or_insert(Memory, SCREEN) + /*skip refcount*/1;
389   int width_offset = find_element_name(get(Type_ordinal, "screen"), "num-columns", "");
390   int screen_width = get_or_insert(Memory, screen_location+width_offset);
391   int height_offset = find_element_name(get(Type_ordinal, "screen"), "num-rows", "");
392   int screen_height = get_or_insert(Memory, screen_location+height_offset);
393   int data_offset = find_element_name(get(Type_ordinal, "screen"), "data", "");
394   assert(data_offset >= 0);
395   int screen_data_location = screen_location+data_offset;  // type: address:array:character
396   int screen_data_start = get_or_insert(Memory, screen_data_location) + /*skip refcount*/1;  // type: array:character
397   assert(get_or_insert(Memory, screen_data_start) == screen_width*screen_height);
398   int top_index_offset = find_element_name(get(Type_ordinal, "screen"), "top-idx", "");
399   int top_index = get_or_insert(Memory, screen_location+top_index_offset);
400   for (int i=0, row=top_index/screen_width;  i < screen_height;  ++i, row=(row+1)%screen_height) {
401   ¦ cerr << '.';
402   ¦ int curr = screen_data_start+/*length*/1+row*screen_width* /*size of screen-cell*/2;
403   ¦ for (int col = 0;  col < screen_width;  ++col) {
404   ¦ ¦ if (get_or_insert(Memory, curr))
405   ¦ ¦ ¦ cerr << to_unicode(static_cast<uint32_t>(get_or_insert(Memory, curr)));
406   ¦ ¦ else
407   ¦ ¦ ¦ cerr << ' ';
408   ¦ ¦ curr += /*size of screen-cell*/2;
409   ¦ }
410   ¦ cerr << ".\n";
411   }
412 }