// Includes #include #define SIZE(X) (assert((X).size() < (1LL<<(sizeof(long long int)*8-2))), static_cast((X).size())) #include #include using std::istream; using std::ostream; using std::iostream; using std::cin; using std::cout; using std::cerr; #include #include using std::string; #include #define CHECK_TRACE_CONTENTS(...) check_trace_contents(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__) #include using std::vector; #include using std::list; #include using std::map; #include using std::set; #include #include using std::istream; using std::ostream; using std::cin; using std::cout; using std::cerr; #include #include using std::istringstream; using std::ostringstream; #include using std::ifstream; using std::ofstream; #include"termbox/termbox.h" #define unused __attribute__((unused)) #include using std::pair; #include #include #include #include using std::stack; using std::min; using std::max; using std::abs; #include // End Includes // Types // Mu types encode how the numbers stored in different parts of memory are // interpreted. A location tagged as a 'character' type will interpret the // value 97 as the letter 'a', while a different location of type 'number' // would not. // // Unlike most computers today, mu stores types in a single big table, shared // by all the mu programs on the computer. This is useful in providing a // seamless experience to help understand arbitrary mu programs. typedef long long int type_ordinal; typedef long long int recipe_ordinal; typedef void (*test_fn)(void); struct trace_line { int depth; // optional field just to help browse traces later string label; string contents; trace_line(string l, string c) :depth(0), label(l), contents(c) {} trace_line(int d, string l, string c) :depth(d), label(l), contents(c) {} }; struct end {}; // Recipes are lists of instructions. To perform or 'run' a recipe, the // computer runs its instructions. // Each instruction is either of the form: // product1, product2, product3, ... <- operation ingredient1, ingredient2, ingredient3, ... // or just a single 'label' starting with a non-alphanumeric character // +label // Labels don't do anything, they're just waypoints. // Ingredients and products are a single species -- a reagent. Reagents refer // either to numbers or to locations in memory along with 'type' tags telling // us how to interpret them. They also can contain arbitrary other lists of // properties besides types, but we're getting ahead of ourselves. struct property { vector values; }; // Types can range from a simple type ordinal, to arbitrarily complex trees of // type parameters, like (map (address array character) (list number)) struct type_tree { string name; type_ordinal value; type_tree* left; type_tree* right; ~type_tree(); type_tree(const type_tree& old); // simple: type ordinal explicit type_tree(string name, type_ordinal v) :name(name), value(v), left(NULL), right(NULL) {} // intermediate: list of type ordinals type_tree(string name, type_ordinal v, type_tree* r) :name(name), value(v), left(NULL), right(r) {} // advanced: tree containing type ordinals type_tree(type_tree* l, type_tree* r) :value(0), left(l), right(r) {} }; struct string_tree { string value; string_tree* left; string_tree* right; ~string_tree(); string_tree(const string_tree& old); // simple: flat string explicit string_tree(string v) :value(v), left(NULL), right(NULL) {} // intermediate: list of strings string_tree(string v, string_tree* r) :value(v), left(NULL), right(r) {} // advanced: tree containing strings string_tree(string_tree* l, string_tree* r) :left(l), right(r) {} }; struct reagent { string original_string; string name; type_tree* type; vector > properties; double value; bool initialized; reagent(string s); reagent() :type(NULL), value(0), initialized(false) {} ~reagent(); void clear(); reagent(const reagent& old); reagent& operator=(const reagent& old); void set_value(double v) { value = v; initialized = true; } }; struct instruction { bool is_label; string label; // only if is_label string name; // only if !is_label string old_name; // before our automatic rewrite rules string original_string; recipe_ordinal operation; // get(Recipe_ordinal, name) vector ingredients; // only if !is_label vector products; // only if !is_label mutable bool tangle_done; // End instruction Fields instruction(); void clear(); bool is_empty(); }; struct recipe { string name; vector steps; long long int transformed_until; bool has_header; vector ingredients; vector products; map ingredient_index; string original_name; // End recipe Fields recipe(); }; // You can construct arbitrary new types. New types are either 'containers' // with multiple 'elements' of other types, or 'exclusive containers' containing // one of multiple 'variants'. (These are similar to C structs and unions, // respectively, though exclusive containers implicitly include a tag element // recording which variant they should be interpreted as.) // // For example, storing bank balance and name for an account might require a // container, but if bank accounts may be either for individuals or groups, // with different properties for each, that may require an exclusive container // whose variants are individual-account and joint-account containers. enum kind_of_type { PRIMITIVE, CONTAINER, EXCLUSIVE_CONTAINER }; struct type_info { string name; kind_of_type kind; long long int size; // only if type is not primitive; primitives and addresses have size 1 (except arrays are dynamic) vector elements; map type_ingredient_names; // End type_info Fields type_info() :kind(PRIMITIVE), size(0) {} }; enum primitive_recipes { IDLE = 0, COPY, ADD, SUBTRACT, MULTIPLY, DIVIDE, DIVIDE_WITH_REMAINDER, SHIFT_LEFT, SHIFT_RIGHT, AND_BITS, OR_BITS, XOR_BITS, FLIP_BITS, AND, OR, NOT, JUMP, JUMP_IF, JUMP_UNLESS, EQUAL, GREATER_THAN, LESSER_THAN, GREATER_OR_EQUAL, LESSER_OR_EQUAL, TRACE, STASH, HIDE_ERRORS, SHOW_ERRORS, TRACE_UNTIL, _DUMP_TRACE, _CLEAR_TRACE, _SAVE_TRACE, ASSERT, _PRINT, _EXIT, _SYSTEM, _DUMP_MEMORY, _LOG, GET, GET_ADDRESS, MERGE, _DUMP, _FOO, CREATE_ARRAY, INDEX, INDEX_ADDRESS, LENGTH, MAYBE_CONVERT, NEXT_INGREDIENT, REWIND_INGREDIENTS, INGREDIENT, REPLY, NEW, ALLOCATE, ABANDON, TO_LOCATION_ARRAY, BREAK, BREAK_IF, BREAK_UNLESS, LOOP, LOOP_IF, LOOP_UNLESS, RUN, MEMORY_SHOULD_CONTAIN, TRACE_SHOULD_CONTAIN, TRACE_SHOULD_NOT_CONTAIN, CHECK_TRACE_COUNT_FOR_LABEL, NEXT_INGREDIENT_WITHOUT_TYPECHECKING, CALL, START_RUNNING, ROUTINE_STATE, RESTART, STOP, _DUMP_ROUTINES, LIMIT_TIME, WAIT_FOR_LOCATION, WAIT_FOR_ROUTINE, SWITCH, RANDOM, MAKE_RANDOM_NONDETERMINISTIC, ROUND, HASH, HASH_OLD, OPEN_CONSOLE, CLOSE_CONSOLE, CLEAR_DISPLAY, SYNC_DISPLAY, CLEAR_LINE_ON_DISPLAY, PRINT_CHARACTER_TO_DISPLAY, CURSOR_POSITION_ON_DISPLAY, MOVE_CURSOR_ON_DISPLAY, MOVE_CURSOR_DOWN_ON_DISPLAY, MOVE_CURSOR_UP_ON_DISPLAY, MOVE_CURSOR_RIGHT_ON_DISPLAY, MOVE_CURSOR_LEFT_ON_DISPLAY, DISPLAY_WIDTH, DISPLAY_HEIGHT, HIDE_CURSOR_ON_DISPLAY, SHOW_CURSOR_ON_DISPLAY, HIDE_DISPLAY, SHOW_DISPLAY, WAIT_FOR_SOME_INTERACTION, CHECK_FOR_INTERACTION, INTERACTIONS_LEFT, CLEAR_DISPLAY_FROM, SCREEN_SHOULD_CONTAIN, SCREEN_SHOULD_CONTAIN_IN_COLOR, _DUMP_SCREEN, ASSUME_CONSOLE, REPLACE_IN_CONSOLE, _BROWSE_TRACE, RUN_INTERACTIVE, _START_TRACKING_PRODUCTS, _STOP_TRACKING_PRODUCTS, _MOST_RECENT_PRODUCTS, SAVE_ERRORS_WARNINGS, SAVE_APP_TRACE, _CLEANUP_RUN_INTERACTIVE, RELOAD, RESTORE, SAVE, // End Primitive Recipe Declarations MAX_PRIMITIVE_RECIPES, }; struct no_scientific { double x; explicit no_scientific(double y) :x(y) {} }; typedef void (*transform_fn)(recipe_ordinal); // Book-keeping while running a recipe. // Everytime a recipe runs another, we interrupt it and start running the new // recipe. When that finishes, we continue this one where we left off. // This requires maintaining a 'stack' of interrupted recipes or 'calls'. struct call { recipe_ordinal running_recipe; long long int running_step_index; vector > ingredient_atoms; vector ingredients; long long int next_ingredient_to_process; long long int default_space; // End call Fields call(recipe_ordinal r) { running_recipe = r; running_step_index = 0; next_ingredient_to_process = 0; default_space = 0; // End call Constructor } ~call() { // End call Destructor } }; typedef list call_stack; enum routine_state { RUNNING, COMPLETED, DISCONTINUED, WAITING, // End routine States }; struct routine { call_stack calls; long long int alloc, alloc_max; long long int global_space; enum routine_state state; long long int id; // todo: really should be routine_id, but that's less efficient. long long int parent_index; // only < 0 if there's no parent_index long long int limit; // only if state == WAITING long long int waiting_on_location; int old_value_of_waiting_location; // only if state == WAITING long long int waiting_on_routine; // End routine Fields routine(recipe_ordinal r); bool completed() const; const vector& steps() const; }; struct merge_check_point { reagent container; long long int container_element_index; merge_check_point(const reagent& c, long long int i) :container(c), container_element_index(i) {} }; struct merge_check_state { stack data; }; struct scenario { string name; string to_run; }; // scan an array of characters in a unicode-aware, bounds-checked manner struct raw_string_stream { long long int index; const long long int max; const char* buf; raw_string_stream(const string&); uint32_t get(); // unicode codepoint uint32_t peek(); // unicode codepoint bool at_end() const; void skip_whitespace_and_comments(); }; // End Types // prototypes are auto-generated in the makefile; define your functions in any order #include "function_list" // by convention, files ending with '_list' are auto-generated // from http://stackoverflow.com/questions/152643/idiomatic-c-for-reading-from-a-const-map template typename T::mapped_type& get(T& map, typename T::key_type const& key) { typename T::iterator iter(map.find(key)); assert(iter != map.end()); return iter->second; } template typename T::mapped_type const& get(const T& map, typename T::key_type const& key) { typename T::const_iterator iter(map.find(key)); assert(iter != map.end()); return iter->second; } template typename T::mapped_type const& put(T& map, typename T::key_type const& key, typename T::mapped_type const& value) { map[key] = value; return map[key]; } template bool contains_key(T& map, typename T::key_type const& key) { return map.find(key) != map.end(); } template typename T::mapped_type& get_or_insert(T& map, typename T::key_type const& key) { return map[key]; } bool has_data(istream& in) { return in && !in.eof(); } // Globals const test_fn Tests[] = { }; bool Run_tests = false; bool Passed = true; // set this to false inside any test to indicate failure long Num_failures = 0; #define CHECK(X) \ if (!(X)) { \ ++Num_failures; \ cerr << "\nF - " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): " << #X << '\n'; \ Passed = false; \ return; /* Currently we stop at the very first failure. */ \ } #define CHECK_EQ(X, Y) \ if ((X) != (Y)) { \ ++Num_failures; \ cerr << "\nF - " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): " << #X << " == " << #Y << '\n'; \ cerr << " got " << (X) << '\n'; /* BEWARE: multiple eval */ \ Passed = false; \ return; /* Currently we stop at the very first failure. */ \ } const int Max_depth = 9999; const int Error_depth = 0; // definitely always print the error that caused death const int Warning_depth = 1; const int App_depth = 2; // temporarily where all mu code will trace to const int Initial_callstack_depth = 101; const int Max_callstack_depth = 9989; map Recipe; map Recipe_ordinal; recipe_ordinal Next_recipe_ordinal = 1; // Locations refer to a common 'memory'. Each location can store a number. map Memory; map Type_ordinal; map Type; type_ordinal Next_type_ordinal = 1; const string Ignore(","); // commas are ignored in mu except within [] strings // word boundaries const string Terminators("(){}"); bool Disable_redefine_warnings = false; vector Recently_added_recipes; long long int Reserved_for_tests = 1000; vector Transform; routine* Current_routine = NULL; map Instructions_running; map Locations_read; map Locations_read_by_instruction; ofstream LOG; vector Recently_added_types; long long int foo = -1; long long int Memory_allocated_until = Reserved_for_tests; long long int Initial_memory_per_routine = 100000; map Free_list; map > Name; bool Warn_on_missing_default_space = false; map Surrounding_space; vector Scenarios; set Scenario_names; long long int Num_core_mu_tests = 0; const scenario* Current_scenario = NULL; bool Scenario_testing_scenario = false; map Before_fragments, After_fragments; set Fragments_used; bool Transform_check_insert_fragments_Ran = false; map > Recipe_variants; set Literal_type_names; list resolve_stack; // We'll use large type ordinals to mean "the following type of the variable". const int START_TYPE_INGREDIENTS = 2000; vector Recently_added_shape_shifting_recipes; vector Routines; long long int Current_routine_index = 0; long long int Scheduling_interval = 500; long long int Next_routine_id = 1; long long int Display_row = 0, Display_column = 0; bool Autodisplay = true; // Scenarios may not define default-space, so they should fit within the // initial area of memory reserved for tests. We'll put the predefined // variables available to them at the end of that region. const long long int Max_variables_in_scenarios = Reserved_for_tests-100; long long int Next_predefined_global_for_scenarios = Max_variables_in_scenarios; // Scenario Globals. const long long int SCREEN = Next_predefined_global_for_scenarios++; const long long int CONSOLE = Next_predefined_global_for_scenarios++; // End Scenario Globals. map Key; set Visible; long long int Top_of_screen = 0; long long int Last_printed_row = 0; map Trace_index; // screen row -> trace index bool Track_most_recent_products = false; string Most_recent_products; // End Globals bool Hide_errors = false; bool Hide_warnings = false; struct trace_stream { vector past_lines; // accumulator for current line ostringstream* curr_stream; string curr_label; int curr_depth; int callstack_depth; int collect_depth; ofstream null_stream; // never opens a file, so writes silently fail trace_stream() :curr_stream(NULL), curr_depth(Max_depth), callstack_depth(0), collect_depth(Max_depth) {} ~trace_stream() { if (curr_stream) delete curr_stream; } ostream& stream(string label) { return stream(Max_depth, label); } ostream& stream(int depth, string label) { if (depth > collect_depth) return null_stream; curr_stream = new ostringstream; curr_label = label; curr_depth = depth; return *curr_stream; } // be sure to call this before messing with curr_stream or curr_label void newline() { if (!curr_stream) return; string curr_contents = curr_stream->str(); if (curr_contents.empty()) return; past_lines.push_back(trace_line(curr_depth, trim(curr_label), curr_contents)); // preserve indent in contents if (!Hide_errors && curr_label == "error") cerr << curr_label << ": " << curr_contents << '\n'; else if (!Hide_warnings && curr_label == "warn") cerr << curr_label << ": " << curr_contents << '\n'; delete curr_stream; curr_stream = NULL; curr_label.clear(); curr_depth = Max_depth; } // Useful for debugging. string readable_contents(string label) { // missing label = everything ostringstream output; label = trim(label); for (vector::iterator p = past_lines.begin(); p != past_lines.end(); ++p) if (label.empty() || label == p->label) { output << std::setw(4) << p->depth << ' ' << p->label << ": " << p->contents << '\n'; } return output.str(); } }; trace_stream* Trace_stream = NULL; // Top-level helper. IMPORTANT: can't nest. #define trace(...) !Trace_stream ? cerr /*print nothing*/ : Trace_stream->stream(__VA_ARGS__) #define raise (!Trace_stream ? (tb_shutdown(),cerr) /*do print*/ : Trace_stream->stream(Warning_depth, "warn")) #define raise_error (!Trace_stream ? (tb_shutdown(),cerr) /*do print*/ : Trace_stream->stream(Error_depth, "error")) ostream& operator<<(ostream& os, unused end) { if (Trace_stream && Trace_stream->curr_label == "error" && Current_routine) { Current_routine->state = COMPLETED; } if (Trace_stream) Trace_stream->newline(); return os; } #define CLEAR_TRACE delete Trace_stream, Trace_stream = new trace_stream; #define DUMP(label) if (Trace_stream) cerr << Trace_stream->readable_contents(label); // All scenarios save their traces in the repo, just like code. This gives // future readers more meat when they try to make sense of a new project. static string Trace_dir = ".traces/"; string Trace_file; // Trace_stream is a resource, lease_tracer uses RAII to manage it. struct lease_tracer { lease_tracer() { Trace_stream = new trace_stream; } ~lease_tracer() { if (!Trace_stream) return; // in case tests close Trace_stream if (!Trace_file.empty()) { ofstream fout((Trace_dir+Trace_file).c_str()); fout << Trace_stream->readable_contents(""); fout.close(); } delete Trace_stream, Trace_stream = NULL, Trace_file = ""; } }; #define START_TRACING_UNTIL_END_OF_SCOPE lease_tracer leased_tracer; bool check_trace_contents(string FUNCTION, string FILE, int LINE, string expected) { if (!Trace_stream) return false; vector expected_lines = split(expected, ""); long long int curr_expected_line = 0; while (curr_expected_line < SIZE(expected_lines) && expected_lines.at(curr_expected_line).empty()) ++curr_expected_line; if (curr_expected_line == SIZE(expected_lines)) return true; string label, contents; split_label_contents(expected_lines.at(curr_expected_line), &label, &contents); for (vector::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) { if (label != p->label) continue; if (contents != trim(p->contents)) continue; ++curr_expected_line; while (curr_expected_line < SIZE(expected_lines) && expected_lines.at(curr_expected_line).empty()) ++curr_expected_line; if (curr_expected_line == SIZE(expected_lines)) return true; split_label_contents(expected_lines.at(curr_expected_line), &label, &contents); } ++Num_failures; cerr << "\nF - " << FUNCTION << "(" << FILE << ":" << LINE << "): missing [" << contents << "] in trace:\n"; DUMP(label); Passed = false; return false; } void split_label_contents(const string& s, string* label, string* contents) { static const string delim(": "); size_t pos = s.find(delim); if (pos == string::npos) { *label = ""; *contents = trim(s); } else { *label = trim(s.substr(0, pos)); *contents = trim(s.substr(pos+SIZE(delim))); } } int trace_count(string label) { return trace_count(label, ""); } int trace_count(string label, string line) { long result = 0; for (vector::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) { if (label == p->label) { if (line == "" || trim(line) == trim(p->contents)) ++result; } } return result; } #define CHECK_TRACE_CONTAINS_ERROR() CHECK(trace_count("error") > 0) #define CHECK_TRACE_DOESNT_CONTAIN_ERROR() \ if (trace_count("error") > 0) { \ ++Num_failures; \ cerr << "\nF - " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): unexpected errors\n"; \ DUMP("error"); \ Passed = false; \ return; \ } #define CHECK_TRACE_COUNT(label, count) \ if (trace_count(label) != (count)) { \ ++Num_failures; \ cerr << "\nF - " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): trace_count of " << label << " should be " << count << '\n'; \ cerr << " got " << trace_count(label) << '\n'; /* multiple eval */ \ DUMP(label); \ Passed = false; \ return; /* Currently we stop at the very first failure. */ \ } bool trace_doesnt_contain(string label, string line) { return trace_count(label, line) == 0; } bool trace_doesnt_contain(string expected) { vector tmp = split_first(expected, ": "); return trace_doesnt_contain(tmp.at(0), tmp.at(1)); } #define CHECK_TRACE_DOESNT_CONTAIN(...) CHECK(trace_doesnt_contain(__VA_ARGS__)) vector split(string s, string delim) { vector result; size_t begin=0, end=s.find(delim); while (true) { if (end == string::npos) { result.push_back(string(s, begin, string::npos)); break; } result.push_back(string(s, begin, end-begin)); begin = end+SIZE(delim); end = s.find(delim, begin); } return result; } vector split_first(string s, string delim) { vector result; size_t end=s.find(delim); result.push_back(string(s, 0, end)); if (end != string::npos) result.push_back(string(s, end+SIZE(delim), string::npos)); return result; } string trim(const string& s) { string::const_iterator first = s.begin(); while (first != s.end() && isspace(*first)) ++first; if (first == s.end()) return ""; string::const_iterator last = --s.end(); while (last != s.begin() && isspace(*last)) --last; ++last; return string(first, last); } trace_stream* Save_trace_stream = NULL; string Save_trace_file; vector Save_recently_added_recipes; vector Save_recently_added_shape_shifting_recipes; // End Tracing // hack to ensure most code in this layer comes before anything else int main() { // Begin Transforms // Begin Instruction Inserting/Deleting Transforms Transform.push_back(insert_fragments); // NOT idempotent Transform.push_back(check_insert_fragments); // idempotent Transform.push_back(rewrite_stashes_to_text); // End Instruction Inserting/Deleting Transforms // Begin Instruction Modifying Transforms Transform.push_back(check_header_ingredients); // idempotent Transform.push_back(fill_in_reply_ingredients); // idempotent Transform.push_back(check_or_set_types_by_name); // idempotent // Begin Type Modifying Transforms Transform.push_back(deduce_types_from_header); // idempotent Transform.push_back(check_or_set_invalid_types); // idempotent // End Type Modifying Transforms Transform.push_back(collect_surrounding_spaces); // idempotent Transform.push_back(transform_names); // idempotent Transform.push_back(resolve_ambiguous_calls); // idempotent Transform.push_back(update_instruction_operations); // idempotent Transform.push_back(transform_braces); // idempotent Transform.push_back(transform_labels); // idempotent // End Instruction Modifying Transforms Transform.push_back(check_immutable_ingredients); // idempotent // End Transforms // Begin Checks Transform.push_back(check_instruction); // idempotent Transform.push_back(check_indirect_calls_against_header); // idempotent Transform.push_back(check_calls_against_header); // idempotent Transform.push_back(transform_new_to_allocate); // idempotent Transform.push_back(check_merge_calls); Transform.push_back(check_types_of_reply_instructions); Transform.push_back(check_default_space); // idempotent Transform.push_back(check_reply_instructions_against_header); // idempotent // End Checks setup_types(); setup_recipes(); assert(MAX_PRIMITIVE_RECIPES < 200); // level 0 is primitives; until 199 Next_recipe_ordinal = 200; put(Recipe_ordinal, "main", Next_recipe_ordinal++); put(Recipe_variants, "main", vector()); // since we manually added main to Recipe_ordinal Literal_type_names.insert("number"); Literal_type_names.insert("character"); load_permanently("core.mu"); for (long long int t = 0; t < SIZE(Transform); ++t) { for (map::iterator p = Recipe.begin(); p != Recipe.end(); ++p) { recipe& r = p->second; if (r.steps.empty()) continue; if (r.transformed_until != t-1) continue; if (any_type_ingredient_in_header(/*recipe_ordinal*/p->first)) continue; (*Transform.at(t))(/*recipe_ordinal*/p->first); r.transformed_until = t; } } load( "recipe new-editor [\n" " local-scope\n" " init:address:shared:list:character <- push 97/a, 0\n" "]\n"); cerr << "AAA " << contains_key(Type_ordinal, "_elem") << '\n'; for (long long int t = 0; t < SIZE(Transform); ++t) { cerr << "BBB " << t << ' ' << contains_key(Type_ordinal, "_elem") << '\n'; for (map::iterator p = Recipe.begin(); p != Recipe.end(); ++p) { recipe& r = p->second; if (r.steps.empty()) continue; if (r.transformed_until != t-1) continue; cerr << "CCC " << t << ' ' << p->second.name << ' ' << contains_key(Type_ordinal, "_elem") << '\n'; if (any_type_ingredient_in_header(/*recipe_ordinal*/p->first)) continue; (*Transform.at(t))(/*recipe_ordinal*/p->first); r.transformed_until = t; } } parse_int_reagents(); // do this after all other transforms have run check_container_field_types(); test_replace_type_ingredients_entire(); return 0; } bool is_integer(const string& s) { return s.find_first_not_of("0123456789-") == string::npos && s.find('-', 1) == string::npos && s.find_first_of("0123456789") != string::npos; } long long int to_integer(string n) { char* end = NULL; // safe because string.c_str() is guaranteed to be null-terminated long long int result = strtoll(n.c_str(), &end, /*any base*/0); if (*end != '\0') cerr << "tried to convert " << n << " to number\n"; assert(*end == '\0'); return result; } void test_is_integer() { CHECK(is_integer("1234")); CHECK(is_integer("-1")); CHECK(!is_integer("234.0")); CHECK(is_integer("-567")); CHECK(!is_integer("89-0")); CHECK(!is_integer("-")); CHECK(!is_integer("1e3")); // not supported } void test_trace_check_compares() { trace("test layer") << "foo" << end(); CHECK_TRACE_CONTENTS("test layer: foo"); } void test_trace_check_ignores_other_layers() { trace("test layer 1") << "foo" << end(); trace("test layer 2") << "bar" << end(); CHECK_TRACE_CONTENTS("test layer 1: foo"); CHECK_TRACE_DOESNT_CONTAIN("test layer 2: foo"); } void test_trace_check_ignores_leading_whitespace() { trace("test layer 1") << " foo" << end(); CHECK(trace_count("test layer 1", /*too little whitespace*/"foo") == 1); CHECK(trace_count("test layer 1", /*too much whitespace*/" foo") == 1); } void test_trace_check_ignores_other_lines() { trace("test layer 1") << "foo" << end(); trace("test layer 1") << "bar" << end(); CHECK_TRACE_CONTENTS("test layer 1: foo"); } void test_trace_check_ignores_other_lines2() { trace("test layer 1") << "foo" << end(); trace("test layer 1") << "bar" << end(); CHECK_TRACE_CONTENTS("test layer 1: bar"); } void test_trace_ignores_trailing_whitespace() { trace("test layer 1") << "foo\n" << end(); CHECK_TRACE_CONTENTS("test layer 1: foo"); } void test_trace_ignores_trailing_whitespace2() { trace("test layer 1") << "foo " << end(); CHECK_TRACE_CONTENTS("test layer 1: foo"); } void test_trace_orders_across_layers() { trace("test layer 1") << "foo" << end(); trace("test layer 2") << "bar" << end(); trace("test layer 1") << "qux" << end(); CHECK_TRACE_CONTENTS("test layer 1: footest layer 2: bartest layer 1: qux"); } void test_trace_supports_count() { trace("test layer 1") << "foo" << end(); trace("test layer 1") << "foo" << end(); CHECK_EQ(trace_count("test layer 1", "foo"), 2); } void test_trace_supports_count2() { trace("test layer 1") << "foo" << end(); trace("test layer 1") << "bar" << end(); CHECK_EQ(trace_count("test layer 1"), 2); } void test_trace_count_ignores_trailing_whitespace() { trace("test layer 1") << "foo\n" << end(); CHECK(trace_count("test layer 1", "foo") == 1); } // pending: DUMP tests // pending: readable_contents() adds newline if necessary. // pending: raise also prints to stderr. // pending: raise doesn't print to stderr if Hide_errors is set. // pending: raise doesn't have to be saved if Hide_errors is set, just printed. // pending: raise prints to stderr if Trace_stream is NULL. // pending: raise prints to stderr if Trace_stream is NULL even if Hide_errors is set. // pending: raise << ... die() doesn't die if Hide_errors is set. // can't check trace because trace methods call 'split' void test_split_returns_at_least_one_elem() { vector result = split("", ","); CHECK_EQ(result.size(), 1); CHECK_EQ(result.at(0), ""); } void test_split_returns_entire_input_when_no_delim() { vector result = split("abc", ","); CHECK_EQ(result.size(), 1); CHECK_EQ(result.at(0), "abc"); } void test_split_works() { vector result = split("abc,def", ","); CHECK_EQ(result.size(), 2); CHECK_EQ(result.at(0), "abc"); CHECK_EQ(result.at(1), "def"); } void test_split_works2() { vector result = split("abc,def,ghi", ","); CHECK_EQ(result.size(), 3); CHECK_EQ(result.at(0), "abc"); CHECK_EQ(result.at(1), "def"); CHECK_EQ(result.at(2), "ghi"); } void test_split_handles_multichar_delim() { vector result = split("abc,,def,,ghi", ",,"); CHECK_EQ(result.size(), 3); CHECK_EQ(result.at(0), "abc"); CHECK_EQ(result.at(1), "def"); CHECK_EQ(result.at(2), "ghi"); } void test_trim() { CHECK_EQ(trim(""), ""); CHECK_EQ(trim(" "), ""); CHECK_EQ(trim(" "), ""); CHECK_EQ(trim("a"), "a"); CHECK_EQ(trim(" a"), "a"); CHECK_EQ(trim(" a"), "a"); CHECK_EQ(trim(" ab"), "ab"); CHECK_EQ(trim("a "), "a"); CHECK_EQ(trim("a "), "a"); CHECK_EQ(trim("ab "), "ab"); CHECK_EQ(trim(" a "), "a"); CHECK_EQ(trim(" a "), "a"); CHECK_EQ(trim(" ab "), "ab"); } void setup_types() { Type.clear(); Type_ordinal.clear(); put(Type_ordinal, "literal", 0); Next_type_ordinal = 1; // Mu Types Initialization type_ordinal number = put(Type_ordinal, "number", Next_type_ordinal++); put(Type_ordinal, "location", get(Type_ordinal, "number")); // wildcard type: either a pointer or a scalar get_or_insert(Type, number).name = "number"; type_ordinal address = put(Type_ordinal, "address", Next_type_ordinal++); get_or_insert(Type, address).name = "address"; type_ordinal boolean = put(Type_ordinal, "boolean", Next_type_ordinal++); get_or_insert(Type, boolean).name = "boolean"; type_ordinal character = put(Type_ordinal, "character", Next_type_ordinal++); get_or_insert(Type, character).name = "character"; // Array types are a special modifier to any other type. For example, // array:number or array:address:boolean. type_ordinal array = put(Type_ordinal, "array", Next_type_ordinal++); get_or_insert(Type, array).name = "array"; put(Type_ordinal, "literal-string", 0); put(Type_ordinal, "offset", 0); type_ordinal point = put(Type_ordinal, "point", Next_type_ordinal++); get_or_insert(Type, point).size = 2; get(Type, point).kind = CONTAINER; get(Type, point).name = "point"; get(Type, point).elements.push_back(reagent("x:number")); get(Type, point).elements.push_back(reagent("y:number")); // A more complex container, containing another container as one of its // elements. type_ordinal point_number = put(Type_ordinal, "point-number", Next_type_ordinal++); get_or_insert(Type, point_number).size = 2; get(Type, point_number).kind = CONTAINER; get(Type, point_number).name = "point-number"; get(Type, point_number).elements.push_back(reagent("xy:point")); get(Type, point_number).elements.push_back(reagent("z:number")); { type_ordinal tmp = put(Type_ordinal, "number-or-point", Next_type_ordinal++); get_or_insert(Type, tmp).size = 2; get(Type, tmp).kind = EXCLUSIVE_CONTAINER; get(Type, tmp).name = "number-or-point"; get(Type, tmp).elements.push_back(reagent("i:number")); get(Type, tmp).elements.push_back(reagent("p:point")); } put(Type_ordinal, "variant", 0); type_ordinal shared = put(Type_ordinal, "shared", Next_type_ordinal++); get_or_insert(Type, shared).name = "shared"; put(Type_ordinal, "type", 0); put(Type_ordinal, "label", 0); put(Type_ordinal, "recipe-literal", 0); // 'recipe' variables can store recipe-literal type_ordinal recipe = put(Type_ordinal, "recipe", Next_type_ordinal++); get_or_insert(Type, recipe).name = "recipe"; // End Mu Types Initialization } void teardown_types() { for (map::iterator p = Type.begin(); p != Type.end(); ++p) { for (long long int i = 0; i < SIZE(p->second.elements); ++i) p->second.elements.clear(); } Type_ordinal.clear(); } void setup_recipes() { Recipe.clear(); Recipe_ordinal.clear(); put(Recipe_ordinal, "idle", IDLE); // Primitive Recipe Numbers put(Recipe_ordinal, "copy", COPY); put(Recipe_ordinal, "add", ADD); put(Recipe_ordinal, "subtract", SUBTRACT); put(Recipe_ordinal, "multiply", MULTIPLY); put(Recipe_ordinal, "divide", DIVIDE); put(Recipe_ordinal, "divide-with-remainder", DIVIDE_WITH_REMAINDER); put(Recipe_ordinal, "shift-left", SHIFT_LEFT); put(Recipe_ordinal, "shift-right", SHIFT_RIGHT); put(Recipe_ordinal, "and-bits", AND_BITS); put(Recipe_ordinal, "or-bits", OR_BITS); put(Recipe_ordinal, "xor-bits", XOR_BITS); put(Recipe_ordinal, "flip-bits", FLIP_BITS); put(Recipe_ordinal, "and", AND); put(Recipe_ordinal, "or", OR); put(Recipe_ordinal, "not", NOT); put(Recipe_ordinal, "jump", JUMP); put(Recipe_ordinal, "jump-if", JUMP_IF); put(Recipe_ordinal, "jump-unless", JUMP_UNLESS); put(Recipe_ordinal, "equal", EQUAL); put(Recipe_ordinal, "greater-than", GREATER_THAN); put(Recipe_ordinal, "lesser-than", LESSER_THAN); put(Recipe_ordinal, "greater-or-equal", GREATER_OR_EQUAL); put(Recipe_ordinal, "lesser-or-equal", LESSER_OR_EQUAL); put(Recipe_ordinal, "trace", TRACE); put(Recipe_ordinal, "stash", STASH); put(Recipe_ordinal, "hide-errors", HIDE_ERRORS); put(Recipe_ordinal, "show-errors", SHOW_ERRORS); put(Recipe_ordinal, "trace-until", TRACE_UNTIL); put(Recipe_ordinal, "$dump-trace", _DUMP_TRACE); put(Recipe_ordinal, "$clear-trace", _CLEAR_TRACE); put(Recipe_ordinal, "$save-trace", _SAVE_TRACE); put(Recipe_ordinal, "assert", ASSERT); put(Recipe_ordinal, "$print", _PRINT); put(Recipe_ordinal, "$exit", _EXIT); put(Recipe_ordinal, "$system", _SYSTEM); put(Recipe_ordinal, "$dump-memory", _DUMP_MEMORY); put(Recipe_ordinal, "$log", _LOG); put(Recipe_ordinal, "get", GET); put(Recipe_ordinal, "get-address", GET_ADDRESS); put(Recipe_ordinal, "merge", MERGE); put(Recipe_ordinal, "$dump", _DUMP); put(Recipe_ordinal, "$foo", _FOO); put(Recipe_ordinal, "create-array", CREATE_ARRAY); put(Recipe_ordinal, "index", INDEX); put(Recipe_ordinal, "index-address", INDEX_ADDRESS); put(Recipe_ordinal, "length", LENGTH); put(Recipe_ordinal, "maybe-convert", MAYBE_CONVERT); put(Recipe_ordinal, "next-ingredient", NEXT_INGREDIENT); put(Recipe_ordinal, "rewind-ingredients", REWIND_INGREDIENTS); put(Recipe_ordinal, "ingredient", INGREDIENT); put(Recipe_ordinal, "reply", REPLY); put(Recipe_ordinal, "new", NEW); put(Recipe_ordinal, "allocate", ALLOCATE); put(Recipe_ordinal, "abandon", ABANDON); put(Recipe_ordinal, "to-location-array", TO_LOCATION_ARRAY); put(Recipe_ordinal, "break", BREAK); put(Recipe_ordinal, "break-if", BREAK_IF); put(Recipe_ordinal, "break-unless", BREAK_UNLESS); put(Recipe_ordinal, "loop", LOOP); put(Recipe_ordinal, "loop-if", LOOP_IF); put(Recipe_ordinal, "loop-unless", LOOP_UNLESS); put(Recipe_ordinal, "run", RUN); put(Recipe_ordinal, "memory-should-contain", MEMORY_SHOULD_CONTAIN); put(Recipe_ordinal, "trace-should-contain", TRACE_SHOULD_CONTAIN); put(Recipe_ordinal, "trace-should-not-contain", TRACE_SHOULD_NOT_CONTAIN); put(Recipe_ordinal, "check-trace-count-for-label", CHECK_TRACE_COUNT_FOR_LABEL); put(Recipe_ordinal, "next-ingredient-without-typechecking", NEXT_INGREDIENT_WITHOUT_TYPECHECKING); put(Recipe_ordinal, "call", CALL); put(Recipe_ordinal, "start-running", START_RUNNING); put(Recipe_ordinal, "routine-state", ROUTINE_STATE); put(Recipe_ordinal, "restart", RESTART); put(Recipe_ordinal, "stop", STOP); put(Recipe_ordinal, "$dump-routines", _DUMP_ROUTINES); put(Recipe_ordinal, "limit-time", LIMIT_TIME); put(Recipe_ordinal, "wait-for-location", WAIT_FOR_LOCATION); put(Recipe_ordinal, "wait-for-routine", WAIT_FOR_ROUTINE); put(Recipe_ordinal, "switch", SWITCH); put(Recipe_ordinal, "random", RANDOM); put(Recipe_ordinal, "make-random-nondeterministic", MAKE_RANDOM_NONDETERMINISTIC); put(Recipe_ordinal, "round", ROUND); put(Recipe_ordinal, "hash", HASH); put(Recipe_ordinal, "hash_old", HASH_OLD); put(Recipe_ordinal, "open-console", OPEN_CONSOLE); put(Recipe_ordinal, "close-console", CLOSE_CONSOLE); put(Recipe_ordinal, "clear-display", CLEAR_DISPLAY); put(Recipe_ordinal, "sync-display", SYNC_DISPLAY); put(Recipe_ordinal, "clear-line-on-display", CLEAR_LINE_ON_DISPLAY); put(Recipe_ordinal, "print-character-to-display", PRINT_CHARACTER_TO_DISPLAY); put(Recipe_ordinal, "cursor-position-on-display", CURSOR_POSITION_ON_DISPLAY); put(Recipe_ordinal, "move-cursor-on-display", MOVE_CURSOR_ON_DISPLAY); put(Recipe_ordinal, "move-cursor-down-on-display", MOVE_CURSOR_DOWN_ON_DISPLAY); put(Recipe_ordinal, "move-cursor-up-on-display", MOVE_CURSOR_UP_ON_DISPLAY); put(Recipe_ordinal, "move-cursor-right-on-display", MOVE_CURSOR_RIGHT_ON_DISPLAY); put(Recipe_ordinal, "move-cursor-left-on-display", MOVE_CURSOR_LEFT_ON_DISPLAY); put(Recipe_ordinal, "display-width", DISPLAY_WIDTH); put(Recipe_ordinal, "display-height", DISPLAY_HEIGHT); put(Recipe_ordinal, "hide-cursor-on-display", HIDE_CURSOR_ON_DISPLAY); put(Recipe_ordinal, "show-cursor-on-display", SHOW_CURSOR_ON_DISPLAY); put(Recipe_ordinal, "hide-display", HIDE_DISPLAY); put(Recipe_ordinal, "show-display", SHOW_DISPLAY); put(Recipe_ordinal, "wait-for-some-interaction", WAIT_FOR_SOME_INTERACTION); put(Recipe_ordinal, "check-for-interaction", CHECK_FOR_INTERACTION); put(Recipe_ordinal, "interactions-left?", INTERACTIONS_LEFT); put(Recipe_ordinal, "clear-display-from", CLEAR_DISPLAY_FROM); put(Recipe_ordinal, "screen-should-contain", SCREEN_SHOULD_CONTAIN); put(Recipe_ordinal, "screen-should-contain-in-color", SCREEN_SHOULD_CONTAIN_IN_COLOR); put(Recipe_ordinal, "$dump-screen", _DUMP_SCREEN); put(Recipe_ordinal, "assume-console", ASSUME_CONSOLE); put(Recipe_ordinal, "replace-in-console", REPLACE_IN_CONSOLE); put(Recipe_ordinal, "$browse-trace", _BROWSE_TRACE); put(Recipe_ordinal, "run-interactive", RUN_INTERACTIVE); put(Recipe_ordinal, "$start-tracking-products", _START_TRACKING_PRODUCTS); put(Recipe_ordinal, "$stop-tracking-products", _STOP_TRACKING_PRODUCTS); put(Recipe_ordinal, "$most-recent-products", _MOST_RECENT_PRODUCTS); put(Recipe_ordinal, "save-errors-warnings", SAVE_ERRORS_WARNINGS); put(Recipe_ordinal, "save-app-trace", SAVE_APP_TRACE); put(Recipe_ordinal, "$cleanup-run-interactive", _CLEANUP_RUN_INTERACTIVE); put(Recipe_ordinal, "reload", RELOAD); put(Recipe_ordinal, "restore", RESTORE); put(Recipe_ordinal, "save", SAVE); // End Primitive Recipe Numbers } recipe::recipe() { transformed_until = -1; has_header = false; // End recipe Constructor } instruction::instruction() :is_label(false), operation(IDLE) { tangle_done = false; // End instruction Constructor } void instruction::clear() { is_label=false; label.clear(); name.clear(); old_name.clear(); operation=IDLE; ingredients.clear(); products.clear(); original_string.clear(); } bool instruction::is_empty() { return !is_label && name.empty(); } // Reagents have the form :::...///... reagent::reagent(string s) :original_string(s), type(NULL), value(0), initialized(false) { // Parsing reagent(string s) if (s.at(0) == '{') { assert(properties.empty()); istringstream in(s); in >> std::noskipws; in.get(); // skip '{' name = slurp_key(in); if (name.empty()) { raise_error << "invalid reagent " << s << " without a name\n"; return; } if (name == "}") { raise_error << "invalid empty reagent " << s << '\n'; return; } { string_tree* value = new string_tree(next_word(in)); value = parse_string_tree(value); // End Parsing Reagent Type Property(value) type = new_type_tree(value); delete value; } while (has_data(in)) { string key = slurp_key(in); if (key.empty()) continue; if (key == "}") continue; string_tree* value = new string_tree(next_word(in)); value = parse_string_tree(value); // End Parsing Reagent Property(value) properties.push_back(pair(key, value)); } return; } if (is_noninteger(s)) { name = s; type = new type_tree("literal-fractional-number", 0); set_value(to_double(s)); return; } if (s.at(0) == '[') { assert(*s.rbegin() == ']'); // delete [] delimiters s.erase(0, 1); strip_last(s); name = s; type = new type_tree("literal-string", 0); return; } istringstream in(s); in >> std::noskipws; // name and type istringstream first_row(slurp_until(in, '/')); first_row >> std::noskipws; name = slurp_until(first_row, ':'); string_tree* type_names = parse_property_list(first_row); type = new_type_tree(type_names); delete type_names; // special cases if (is_integer(name) && type == NULL) type = new type_tree("literal", get(Type_ordinal, "literal")); if (name == "_" && type == NULL) type = new type_tree("literal", get(Type_ordinal, "literal")); // other properties while (has_data(in)) { istringstream row(slurp_until(in, '/')); row >> std::noskipws; string key = slurp_until(row, ':'); string_tree* value = parse_property_list(row); properties.push_back(pair(key, value)); } { while (!name.empty() && name.at(0) == '*') { name.erase(0, 1); properties.push_back(pair("lookup", NULL)); } if (name.empty()) raise_error << "illegal name " << original_string << '\n' << end(); } // End Parsing reagent } string_tree* parse_property_list(istream& in) { skip_whitespace_but_not_newline(in); if (!has_data(in)) return NULL; string_tree* result = new string_tree(slurp_until(in, ':')); result->right = parse_property_list(in); return result; } // TODO: delete type_tree* new_type_tree(const string_tree* properties) { if (!properties) return NULL; type_tree* result = new type_tree("", 0); if (!properties->value.empty()) { const string& type_name = result->name = properties->value; if (contains_key(Type_ordinal, type_name)) result->value = get(Type_ordinal, type_name); else if (is_integer(type_name)) // sometimes types will contain non-type tags, like numbers for the size of an array result->value = 0; else if (properties->value != "->") // used in recipe types result->value = -1; // should never happen; will trigger errors later } result->left = new_type_tree(properties->left); result->right = new_type_tree(properties->right); return result; } reagent::reagent(const reagent& old) { original_string = old.original_string; name = old.name; value = old.value; initialized = old.initialized; properties.clear(); for (long long int i = 0; i < SIZE(old.properties); ++i) { properties.push_back(pair(old.properties.at(i).first, old.properties.at(i).second ? new string_tree(*old.properties.at(i).second) : NULL)); } type = old.type ? new type_tree(*old.type) : NULL; } type_tree::type_tree(const type_tree& old) { name = old.name; value = old.value; left = old.left ? new type_tree(*old.left) : NULL; right = old.right ? new type_tree(*old.right) : NULL; } string_tree::string_tree(const string_tree& old) { // :value(old.value) { value = old.value; left = old.left ? new string_tree(*old.left) : NULL; right = old.right ? new string_tree(*old.right) : NULL; } reagent& reagent::operator=(const reagent& old) { original_string = old.original_string; properties.clear(); for (long long int i = 0; i < SIZE(old.properties); ++i) properties.push_back(pair(old.properties.at(i).first, old.properties.at(i).second ? new string_tree(*old.properties.at(i).second) : NULL)); name = old.name; value = old.value; initialized = old.initialized; type = old.type ? new type_tree(*old.type) : NULL; return *this; } reagent::~reagent() { clear(); } void reagent::clear() { for (long long int i = 0; i < SIZE(properties); ++i) { if (properties.at(i).second) { delete properties.at(i).second; properties.at(i).second = NULL; } } delete type; type = NULL; } type_tree::~type_tree() { delete left; delete right; } string_tree::~string_tree() { delete left; delete right; } string slurp_until(istream& in, char delim) { ostringstream out; char c; while (in >> c) { if (c == delim) { // drop the delim break; } out << c; } return out.str(); } bool has_property(reagent x, string name) { for (long long int i = 0; i < SIZE(x.properties); ++i) { if (x.properties.at(i).first == name) return true; } return false; } string_tree* property(const reagent& r, const string& name) { for (long long int p = 0; p != SIZE(r.properties); ++p) { if (r.properties.at(p).first == name) return r.properties.at(p).second; } return NULL; } void skip_whitespace_but_not_newline(istream& in) { while (true) { if (!has_data(in)) break; else if (in.peek() == '\n') break; else if (isspace(in.peek())) in.get(); else if (Ignore.find(in.peek()) != string::npos) in.get(); else break; } } void dump_memory() { for (map::iterator p = Memory.begin(); p != Memory.end(); ++p) { cout << p->first << ": " << no_scientific(p->second) << '\n'; } } string to_string(const recipe& r) { ostringstream out; out << "recipe " << r.name << " [\n"; for (long long int i = 0; i < SIZE(r.steps); ++i) out << " " << to_string(r.steps.at(i)) << '\n'; out << "]\n"; return out.str(); } string debug_string(const recipe& x) { ostringstream out; out << "- recipe " << x.name << '\n'; // Begin debug_string(recipe x) out << "ingredients:\n"; for (long long int i = 0; i < SIZE(x.ingredients); ++i) out << " " << debug_string(x.ingredients.at(i)) << '\n'; out << "products:\n"; for (long long int i = 0; i < SIZE(x.products); ++i) out << " " << debug_string(x.products.at(i)) << '\n'; for (long long int index = 0; index < SIZE(x.steps); ++index) { const instruction& inst = x.steps.at(index); out << "inst: " << to_string(inst) << '\n'; out << " ingredients\n"; for (long long int i = 0; i < SIZE(inst.ingredients); ++i) out << " " << debug_string(inst.ingredients.at(i)) << '\n'; out << " products\n"; for (long long int i = 0; i < SIZE(inst.products); ++i) out << " " << debug_string(inst.products.at(i)) << '\n'; } return out.str(); } string to_string(const instruction& inst) { if (inst.is_label) return inst.label; ostringstream out; for (long long int i = 0; i < SIZE(inst.products); ++i) { if (i > 0) out << ", "; out << inst.products.at(i).original_string; } if (!inst.products.empty()) out << " <- "; out << inst.name << ' '; for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (i > 0) out << ", "; out << inst.ingredients.at(i).original_string; } return out.str(); } string to_string(const reagent& r) { if (is_literal_string(r)) return emit_literal_string(r.name); ostringstream out; out << r.name << ": " << names_to_string(r.type); if (!r.properties.empty()) { out << ", {"; for (long long int i = 0; i < SIZE(r.properties); ++i) { if (i > 0) out << ", "; out << "\"" << r.properties.at(i).first << "\": " << to_string(r.properties.at(i).second); } out << "}"; } return out.str(); } string debug_string(const reagent& x) { ostringstream out; out << x.name << ": " << x.value << ' ' << to_string(x.type) << " -- " << to_string(x); return out.str(); } string to_string(const string_tree* property) { if (!property) return "()"; ostringstream out; if (!property->left && !property->right) // abbreviate a single-node tree to just its contents out << '"' << property->value << '"'; else dump(property, out); return out.str(); } void dump(const string_tree* x, ostream& out) { if (!x->left && !x->right) { out << x->value; return; } out << '('; for (const string_tree* curr = x; curr; curr = curr->right) { if (curr != x) out << ' '; if (curr->left) dump(curr->left, out); else out << '"' << curr->value << '"'; } out << ')'; } string to_string(const type_tree* type) { // abbreviate a single-node tree to just its contents if (!type) return "NULLNULLNULL"; // should never happen ostringstream out; dump(type, out); return out.str(); } void dump(const type_tree* x, ostream& out) { if (!x->left && !x->right) { out << x->value; return; } out << '('; for (const type_tree* curr = x; curr; curr = curr->right) { if (curr != x) out << ' '; if (curr->left) dump(curr->left, out); else dump(curr->value, out); } out << ')'; } void dump(type_ordinal type, ostream& out) { if (contains_key(Type, type)) out << get(Type, type).name; else out << "?" << type; } string names_to_string(const type_tree* type) { // abbreviate a single-node tree to just its contents if (!type) return "()"; // should never happen ostringstream out; dump_names(type, out); return out.str(); } void dump_names(const type_tree* type, ostream& out) { if (!type->left && !type->right) { out << '"' << type->name << '"'; return; } out << '('; for (const type_tree* curr = type; curr; curr = curr->right) { if (curr != type) out << ' '; if (curr->left) dump_names(curr->left, out); else out << '"' << curr->name << '"'; } out << ')'; } string names_to_string_without_quotes(const type_tree* type) { // abbreviate a single-node tree to just its contents if (!type) return "NULLNULLNULL"; // should never happen ostringstream out; dump_names_without_quotes(type, out); return out.str(); } void dump_names_without_quotes(const type_tree* type, ostream& out) { if (!type->left && !type->right) { out << type->name; return; } out << '('; for (const type_tree* curr = type; curr; curr = curr->right) { if (curr != type) out << ' '; if (curr->left) dump_names_without_quotes(curr->left, out); else out << curr->name; } out << ')'; } ostream& operator<<(ostream& os, no_scientific x) { if (!isfinite(x.x)) { // Infinity or NaN os << x.x; return os; } ostringstream tmp; tmp << std::fixed << x.x; os << trim_floating_point(tmp.str()); return os; } string trim_floating_point(const string& in) { if (in.empty()) return ""; long long int len = SIZE(in); while (len > 1) { if (in.at(len-1) != '0') break; --len; } if (in.at(len-1) == '.') --len; return in.substr(0, len); } void test_trim_floating_point() { CHECK_EQ("", trim_floating_point("")); CHECK_EQ("0", trim_floating_point("000000000")); CHECK_EQ("1.5", trim_floating_point("1.5000")); CHECK_EQ("1.000001", trim_floating_point("1.000001")); CHECK_EQ("23", trim_floating_point("23.000000")); CHECK_EQ("23", trim_floating_point("23.0")); CHECK_EQ("23", trim_floating_point("23.")); CHECK_EQ("23", trim_floating_point("23")); CHECK_EQ("3", trim_floating_point("3.000000")); CHECK_EQ("3", trim_floating_point("3.0")); CHECK_EQ("3", trim_floating_point("3.")); CHECK_EQ("3", trim_floating_point("3")); } void test_first_recipe() { Trace_file = "first_recipe"; load("recipe main [\n 1:number <- copy 23\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: product: 1: \"number\""); } vector load(string form) { istringstream in(form); in >> std::noskipws; return load(in); } vector load(istream& in) { in >> std::noskipws; vector result; while (has_data(in)) { skip_whitespace_and_comments(in); if (!has_data(in)) break; string command = next_word(in); // Command Handlers if (command == "recipe") { result.push_back(slurp_recipe(in)); } else if (command == "recipe!") { Disable_redefine_warnings = true; result.push_back(slurp_recipe(in)); Disable_redefine_warnings = false; } else if (command == "container") { insert_container(command, CONTAINER, in); } else if (command == "exclusive-container") { insert_container(command, EXCLUSIVE_CONTAINER, in); } else if (command == "scenario") { Scenarios.push_back(parse_scenario(in)); } else if (command == "before") { string label = next_word(in); recipe tmp; slurp_body(in, tmp); if (is_waypoint(label)) Before_fragments[label].steps.insert(Before_fragments[label].steps.end(), tmp.steps.begin(), tmp.steps.end()); else raise_error << "can't tangle before label " << label << '\n' << end(); } else if (command == "after") { string label = next_word(in); recipe tmp; slurp_body(in, tmp); if (is_waypoint(label)) After_fragments[label].steps.insert(After_fragments[label].steps.begin(), tmp.steps.begin(), tmp.steps.end()); else raise_error << "can't tangle after label " << label << '\n' << end(); } // End Command Handlers else { raise_error << "unknown top-level command: " << command << '\n' << end(); } } return result; } long long int slurp_recipe(istream& in) { recipe result; result.name = next_word(in); result.original_name = result.name; // End Load Recipe Name skip_whitespace_but_not_newline(in); if (in.peek() != '[') { trace(9999, "parse") << "recipe has a header; parsing" << end(); load_recipe_header(in, result); } // End Recipe Refinements if (result.name.empty()) raise_error << "empty result.name\n" << end(); trace(9991, "parse") << "--- defining " << result.name << end(); if (!contains_key(Recipe_ordinal, result.name)) put(Recipe_ordinal, result.name, Next_recipe_ordinal++); if (Recipe.find(get(Recipe_ordinal, result.name)) != Recipe.end()) { trace(9991, "parse") << "already exists" << end(); if (warn_on_redefine(result.name)) raise << "redefining recipe " << result.name << "\n" << end(); Recipe.erase(get(Recipe_ordinal, result.name)); } slurp_body(in, result); if (!result.has_header) { result.has_header = true; for (long long int i = 0; i < SIZE(result.steps); ++i) { const instruction& inst = result.steps.at(i); if ((inst.name == "reply" && !inst.ingredients.empty()) || inst.name == "next-ingredient" || inst.name == "ingredient" || inst.name == "rewind-ingredients") { result.has_header = false; break; } } } if (result.has_header) { trace(9999, "parse") << "recipe " << result.name << " has a header" << end(); } // End Recipe Body(result) put(Recipe, get(Recipe_ordinal, result.name), result); // track added recipes because we may need to undo them in tests; see below Recently_added_recipes.push_back(get(Recipe_ordinal, result.name)); return get(Recipe_ordinal, result.name); } void slurp_body(istream& in, recipe& result) { in >> std::noskipws; skip_whitespace_but_not_newline(in); if (in.get() != '[') raise_error << "recipe body must begin with '['\n" << end(); skip_whitespace_and_comments(in); // permit trailing comment after '[' instruction curr; while (next_instruction(in, &curr)) { // rewrite `reply-if a, b, c, ...` to // ``` // jump-unless a, 1:offset // reply b, c, ... // ``` if (curr.name == "reply-if") { if (curr.products.empty()) { curr.operation = get(Recipe_ordinal, "jump-unless"); curr.name = "jump-unless"; vector results; copy(++curr.ingredients.begin(), curr.ingredients.end(), inserter(results, results.end())); curr.ingredients.resize(1); curr.ingredients.push_back(reagent("1:offset")); result.steps.push_back(curr); curr.clear(); curr.operation = get(Recipe_ordinal, "reply"); curr.name = "reply"; curr.ingredients.swap(results); } else { raise_error << "'reply-if' never yields any products\n" << end(); } } // rewrite `reply-unless a, b, c, ...` to // ``` // jump-if a, 1:offset // reply b, c, ... // ``` if (curr.name == "reply-unless") { if (curr.products.empty()) { curr.operation = get(Recipe_ordinal, "jump-if"); curr.name = "jump-if"; vector results; copy(++curr.ingredients.begin(), curr.ingredients.end(), inserter(results, results.end())); curr.ingredients.resize(1); curr.ingredients.push_back(reagent("1:offset")); result.steps.push_back(curr); curr.clear(); curr.operation = get(Recipe_ordinal, "reply"); curr.name = "reply"; curr.ingredients.swap(results); } else { raise_error << "'reply-unless' never yields any products\n" << end(); } } // rewrite `new-default-space` to // `default-space:address:shared:array:location <- new location:type, number-of-locals:literal` // where N is Name[recipe][""] if (curr.name == "new-default-space") { rewrite_default_space_instruction(curr); } if (curr.name == "local-scope") { rewrite_default_space_instruction(curr); } if (curr.name == "load-ingredients") { curr.clear(); recipe_ordinal op = get(Recipe_ordinal, "next-ingredient-without-typechecking"); for (long long int i = 0; i < SIZE(result.ingredients); ++i) { curr.operation = op; curr.name = "next-ingredient-without-typechecking"; curr.products.push_back(result.ingredients.at(i)); result.steps.push_back(curr); curr.clear(); } } // rewrite `assume-screen width, height` to // `screen:address:shared:screen <- new-fake-screen width, height` if (curr.name == "assume-screen") { curr.name = "new-fake-screen"; assert(curr.products.empty()); curr.products.push_back(reagent("screen:address:shared:screen")); curr.products.at(0).set_value(SCREEN); } // End Rewrite Instruction(curr, recipe result) trace(9992, "load") << "after rewriting: " << to_string(curr) << end(); if (!curr.is_empty()) { curr.original_string = to_string(curr); result.steps.push_back(curr); } } } bool next_instruction(istream& in, instruction* curr) { curr->clear(); skip_whitespace_and_comments(in); if (!has_data(in)) { raise_error << "0: unbalanced '[' for recipe\n" << end(); return false; } vector words; while (has_data(in) && in.peek() != '\n') { skip_whitespace_but_not_newline(in); if (!has_data(in)) { raise_error << "1: unbalanced '[' for recipe\n" << end(); return false; } string word = next_word(in); words.push_back(word); skip_whitespace_but_not_newline(in); } skip_whitespace_and_comments(in); if (SIZE(words) == 1 && words.at(0) == "]") return false; // end of recipe if (SIZE(words) == 1 && !isalnum(words.at(0).at(0)) && words.at(0).at(0) != '$') { curr->is_label = true; curr->label = words.at(0); trace(9993, "parse") << "label: " << curr->label << end(); if (!has_data(in)) { raise_error << "7: unbalanced '[' for recipe\n" << end(); return false; } return true; } vector::iterator p = words.begin(); if (find(words.begin(), words.end(), "<-") != words.end()) { for (; *p != "<-"; ++p) curr->products.push_back(reagent(*p)); ++p; // skip <- } if (p == words.end()) { raise_error << "instruction prematurely ended with '<-'\n" << end(); return false; } curr->old_name = curr->name = *p; p++; // curr->operation will be set in a later layer for (; p != words.end(); ++p) curr->ingredients.push_back(reagent(*p)); trace(9993, "parse") << "instruction: " << curr->name << end(); trace(9993, "parse") << " number of ingredients: " << SIZE(curr->ingredients) << end(); for (vector::iterator p = curr->ingredients.begin(); p != curr->ingredients.end(); ++p) trace(9993, "parse") << " ingredient: " << to_string(*p) << end(); for (vector::iterator p = curr->products.begin(); p != curr->products.end(); ++p) trace(9993, "parse") << " product: " << to_string(*p) << end(); if (!has_data(in)) { raise_error << "9: unbalanced '[' for recipe\n" << end(); return false; } return true; } string next_word(istream& in) { skip_whitespace_but_not_newline(in); if (in.peek() == '[') { string result = slurp_quoted(in); skip_whitespace_and_comments_but_not_newline(in); return result; } if (in.peek() == '(') return slurp_balanced_bracket(in); // treat curlies mostly like parens, but don't mess up labels if (start_of_dilated_reagent(in)) return slurp_balanced_bracket(in); // End next_word Special-cases ostringstream out; slurp_word(in, out); skip_whitespace_and_comments_but_not_newline(in); return out.str(); } void slurp_word(istream& in, ostream& out) { char c; if (has_data(in) && Terminators.find(in.peek()) != string::npos) { in >> c; out << c; return; } while (in >> c) { if (isspace(c) || Terminators.find(c) != string::npos || Ignore.find(c) != string::npos) { in.putback(c); break; } out << c; } } void skip_whitespace_and_comments(istream& in) { while (true) { if (!has_data(in)) break; if (isspace(in.peek())) in.get(); else if (Ignore.find(in.peek()) != string::npos) in.get(); else if (in.peek() == '#') skip_comment(in); else break; } } // confusing; move to the next line only to skip a comment, but never otherwise void skip_whitespace_and_comments_but_not_newline(istream& in) { while (true) { if (!has_data(in)) break; if (in.peek() == '\n') break; if (isspace(in.peek())) in.get(); else if (Ignore.find(in.peek()) != string::npos) in.get(); else if (in.peek() == '#') skip_comment(in); else break; } } void skip_comment(istream& in) { if (has_data(in) && in.peek() == '#') { in.get(); while (has_data(in) && in.peek() != '\n') in.get(); } } bool warn_on_redefine(const string& recipe_name) { if (recipe_name.find("scenario-") == 0) return true; if (Disable_redefine_warnings) return false; return true; } // for debugging void show_rest_of_stream(istream& in) { cerr << '^'; char c; while (in >> c) cerr << c; cerr << "$\n"; exit(0); } void clear_recently_added_recipes() { for (long long int i = 0; i < SIZE(Recently_added_recipes); ++i) { if (Recently_added_recipes.at(i) >= Reserved_for_tests // don't renumber existing recipes, like 'interactive' && contains_key(Recipe, Recently_added_recipes.at(i))) // in case previous test had duplicate definitions Recipe_ordinal.erase(get(Recipe, Recently_added_recipes.at(i)).name); Recipe.erase(Recently_added_recipes.at(i)); } for (map >::iterator p = Recipe_variants.begin(); p != Recipe_variants.end(); ++p) { for (long long int i = 0; i < SIZE(p->second); ++i) { if (find(Recently_added_recipes.begin(), Recently_added_recipes.end(), p->second.at(i)) != Recently_added_recipes.end()) p->second.at(i) = -1; // just leave a ghost } } // Clear Other State For Recently_added_recipes for (long long int i = 0; i < SIZE(Recently_added_recipes); ++i) { Name.erase(Recently_added_recipes.at(i)); } Recently_added_recipes.clear(); } void test_parse_comment_outside_recipe() { Trace_file = "parse_comment_outside_recipe"; load("# this comment will be dropped by the tangler, so we need a dummy recipe to stop that\nrecipe f1 [ ]\n# this comment will go through to 'load'\nrecipe main [\n 1:number <- copy 23\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: product: 1: \"number\""); } void test_parse_comment_amongst_instruction() { Trace_file = "parse_comment_amongst_instruction"; load("recipe main [\n # comment\n 1:number <- copy 23\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: product: 1: \"number\""); } void test_parse_comment_amongst_instruction_2() { Trace_file = "parse_comment_amongst_instruction_2"; load("recipe main [\n # comment\n 1:number <- copy 23\n # comment\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: product: 1: \"number\""); } void test_parse_comment_amongst_instruction_3() { Trace_file = "parse_comment_amongst_instruction_3"; load("recipe main [\n 1:number <- copy 23\n # comment\n 2:number <- copy 23\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: product: 1: \"number\"parse: instruction: copyparse: ingredient: 23: \"literal\"parse: product: 2: \"number\""); } void test_parse_comment_after_instruction() { Trace_file = "parse_comment_after_instruction"; load("recipe main [\n 1:number <- copy 23 # comment\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: product: 1: \"number\""); } void test_parse_label() { Trace_file = "parse_label"; load("recipe main [\n +foo\n]\n"); CHECK_TRACE_CONTENTS("parse: label: +foo"); } void test_parse_dollar_as_recipe_name() { Trace_file = "parse_dollar_as_recipe_name"; load("recipe main [\n $foo\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: $foo"); } void test_parse_multiple_properties() { Trace_file = "parse_multiple_properties"; load("recipe main [\n 1:number <- copy 23/foo:bar:baz\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\", {\"foo\": (\"bar\" \"baz\")}parse: product: 1: \"number\""); } void test_parse_multiple_products() { Trace_file = "parse_multiple_products"; load("recipe main [\n 1:number, 2:number <- copy 23\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: product: 1: \"number\"parse: product: 2: \"number\""); } void test_parse_multiple_ingredients() { Trace_file = "parse_multiple_ingredients"; load("recipe main [\n 1:number, 2:number <- copy 23, 4:number\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: ingredient: 4: \"number\"parse: product: 1: \"number\"parse: product: 2: \"number\""); } void test_parse_multiple_types() { Trace_file = "parse_multiple_types"; load("recipe main [\n 1:number, 2:address:number <- copy 23, 4:number\n]\n"); CHECK_TRACE_CONTENTS("parse: instruction: copyparse: ingredient: 23: \"literal\"parse: ingredient: 4: \"number\"parse: product: 1: \"number\"parse: product: 2: (\"address\" \"number\")"); } void test_parse_properties() { Trace_file = "parse_properties"; load("recipe main [\n 1:address:number/lookup <- copy 23\n]\n"); CHECK_TRACE_CONTENTS("parse: product: 1: (\"address\" \"number\"), {\"lookup\": ()}"); } void test_parse_comment_terminated_by_eof() { Trace_file = "parse_comment_terminated_by_eof"; load("recipe main [\n" " a:number <- copy 34\n" "]\n" "# abc"); // no newline after comment cerr << "."; // termination = success } void test_warn_on_redefine() { Trace_file = "warn_on_redefine"; Hide_warnings = true; load("recipe main [\n 1:number <- copy 23\n]\nrecipe main [\n 1:number <- copy 24\n]\n"); CHECK_TRACE_CONTENTS("warn: redefining recipe main"); } void test_redefine_without_warning() { Trace_file = "redefine_without_warning"; Hide_warnings = true; load("recipe main [\n 1:number <- copy 23\n]\nrecipe! main [\n 1:number <- copy 24\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("warn: redefining recipe main"); CHECK_TRACE_COUNT("warn", 0); } void transform_all() { trace(9990, "transform") << "=== transform_all()" << end(); for (long long int t = 0; t < SIZE(Transform); ++t) { //? cerr << "transform " << t << '\n'; for (map::iterator p = Recipe.begin(); p != Recipe.end(); ++p) { recipe& r = p->second; if (r.steps.empty()) continue; if (r.transformed_until != t-1) continue; if (any_type_ingredient_in_header(/*recipe_ordinal*/p->first)) continue; // End Transform Checks (*Transform.at(t))(/*recipe_ordinal*/p->first); r.transformed_until = t; } } //? cerr << "wrapping up transform\n"; parse_int_reagents(); // do this after all other transforms have run check_container_field_types(); // End Transform All } void parse_int_reagents() { trace(9991, "transform") << "--- parsing any uninitialized reagents as integers" << end(); //? cerr << "--- parsing any uninitialized reagents as integers" << '\n'; for (map::iterator p = Recipe.begin(); p != Recipe.end(); ++p) { recipe& r = p->second; if (r.steps.empty()) continue; for (long long int index = 0; index < SIZE(r.steps); ++index) { instruction& inst = r.steps.at(index); for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { populate_value(inst.ingredients.at(i)); } for (long long int i = 0; i < SIZE(inst.products); ++i) { populate_value(inst.products.at(i)); } } } } void populate_value(reagent& r) { if (r.initialized) return; // End Reagent-parsing Exceptions if (!is_integer(r.name)) return; r.set_value(to_integer(r.name)); } void update_instruction_operations(recipe_ordinal r) { trace(9991, "transform") << "--- compute instruction operations for recipe " << get(Recipe, r).name << end(); recipe& caller = get(Recipe, r); //? cerr << "--- compute instruction operations for recipe " << caller.name << '\n'; for (long long int index = 0; index < SIZE(caller.steps); ++index) { instruction& inst = caller.steps.at(index); if (inst.is_label) continue; if (!contains_key(Recipe_ordinal, inst.name)) { raise_error << maybe(caller.name) << "instruction " << inst.name << " has no recipe\n" << end(); return; } inst.operation = get(Recipe_ordinal, inst.name); if (contains_key(Recipe, inst.operation) && inst.operation >= MAX_PRIMITIVE_RECIPES && any_type_ingredient_in_header(inst.operation)) { raise_error << maybe(caller.name) << "instruction " << inst.name << " has no valid specialization\n" << end(); return; } // End Instruction Operation Checks } } // hook to suppress inserting recipe name into errors and warnings (for later layers) string maybe(string s) { if (s == "interactive") return ""; return s + ": "; } // temporarily suppress run void transform(string form) { load(form); transform_all(); } void test_string_literal() { Trace_file = "string_literal"; load("recipe main [\n 1:address:array:character <- copy [abc def] # copy can't really take a string\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: \"abc def\": \"literal-string\""); } void test_string_literal_with_colons() { Trace_file = "string_literal_with_colons"; load("recipe main [\n 1:address:array:character <- copy [abc:def/ghi]\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: \"abc:def/ghi\": \"literal-string\""); } string slurp_quoted(istream& in) { ostringstream out; assert(has_data(in)); assert(in.peek() == '['); out << static_cast(in.get()); // slurp the '[' if (is_code_string(in, out)) slurp_quoted_comment_aware(in, out); else slurp_quoted_comment_oblivious(in, out); return out.str(); } // A string is a code string if it contains a newline before any non-whitespace // todo: support comments before the newline. But that gets messy. bool is_code_string(istream& in, ostream& out) { while (has_data(in)) { char c = in.get(); if (!isspace(c)) { in.putback(c); return false; } out << c; if (c == '\n') { return true; } } return false; } // Read a regular string. Regular strings can only contain other regular // strings. void slurp_quoted_comment_oblivious(istream& in, ostream& out) { int brace_depth = 1; while (has_data(in)) { char c = in.get(); if (c == '\\') { out << static_cast(in.get()); continue; } out << c; if (c == '[') ++brace_depth; if (c == ']') --brace_depth; if (brace_depth == 0) break; } if (!has_data(in) && brace_depth > 0) { raise_error << "unbalanced '['\n" << end(); out.clear(); } } // Read a code string. Code strings can contain either code or regular strings. void slurp_quoted_comment_aware(istream& in, ostream& out) { char c; while (in >> c) { if (c == '\\') { out << static_cast(in.get()); continue; } if (c == '#') { out << c; while (has_data(in) && in.peek() != '\n') out << static_cast(in.get()); continue; } if (c == '[') { in.putback(c); // recurse out << slurp_quoted(in); continue; } out << c; if (c == ']') return; } raise_error << "unbalanced '['\n" << end(); out.clear(); } bool is_literal_string(const reagent& x) { return x.type && x.type->name == "literal-string"; } string emit_literal_string(string name) { size_t pos = 0; while (pos != string::npos) pos = replace(name, "\n", "\\n", pos); return '"'+name+"\": \"literal-string\""; } size_t replace(string& str, const string& from, const string& to, size_t n) { size_t result = str.find(from, n); if (result != string::npos) str.replace(result, from.length(), to); return result; } void strip_last(string& s) { if (!s.empty()) s.erase(SIZE(s)-1); } void test_string_literal_nested() { Trace_file = "string_literal_nested"; load("recipe main [\n 1:address:array:character <- copy [abc [def]]\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: \"abc [def]\": \"literal-string\""); } void test_string_literal_escaped() { Trace_file = "string_literal_escaped"; load("recipe main [\n 1:address:array:character <- copy [abc \\[def]\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: \"abc [def\": \"literal-string\""); } void test_string_literal_escaped_comment_aware() { Trace_file = "string_literal_escaped_comment_aware"; load("recipe main [\n 1:address:array:character <- copy [\nabc \\\\\\[def]\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: \"\\nabc \\[def\": \"literal-string\""); } void test_string_literal_and_comment() { Trace_file = "string_literal_and_comment"; load("recipe main [\n 1:address:array:character <- copy [abc] # comment\n]\n"); CHECK_TRACE_CONTENTS("parse: --- defining mainparse: instruction: copyparse: number of ingredients: 1parse: ingredient: \"abc\": \"literal-string\"parse: product: 1: (\"address\" \"array\" \"character\")"); } void test_string_literal_escapes_newlines_in_trace() { Trace_file = "string_literal_escapes_newlines_in_trace"; load("recipe main [\n copy [abc\ndef]\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: \"abc\\ndef\": \"literal-string\""); } void test_string_literal_can_skip_past_comments() { Trace_file = "string_literal_can_skip_past_comments"; load("recipe main [\n copy [\n # ']' inside comment\n bar\n ]\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: \"\\n # ']' inside comment\\n bar\\n \": \"literal-string\""); } void test_string_literal_empty() { Trace_file = "string_literal_empty"; load("recipe main [\n copy []\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: \"\": \"literal-string\""); } void test_noninteger_literal() { Trace_file = "noninteger_literal"; load("recipe main [\n 1:number <- copy 3.14159\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: 3.14159: \"literal-fractional-number\""); } bool is_noninteger(const string& s) { return s.find_first_not_of("0123456789-.") == string::npos && s.find_first_of ("0123456789-") != string::npos && std::count(s.begin(), s.end(), '.') == 1; } double to_double(string n) { char* end = NULL; // safe because string.c_str() is guaranteed to be null-terminated double result = strtod(n.c_str(), &end); assert(*end == '\0'); return result; } void test_is_noninteger() { CHECK(!is_noninteger("1234")); CHECK(!is_noninteger("1a2")); CHECK(is_noninteger("234.0")); CHECK(!is_noninteger("...")); CHECK(!is_noninteger(".")); CHECK(is_noninteger("2.")); CHECK(is_noninteger(".2")); } void test_copy_literal() { Trace_file = "copy_literal"; run("recipe main [\n 1:number <- copy 23\n]\n"); CHECK_TRACE_CONTENTS("run: 1:number <- copy 23mem: storing 23 in location 1"); } void test_copy() { Trace_file = "copy"; run("recipe main [\n 1:number <- copy 23\n 2:number <- copy 1:number\n]\n"); CHECK_TRACE_CONTENTS("run: 2:number <- copy 1:numbermem: location 1 is 23mem: storing 23 in location 2"); } void test_copy_multiple() { Trace_file = "copy_multiple"; run("recipe main [\n 1:number, 2:number <- copy 23, 24\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 23 in location 1mem: storing 24 in location 2"); } void run(recipe_ordinal r) { run(new routine(r)); } void run_current_routine(long long int time_slice) { // curly on a separate line, because later layers will modify header long long int ninstrs = 0; while (Current_routine->state == RUNNING && ninstrs < time_slice) { // when we reach the end of one call, we may reach the end of the one below // it, and the one below that, and so on while (current_step_index() >= SIZE(Current_routine->steps())) { // Falling Through End Of Recipe try_reclaim_locals(); if (Trace_stream) { trace(9999, "trace") << "fall-through: exiting " << current_recipe_name() << "; decrementing callstack depth from " << Trace_stream->callstack_depth << end(); --Trace_stream->callstack_depth; assert(Trace_stream->callstack_depth >= 0); } Current_routine->calls.pop_front(); if (Current_routine->calls.empty()) return; // Complete Call Fallthrough // todo: fail if no products returned ++current_step_index(); } // Running One Instruction ninstrs++; if (Current_routine->calls.front().running_step_index == 0 && any_type_ingredient_in_header(Current_routine->calls.front().running_recipe)) { //? DUMP(""); raise_error << "ran into unspecialized shape-shifting recipe " << current_recipe_name() << '\n' << end(); } if (current_instruction().is_label) { ++current_step_index(); continue; } trace(Initial_callstack_depth + Trace_stream->callstack_depth, "run") << to_string(current_instruction()) << end(); if (get_or_insert(Memory, 0) != 0) { raise_error << "something wrote to location 0; this should never happen\n" << end(); put(Memory, 0, 0); } // read all ingredients from memory, each potentially spanning multiple locations vector > ingredients; if (should_copy_ingredients()) { for (long long int i = 0; i < SIZE(current_instruction().ingredients); ++i) ingredients.push_back(read_memory(current_instruction().ingredients.at(i))); } // instructions below will write to 'products' vector > products; switch (current_instruction().operation) { // Primitive Recipe Implementations case COPY: { copy(ingredients.begin(), ingredients.end(), inserter(products, products.begin())); break; } case ADD: { double result = 0; for (long long int i = 0; i < SIZE(ingredients); ++i) { result += ingredients.at(i).at(0); } products.resize(1); products.at(0).push_back(result); break; } case SUBTRACT: { double result = ingredients.at(0).at(0); for (long long int i = 1; i < SIZE(ingredients); ++i) result -= ingredients.at(i).at(0); products.resize(1); products.at(0).push_back(result); break; } case MULTIPLY: { double result = 1; for (long long int i = 0; i < SIZE(ingredients); ++i) { result *= ingredients.at(i).at(0); } products.resize(1); products.at(0).push_back(result); break; } case DIVIDE: { double result = ingredients.at(0).at(0); for (long long int i = 1; i < SIZE(ingredients); ++i) result /= ingredients.at(i).at(0); products.resize(1); products.at(0).push_back(result); break; } case DIVIDE_WITH_REMAINDER: { products.resize(2); long long int a = static_cast(ingredients.at(0).at(0)); long long int b = static_cast(ingredients.at(1).at(0)); if (b == 0) { raise_error << maybe(current_recipe_name()) << "divide by zero in '" << to_string(current_instruction()) << "'\n" << end(); products.resize(2); products.at(0).push_back(0); products.at(1).push_back(0); break; } long long int quotient = a / b; long long int remainder = a % b; // very large integers will lose precision products.at(0).push_back(quotient); products.at(1).push_back(remainder); break; } case SHIFT_LEFT: { // ingredients must be integers long long int a = static_cast(ingredients.at(0).at(0)); long long int b = static_cast(ingredients.at(1).at(0)); products.resize(1); if (b < 0) { raise_error << maybe(current_recipe_name()) << "second ingredient can't be negative in '" << to_string(current_instruction()) << "'\n" << end(); products.at(0).push_back(0); break; } products.at(0).push_back(a<(ingredients.at(0).at(0)); long long int b = static_cast(ingredients.at(1).at(0)); products.resize(1); if (b < 0) { raise_error << maybe(current_recipe_name()) << "second ingredient can't be negative in '" << to_string(current_instruction()) << "'\n" << end(); products.at(0).push_back(0); break; } products.at(0).push_back(a>>b); break; } case AND_BITS: { // ingredients must be integers long long int a = static_cast(ingredients.at(0).at(0)); long long int b = static_cast(ingredients.at(1).at(0)); products.resize(1); products.at(0).push_back(a&b); break; } case OR_BITS: { // ingredients must be integers long long int a = static_cast(ingredients.at(0).at(0)); long long int b = static_cast(ingredients.at(1).at(0)); products.resize(1); products.at(0).push_back(a|b); break; } case XOR_BITS: { // ingredients must be integers long long int a = static_cast(ingredients.at(0).at(0)); long long int b = static_cast(ingredients.at(1).at(0)); products.resize(1); products.at(0).push_back(a^b); break; } case FLIP_BITS: { // ingredient must be integer long long int a = static_cast(ingredients.at(0).at(0)); products.resize(1); products.at(0).push_back(~a); break; } case AND: { bool result = true; for (long long int i = 0; i < SIZE(ingredients); ++i) result = result && ingredients.at(i).at(0); products.resize(1); products.at(0).push_back(result); break; } case OR: { bool result = false; for (long long int i = 0; i < SIZE(ingredients); ++i) result = result || ingredients.at(i).at(0); products.resize(1); products.at(0).push_back(result); break; } case NOT: { products.resize(SIZE(ingredients)); for (long long int i = 0; i < SIZE(ingredients); ++i) { products.at(i).push_back(!ingredients.at(i).at(0)); } break; } case JUMP: { assert(current_instruction().ingredients.at(0).initialized); current_step_index() += ingredients.at(0).at(0)+1; trace(9998, "run") << "jumping to instruction " << current_step_index() << end(); continue; // skip rest of this instruction } case JUMP_IF: { assert(current_instruction().ingredients.at(1).initialized); if (!ingredients.at(0).at(0)) { trace(9998, "run") << "jump-if fell through" << end(); break; } current_step_index() += ingredients.at(1).at(0)+1; trace(9998, "run") << "jumping to instruction " << current_step_index() << end(); continue; // skip rest of this instruction } case JUMP_UNLESS: { assert(current_instruction().ingredients.at(1).initialized); if (ingredients.at(0).at(0)) { trace(9998, "run") << "jump-unless fell through" << end(); break; } current_step_index() += ingredients.at(1).at(0)+1; trace(9998, "run") << "jumping to instruction " << current_step_index() << end(); continue; // skip rest of this instruction } case EQUAL: { vector& exemplar = ingredients.at(0); bool result = true; for (long long int i = 1; i < SIZE(ingredients); ++i) { if (!equal(ingredients.at(i).begin(), ingredients.at(i).end(), exemplar.begin())) { result = false; break; } } products.resize(1); products.at(0).push_back(result); break; } case GREATER_THAN: { bool result = true; for (long long int i = /**/1; i < SIZE(ingredients); ++i) { if (ingredients.at(i-1).at(0) <= ingredients.at(i).at(0)) { result = false; } } products.resize(1); products.at(0).push_back(result); break; } case LESSER_THAN: { bool result = true; for (long long int i = /**/1; i < SIZE(ingredients); ++i) { if (ingredients.at(i-1).at(0) >= ingredients.at(i).at(0)) { result = false; } } products.resize(1); products.at(0).push_back(result); break; } case GREATER_OR_EQUAL: { bool result = true; for (long long int i = /**/1; i < SIZE(ingredients); ++i) { if (ingredients.at(i-1).at(0) < ingredients.at(i).at(0)) { result = false; } } products.resize(1); products.at(0).push_back(result); break; } case LESSER_OR_EQUAL: { bool result = true; for (long long int i = /**/1; i < SIZE(ingredients); ++i) { if (ingredients.at(i-1).at(0) > ingredients.at(i).at(0)) { result = false; } } products.resize(1); products.at(0).push_back(result); break; } case TRACE: { long long int depth = ingredients.at(0).at(0); string label = current_instruction().ingredients.at(1).name; ostringstream out; for (long long int i = 2; i < SIZE(current_instruction().ingredients); ++i) { out << print_mu(current_instruction().ingredients.at(i), ingredients.at(i)); } trace(depth, label) << out.str() << end(); break; } case STASH: { ostringstream out; for (long long int i = 0; i < SIZE(current_instruction().ingredients); ++i) { out << print_mu(current_instruction().ingredients.at(i), ingredients.at(i)); } trace(2, "app") << out.str() << end(); break; } case HIDE_ERRORS: { Hide_errors = true; Hide_warnings = true; break; } case SHOW_ERRORS: { Hide_errors = false; Hide_warnings = false; break; } case TRACE_UNTIL: { if (Trace_stream) { Trace_stream->collect_depth = ingredients.at(0).at(0); } break; } case _DUMP_TRACE: { if (ingredients.empty()) { DUMP(""); } else { DUMP(current_instruction().ingredients.at(0).name); } break; } case _CLEAR_TRACE: { if (Trace_stream) Trace_stream->past_lines.clear(); break; } case _SAVE_TRACE: { if (!Trace_file.empty()) { ofstream fout((Trace_dir+Trace_file).c_str()); fout << Trace_stream->readable_contents(""); fout.close(); } break; } case ASSERT: { if (!ingredients.at(0).at(0)) { raise_error << current_instruction().ingredients.at(1).name << '\n' << end(); } break; } case _PRINT: { for (long long int i = 0; i < SIZE(ingredients); ++i) { if (is_literal(current_instruction().ingredients.at(i))) { trace(9998, "run") << "$print: " << current_instruction().ingredients.at(i).name << end(); if (has_property(current_instruction().ingredients.at(i), "newline")) cout << '\n'; else cout << current_instruction().ingredients.at(i).name; } else { for (long long int j = 0; j < SIZE(ingredients.at(i)); ++j) { trace(9998, "run") << "$print: " << ingredients.at(i).at(j) << end(); if (j > 0) cout << " "; cout << no_scientific(ingredients.at(i).at(j)); } } } cout.flush(); break; } case _EXIT: { exit(0); break; } case _SYSTEM: { if (Current_scenario) break; int status = system(current_instruction().ingredients.at(0).name.c_str()); products.resize(1); products.at(0).push_back(status); break; } case _DUMP_MEMORY: { dump_memory(); break; } case _LOG: { ostringstream out; for (long long int i = 0; i < SIZE(current_instruction().ingredients); ++i) { out << print_mu(current_instruction().ingredients.at(i), ingredients.at(i)); } LOG << out.str() << '\n'; break; } case GET: { reagent base = current_instruction().ingredients.at(0); // Update GET base in Run canonize(base); long long int base_address = base.value; if (base_address == 0) { raise_error << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_string(current_instruction()) << "'\n" << end(); break; } type_ordinal base_type = base.type->value; long long int offset = ingredients.at(1).at(0); if (offset < 0 || offset >= SIZE(get(Type, base_type).elements)) break; // copied from Check above long long int src = base_address; for (long long int i = 0; i < offset; ++i) { const type_tree* type = get(Type, base_type).elements.at(i).type; if (type->value >= START_TYPE_INGREDIENTS) { long long int size = size_of_type_ingredient(type, base.type->right); if (!size) raise_error << "illegal field type '" << to_string(type) << "' seems to be missing a type ingredient or three\n" << end(); src += size; continue; } // End GET field Cases src += size_of(element_type(base, i)); } trace(9998, "run") << "address to copy is " << src << end(); reagent tmp = element_type(base, offset); tmp.properties.push_back(pair("raw", NULL)); tmp.set_value(src); trace(9998, "run") << "its type is " << to_string(tmp.type) << end(); products.push_back(read_memory(tmp)); break; } case GET_ADDRESS: { reagent base = current_instruction().ingredients.at(0); // Update GET_ADDRESS base in Run canonize(base); long long int base_address = base.value; if (base_address == 0) { raise_error << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_string(current_instruction()) << "'\n" << end(); break; } type_ordinal base_type = base.type->value; long long int offset = ingredients.at(1).at(0); if (offset < 0 || offset >= SIZE(get(Type, base_type).elements)) break; // copied from Check above long long int result = base_address; for (long long int i = 0; i < offset; ++i) { const type_tree* type = get(Type, base_type).elements.at(i).type; if (type->value >= START_TYPE_INGREDIENTS) { long long int size = size_of_type_ingredient(type, base.type->right); if (!size) raise_error << "illegal type '" << to_string(type) << "' seems to be missing a type ingredient or three\n" << end(); result += size; continue; } // End GET_ADDRESS field Cases result += size_of(element_type(base, i)); } trace(9998, "run") << "address to copy is " << result << end(); products.resize(1); products.at(0).push_back(result); break; } case MERGE: { products.resize(1); for (long long int i = 0; i < SIZE(ingredients); ++i) for (long long int j = 0; j < SIZE(ingredients.at(i)); ++j) products.at(0).push_back(ingredients.at(i).at(j)); break; } case _DUMP: { reagent after_canonize = current_instruction().ingredients.at(0); canonize(after_canonize); cerr << maybe(current_recipe_name()) << current_instruction().ingredients.at(0).name << ' ' << no_scientific(current_instruction().ingredients.at(0).value) << " => " << no_scientific(after_canonize.value) << " => " << no_scientific(get_or_insert(Memory, after_canonize.value)) << '\n'; break; } case _FOO: { if (current_instruction().ingredients.empty()) { if (foo != -1) cerr << foo << ": " << no_scientific(get_or_insert(Memory, foo)) << '\n'; else cerr << '\n'; } else { reagent tmp = current_instruction().ingredients.at(0); canonize(tmp); foo = tmp.value; } break; } case CREATE_ARRAY: { reagent product = current_instruction().products.at(0); canonize(product); long long int base_address = product.value; long long int array_size = to_integer(product.type->right->right->name); // initialize array size, so that size_of will work put(Memory, base_address, array_size); // in array elements long long int size = size_of(product); // in locations trace(9998, "run") << "creating array of size " << size << '\n' << end(); // initialize array for (long long int i = 1; i <= size_of(product); ++i) { put(Memory, base_address+i, 0); } // dummy product; doesn't actually do anything products.resize(1); products.at(0).push_back(array_size); break; } case INDEX: { reagent base = current_instruction().ingredients.at(0); canonize(base); long long int base_address = base.value; trace(9998, "run") << "base address is " << base_address << end(); if (base_address == 0) { raise_error << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_string(current_instruction()) << "'\n" << end(); break; } reagent offset = current_instruction().ingredients.at(1); canonize(offset); vector offset_val(read_memory(offset)); type_tree* element_type = array_element(base.type); if (offset_val.at(0) < 0 || offset_val.at(0) >= get_or_insert(Memory, base_address)) { raise_error << maybe(current_recipe_name()) << "invalid index " << no_scientific(offset_val.at(0)) << '\n' << end(); break; } long long int src = base_address + 1 + offset_val.at(0)*size_of(element_type); trace(9998, "run") << "address to copy is " << src << end(); trace(9998, "run") << "its type is " << get(Type, element_type->value).name << end(); reagent tmp; tmp.properties.push_back(pair("raw", NULL)); tmp.set_value(src); tmp.type = new type_tree(*element_type); products.push_back(read_memory(tmp)); break; } case INDEX_ADDRESS: { reagent base = current_instruction().ingredients.at(0); canonize(base); long long int base_address = base.value; if (base_address == 0) { raise_error << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_string(current_instruction()) << "'\n" << end(); break; } reagent offset = current_instruction().ingredients.at(1); canonize(offset); vector offset_val(read_memory(offset)); type_tree* element_type = array_element(base.type); if (offset_val.at(0) < 0 || offset_val.at(0) >= get_or_insert(Memory, base_address)) { raise_error << maybe(current_recipe_name()) << "invalid index " << no_scientific(offset_val.at(0)) << '\n' << end(); break; } long long int result = base_address + 1 + offset_val.at(0)*size_of(element_type); products.resize(1); products.at(0).push_back(result); break; } case LENGTH: { reagent x = current_instruction().ingredients.at(0); canonize(x); if (x.value == 0) { raise_error << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_string(current_instruction()) << "'\n" << end(); break; } products.resize(1); products.at(0).push_back(get_or_insert(Memory, x.value)); break; } case MAYBE_CONVERT: { reagent base = current_instruction().ingredients.at(0); canonize(base); long long int base_address = base.value; if (base_address == 0) { raise_error << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_string(current_instruction()) << "'\n" << end(); break; } long long int tag = current_instruction().ingredients.at(1).value; long long int result; if (tag == static_cast(get_or_insert(Memory, base_address))) { result = base_address+1; } else { result = 0; } products.resize(1); products.at(0).push_back(result); break; } case NEXT_INGREDIENT: { assert(!Current_routine->calls.empty()); if (current_call().next_ingredient_to_process < SIZE(current_call().ingredient_atoms)) { reagent product = current_instruction().products.at(0); canonize_type(product); if (current_recipe_name() == "main") { // no ingredient types since the call might be implicit; assume ingredients are always strings // todo: how to test this? if (!is_mu_string(product)) raise_error << "main: wrong type for ingredient " << product.original_string << '\n' << end(); } else if (!types_coercible(product, current_call().ingredients.at(current_call().next_ingredient_to_process))) { raise_error << maybe(current_recipe_name()) << "wrong type for ingredient " << product.original_string << '\n' << end(); // End next-ingredient Type Mismatch Error } products.push_back( current_call().ingredient_atoms.at(current_call().next_ingredient_to_process)); assert(SIZE(products) == 1); products.resize(2); // push a new vector products.at(1).push_back(1); ++current_call().next_ingredient_to_process; } else { if (SIZE(current_instruction().products) < 2) raise_error << maybe(current_recipe_name()) << "no ingredient to save in " << current_instruction().products.at(0).original_string << '\n' << end(); if (current_instruction().products.empty()) break; products.resize(2); // pad the first product with sufficient zeros to match its type long long int size = size_of(current_instruction().products.at(0)); for (long long int i = 0; i < size; ++i) products.at(0).push_back(0); products.at(1).push_back(0); } break; } case REWIND_INGREDIENTS: { current_call().next_ingredient_to_process = 0; break; } case INGREDIENT: { if (static_cast(ingredients.at(0).at(0)) < SIZE(current_call().ingredient_atoms)) { current_call().next_ingredient_to_process = ingredients.at(0).at(0); products.push_back( current_call().ingredient_atoms.at(current_call().next_ingredient_to_process)); assert(SIZE(products) == 1); products.resize(2); // push a new vector products.at(1).push_back(1); ++current_call().next_ingredient_to_process; } else { if (SIZE(current_instruction().products) > 1) { products.resize(2); products.at(0).push_back(0); // todo: will fail noisily if we try to read a compound value products.at(1).push_back(0); } } break; } case REPLY: { // Starting Reply try_reclaim_locals(); if (Trace_stream) { trace(9999, "trace") << "reply: decrementing callstack depth from " << Trace_stream->callstack_depth << end(); --Trace_stream->callstack_depth; if (Trace_stream->callstack_depth < 0) { Current_routine->calls.clear(); goto stop_running_current_routine; } } Current_routine->calls.pop_front(); // just in case 'main' returns a value, drop it for now if (Current_routine->calls.empty()) goto stop_running_current_routine; const instruction& caller_instruction = current_instruction(); for (long long int i = 0; i < SIZE(caller_instruction.products); ++i) trace(9998, "run") << "result " << i << " is " << to_string(ingredients.at(i)) << end(); // make reply products available to caller copy(ingredients.begin(), ingredients.end(), inserter(products, products.begin())); // End Reply break; // continue to process rest of *caller* instruction } case ALLOCATE: { // compute the space we need long long int size = ingredients.at(0).at(0); if (SIZE(ingredients) > 1) { // array trace(9999, "mem") << "array size is " << ingredients.at(1).at(0) << end(); size = /*space for length*/1 + size*ingredients.at(1).at(0); } // include space for refcount size++; trace(9999, "mem") << "allocating size " << size << end(); //? Total_alloc += size; //? Num_alloc++; // compute the region of memory to return // really crappy at the moment if (get_or_insert(Free_list, size)) { trace(9999, "abandon") << "picking up space from free-list of size " << size << end(); long long int result = get_or_insert(Free_list, size); put(Free_list, size, get_or_insert(Memory, result)); for (long long int curr = result+1; curr < result+size; ++curr) { if (get_or_insert(Memory, curr) != 0) { raise_error << maybe(current_recipe_name()) << "memory in free list was not zeroed out: " << curr << '/' << result << "; somebody wrote to us after free!!!\n" << end(); break; // always fatal } } if (SIZE(current_instruction().ingredients) > 1) put(Memory, result+/*skip refcount*/1, ingredients.at(1).at(0)); else put(Memory, result, 0); products.resize(1); products.at(0).push_back(result); break; } ensure_space(size); const long long int result = Current_routine->alloc; trace(9999, "mem") << "new alloc: " << result << end(); // save result products.resize(1); products.at(0).push_back(result); // initialize allocated space for (long long int address = result; address < result+size; ++address) put(Memory, address, 0); // initialize array length if (SIZE(current_instruction().ingredients) > 1) { trace(9999, "mem") << "storing " << ingredients.at(1).at(0) << " in location " << result+/*skip refcount*/1 << end(); put(Memory, result+/*skip refcount*/1, ingredients.at(1).at(0)); } // bump Current_routine->alloc += size; // no support for reclaiming memory assert(Current_routine->alloc <= Current_routine->alloc_max); break; } case NEW: { if (is_literal_string(current_instruction().ingredients.at(0))) { products.resize(1); products.at(0).push_back(new_mu_string(current_instruction().ingredients.at(0).name)); break; } raise << "no implementation for 'new'; why wasn't it translated to 'allocate'?\n" << end(); break; } //? :(before "End Globals") //? long long int Total_alloc = 0; //? long long int Num_alloc = 0; //? long long int Total_free = 0; //? long long int Num_free = 0; //? :(before "End Setup") //? Total_alloc = Num_alloc = Total_free = Num_free = 0; //? :(before "End Teardown") //? cerr << Total_alloc << "/" << Num_alloc //? << " vs " << Total_free << "/" << Num_free << '\n'; //? cerr << SIZE(Memory) << '\n'; case ABANDON: { long long int address = ingredients.at(0).at(0); trace(9999, "abandon") << "address to abandon is " << address << end(); reagent types = current_instruction().ingredients.at(0); trace(9999, "abandon") << "value of ingredient is " << types.value << end(); canonize(types); // lookup_memory without drop_one_lookup { trace(9999, "abandon") << "value of ingredient after canonization is " << types.value << end(); long long int address_location = types.value; types.set_value(get_or_insert(Memory, types.value)+/*skip refcount*/1); drop_from_type(types, "address"); drop_from_type(types, "shared"); // } abandon(address, size_of(types)+/*refcount*/1); // clear the address trace(9999, "mem") << "resetting location " << address_location << end(); put(Memory, address_location, 0); break; } case TO_LOCATION_ARRAY: { long long int array_size = SIZE(ingredients.at(0)); long long int allocation_size = array_size + /*refcount*/1 + /*length*/1; ensure_space(allocation_size); const long long int result = Current_routine->alloc; products.resize(1); products.at(0).push_back(result); // initialize array refcount put(Memory, result, 0); // initialize array length put(Memory, result+1, array_size); // now copy over data for (long long int i = 0; i < array_size; ++i) put(Memory, result+2+i, ingredients.at(0).at(i)); break; } case RUN: { assert(Name[Next_recipe_ordinal].empty()); ostringstream tmp; tmp << "recipe run_" << Next_recipe_ordinal << " [ " << current_instruction().ingredients.at(0).name << " ]"; //? cerr << tmp.str() << '\n'; //? cerr << "before load\n"; vector tmp_recipe = load(tmp.str()); //? cerr << "before bind\n"; bind_special_scenario_names(tmp_recipe.at(0)); //? cerr << "before transform\n"; transform_all(); // There's a restriction on the number of variables 'run' can use, so that // it can avoid colliding with the dynamic allocator in case it doesn't // initialize a default-space. assert(Name[tmp_recipe.at(0)][""] < Max_variables_in_scenarios); //? cerr << "end\n"; if (Trace_stream) { ++Trace_stream->callstack_depth; trace(9998, "trace") << "run: incrementing callstack depth to " << Trace_stream->callstack_depth << end(); assert(Trace_stream->callstack_depth < 9000); // 9998-101 plus cushion } Current_routine->calls.push_front(call(tmp_recipe.at(0))); continue; // not done with caller; don't increment current_step_index() } // Some variables for fake resources always get special addresses in // scenarios. case MEMORY_SHOULD_CONTAIN: { if (!Passed) break; check_memory(current_instruction().ingredients.at(0).name); break; } case TRACE_SHOULD_CONTAIN: { if (!Passed) break; check_trace(current_instruction().ingredients.at(0).name); break; } case TRACE_SHOULD_NOT_CONTAIN: { if (!Passed) break; check_trace_missing(current_instruction().ingredients.at(0).name); break; } case CHECK_TRACE_COUNT_FOR_LABEL: { if (!Passed) break; long long int expected_count = ingredients.at(0).at(0); string label = current_instruction().ingredients.at(1).name; long long int count = trace_count(label); if (count != expected_count) { if (Current_scenario && !Scenario_testing_scenario) { // genuine test in a mu file raise_error << "\nF - " << Current_scenario->name << ": " << maybe(current_recipe_name()) << "expected " << expected_count << " lines in trace with label " << label << " in trace: "; DUMP(label); raise_error; } else { // just testing scenario support raise_error << maybe(current_recipe_name()) << "expected " << expected_count << " lines in trace with label " << label << " in trace\n" << end(); } if (!Scenario_testing_scenario) { Passed = false; ++Num_failures; } } break; } case NEXT_INGREDIENT_WITHOUT_TYPECHECKING: { assert(!Current_routine->calls.empty()); if (current_call().next_ingredient_to_process < SIZE(current_call().ingredient_atoms)) { products.push_back( current_call().ingredient_atoms.at(current_call().next_ingredient_to_process)); assert(SIZE(products) == 1); products.resize(2); // push a new vector products.at(1).push_back(1); ++current_call().next_ingredient_to_process; } else { products.resize(2); // pad the first product with sufficient zeros to match its type long long int size = size_of(current_instruction().products.at(0)); for (long long int i = 0; i < size; ++i) products.at(0).push_back(0); products.at(1).push_back(0); } break; } case CALL: { // Begin Call if (Trace_stream) { ++Trace_stream->callstack_depth; trace("trace") << "indirect 'call': incrementing callstack depth to " << Trace_stream->callstack_depth << end(); assert(Trace_stream->callstack_depth < 9000); // 9998-101 plus cushion } const instruction& caller_instruction = current_instruction(); Current_routine->calls.push_front(call(ingredients.at(0).at(0))); ingredients.erase(ingredients.begin()); // drop the callee finish_call_housekeeping(caller_instruction, ingredients); continue; } case START_RUNNING: { routine* new_routine = new routine(ingredients.at(0).at(0)); new_routine->parent_index = Current_routine_index; // populate ingredients for (long long int i = 1; i < SIZE(current_instruction().ingredients); ++i) { new_routine->calls.front().ingredient_atoms.push_back(ingredients.at(i)); reagent ingredient = current_instruction().ingredients.at(i); canonize_type(ingredient); new_routine->calls.front().ingredients.push_back(ingredient); } Routines.push_back(new_routine); products.resize(1); products.at(0).push_back(new_routine->id); break; } case ROUTINE_STATE: { long long int id = ingredients.at(0).at(0); long long int result = -1; for (long long int i = 0; i < SIZE(Routines); ++i) { if (Routines.at(i)->id == id) { result = Routines.at(i)->state; break; } } products.resize(1); products.at(0).push_back(result); break; } case RESTART: { long long int id = ingredients.at(0).at(0); for (long long int i = 0; i < SIZE(Routines); ++i) { if (Routines.at(i)->id == id) { Routines.at(i)->state = RUNNING; break; } } break; } case STOP: { long long int id = ingredients.at(0).at(0); for (long long int i = 0; i < SIZE(Routines); ++i) { if (Routines.at(i)->id == id) { Routines.at(i)->state = COMPLETED; break; } } break; } case _DUMP_ROUTINES: { for (long long int i = 0; i < SIZE(Routines); ++i) { cerr << i << ": " << Routines.at(i)->id << ' ' << Routines.at(i)->state << ' ' << Routines.at(i)->parent_index << '\n'; } break; } case LIMIT_TIME: { long long int id = ingredients.at(0).at(0); for (long long int i = 0; i < SIZE(Routines); ++i) { if (Routines.at(i)->id == id) { Routines.at(i)->limit = ingredients.at(1).at(0); break; } } break; } case WAIT_FOR_LOCATION: { reagent loc = current_instruction().ingredients.at(0); canonize(loc); Current_routine->state = WAITING; Current_routine->waiting_on_location = loc.value; Current_routine->old_value_of_waiting_location = get_or_insert(Memory, loc.value); trace(9998, "run") << "waiting for location " << loc.value << " to change from " << no_scientific(get_or_insert(Memory, loc.value)) << end(); break; } case WAIT_FOR_ROUTINE: { if (ingredients.at(0).at(0) == Current_routine->id) { raise_error << maybe(current_recipe_name()) << "routine can't wait for itself! " << to_string(current_instruction()) << '\n' << end(); break; } Current_routine->state = WAITING; Current_routine->waiting_on_routine = ingredients.at(0).at(0); trace(9998, "run") << "waiting for routine " << ingredients.at(0).at(0) << end(); break; } case SWITCH: { long long int id = some_other_running_routine(); if (id) { assert(id != Current_routine->id); Current_routine->state = WAITING; Current_routine->waiting_on_routine = id; } break; } case RANDOM: { // todo: limited range of numbers, might be imperfectly random // todo: thread state in extra ingredients and products products.resize(1); products.at(0).push_back(rand()); break; } case MAKE_RANDOM_NONDETERMINISTIC: { srand(time(NULL)); break; } case ROUND: { products.resize(1); products.at(0).push_back(rint(ingredients.at(0).at(0))); break; } case HASH: { reagent input = current_instruction().ingredients.at(0); // copy products.resize(1); products.at(0).push_back(hash(0, input)); break; } case HASH_OLD: { string input = read_mu_string(ingredients.at(0).at(0)); size_t h = 0 ; for (long long int i = 0; i < SIZE(input); ++i) { h += static_cast(input.at(i)); h += (h<<10); h ^= (h>>6); h += (h<<3); h ^= (h>>11); h += (h<<15); } products.resize(1); products.at(0).push_back(h); break; } case OPEN_CONSOLE: { tb_init(); Display_row = Display_column = 0; long long int width = tb_width(); long long int height = tb_height(); if (width > 222 || height > 222) tb_shutdown(); if (width > 222) raise_error << "sorry, mu doesn't support windows wider than 222 characters. Please resize your window.\n" << end(); if (height > 222) raise_error << "sorry, mu doesn't support windows taller than 222 characters. Please resize your window.\n" << end(); break; } case CLOSE_CONSOLE: { tb_shutdown(); break; } case CLEAR_DISPLAY: { tb_clear(); Display_row = Display_column = 0; break; } case SYNC_DISPLAY: { tb_sync(); break; } case CLEAR_LINE_ON_DISPLAY: { long long int width = tb_width(); for (long long int x = Display_column; x < width; ++x) { tb_change_cell(x, Display_row, ' ', TB_WHITE, TB_BLACK); } tb_set_cursor(Display_column, Display_row); if (Autodisplay) tb_present(); break; } case PRINT_CHARACTER_TO_DISPLAY: { int h=tb_height(), w=tb_width(); long long int height = (h >= 0) ? h : 0; long long int width = (w >= 0) ? w : 0; long long int c = ingredients.at(0).at(0); int color = TB_BLACK; if (SIZE(ingredients) > 1) { color = ingredients.at(1).at(0); } int bg_color = TB_BLACK; if (SIZE(ingredients) > 2) { bg_color = ingredients.at(2).at(0); if (bg_color == 0) bg_color = TB_BLACK; } tb_change_cell(Display_column, Display_row, c, color, bg_color); if (c == '\n' || c == '\r') { if (Display_row < height-1) { Display_column = 0; ++Display_row; tb_set_cursor(Display_column, Display_row); if (Autodisplay) tb_present(); } break; } if (c == '\b') { if (Display_column > 0) { tb_change_cell(Display_column-1, Display_row, ' ', color, bg_color); --Display_column; tb_set_cursor(Display_column, Display_row); if (Autodisplay) tb_present(); } break; } if (Display_column < width-1) { ++Display_column; tb_set_cursor(Display_column, Display_row); } if (Autodisplay) tb_present(); break; } case CURSOR_POSITION_ON_DISPLAY: { products.resize(2); products.at(0).push_back(Display_row); products.at(1).push_back(Display_column); break; } case MOVE_CURSOR_ON_DISPLAY: { Display_row = ingredients.at(0).at(0); Display_column = ingredients.at(1).at(0); tb_set_cursor(Display_column, Display_row); if (Autodisplay) tb_present(); break; } case MOVE_CURSOR_DOWN_ON_DISPLAY: { int h=tb_height(); long long int height = (h >= 0) ? h : 0; if (Display_row < height-1) { Display_row++; tb_set_cursor(Display_column, Display_row); if (Autodisplay) tb_present(); } break; } case MOVE_CURSOR_UP_ON_DISPLAY: { if (Display_row > 0) { Display_row--; tb_set_cursor(Display_column, Display_row); if (Autodisplay) tb_present(); } break; } case MOVE_CURSOR_RIGHT_ON_DISPLAY: { int w=tb_width(); long long int width = (w >= 0) ? w : 0; if (Display_column < width-1) { Display_column++; tb_set_cursor(Display_column, Display_row); if (Autodisplay) tb_present(); } break; } case MOVE_CURSOR_LEFT_ON_DISPLAY: { if (Display_column > 0) { Display_column--; tb_set_cursor(Display_column, Display_row); if (Autodisplay) tb_present(); } break; } case DISPLAY_WIDTH: { products.resize(1); products.at(0).push_back(tb_width()); break; } case DISPLAY_HEIGHT: { products.resize(1); products.at(0).push_back(tb_height()); break; } case HIDE_CURSOR_ON_DISPLAY: { tb_set_cursor(TB_HIDE_CURSOR, TB_HIDE_CURSOR); break; } case SHOW_CURSOR_ON_DISPLAY: { tb_set_cursor(Display_row, Display_column); break; } case HIDE_DISPLAY: { Autodisplay = false; break; } case SHOW_DISPLAY: { Autodisplay = true; tb_present(); break; } case WAIT_FOR_SOME_INTERACTION: { tb_event event; tb_poll_event(&event); break; } case CHECK_FOR_INTERACTION: { products.resize(2); // result and status tb_event event; int event_type = tb_peek_event(&event, 5/*ms*/); if (event_type == TB_EVENT_KEY && event.ch) { products.at(0).push_back(/*text event*/0); products.at(0).push_back(event.ch); products.at(0).push_back(0); products.at(0).push_back(0); products.at(1).push_back(/*found*/true); break; } // treat keys within ascii as unicode characters if (event_type == TB_EVENT_KEY && event.key < 0xff) { products.at(0).push_back(/*text event*/0); if (event.key == TB_KEY_CTRL_C) { tb_shutdown(); exit(1); } if (event.key == TB_KEY_BACKSPACE2) event.key = TB_KEY_BACKSPACE; if (event.key == TB_KEY_CARRIAGE_RETURN) event.key = TB_KEY_NEWLINE; products.at(0).push_back(event.key); products.at(0).push_back(0); products.at(0).push_back(0); products.at(1).push_back(/*found*/true); break; } // keys outside ascii aren't unicode characters but arbitrary termbox inventions if (event_type == TB_EVENT_KEY) { products.at(0).push_back(/*keycode event*/1); products.at(0).push_back(event.key); products.at(0).push_back(0); products.at(0).push_back(0); products.at(1).push_back(/*found*/true); break; } if (event_type == TB_EVENT_MOUSE) { products.at(0).push_back(/*touch event*/2); products.at(0).push_back(event.key); // which button, etc. products.at(0).push_back(event.y); // row products.at(0).push_back(event.x); // column products.at(1).push_back(/*found*/true); break; } if (event_type == TB_EVENT_RESIZE) { products.at(0).push_back(/*resize event*/3); products.at(0).push_back(event.w); // width products.at(0).push_back(event.h); // height products.at(0).push_back(0); products.at(1).push_back(/*found*/true); break; } assert(event_type == 0); products.at(0).push_back(0); products.at(0).push_back(0); products.at(0).push_back(0); products.at(0).push_back(0); products.at(1).push_back(/*found*/false); break; } case INTERACTIONS_LEFT: { products.resize(1); products.at(0).push_back(tb_event_ready()); break; } case CLEAR_DISPLAY_FROM: { // todo: error checking int row = ingredients.at(0).at(0); int column = ingredients.at(1).at(0); int left = ingredients.at(2).at(0); int right = ingredients.at(3).at(0); int height=tb_height(); for (; row < height; ++row, column=left) { // start column from left in every inner loop except first for (; column <= right; ++column) { tb_change_cell(column, row, ' ', TB_WHITE, TB_BLACK); } } if (Autodisplay) tb_present(); break; } case SCREEN_SHOULD_CONTAIN: { //? cerr << SIZE(get(Recipe_variants, "insert")) << '\n'; //? cerr << debug_string(get(Recipe, get(Recipe_ordinal, "insert_4"))) << '\n'; if (!Passed) break; assert(scalar(ingredients.at(0))); check_screen(current_instruction().ingredients.at(0).name, -1); break; } case SCREEN_SHOULD_CONTAIN_IN_COLOR: { if (!Passed) break; assert(scalar(ingredients.at(0))); assert(scalar(ingredients.at(1))); check_screen(current_instruction().ingredients.at(1).name, ingredients.at(0).at(0)); break; } case _DUMP_SCREEN: { dump_screen(); break; } case ASSUME_CONSOLE: { // create a temporary recipe just for parsing; it won't contain valid instructions istringstream in("[" + current_instruction().ingredients.at(0).name + "]"); recipe r; slurp_body(in, r); long long int num_events = count_events(r); // initialize the events like in new-fake-console long long int size = /*space for refcount*/1 + /*space for length*/1 + num_events*size_of_event(); ensure_space(size); long long int event_data_address = Current_routine->alloc; // store length put(Memory, Current_routine->alloc+/*skip refcount*/1, num_events); Current_routine->alloc += /*skip refcount and length*/2; for (long long int i = 0; i < SIZE(r.steps); ++i) { const instruction& curr = r.steps.at(i); if (curr.name == "left-click") { trace(9999, "mem") << "storing 'left-click' event starting at " << Current_routine->alloc << end(); put(Memory, Current_routine->alloc, /*tag for 'touch-event' variant of 'event' exclusive-container*/2); put(Memory, Current_routine->alloc+1+/*offset of 'type' in 'mouse-event'*/0, TB_KEY_MOUSE_LEFT); put(Memory, Current_routine->alloc+1+/*offset of 'row' in 'mouse-event'*/1, to_integer(curr.ingredients.at(0).name)); put(Memory, Current_routine->alloc+1+/*offset of 'column' in 'mouse-event'*/2, to_integer(curr.ingredients.at(1).name)); Current_routine->alloc += size_of_event(); } else if (curr.name == "press") { trace(9999, "mem") << "storing 'press' event starting at " << Current_routine->alloc << end(); string key = curr.ingredients.at(0).name; if (is_integer(key)) put(Memory, Current_routine->alloc+1, to_integer(key)); else if (contains_key(Key, key)) put(Memory, Current_routine->alloc+1, Key[key]); else raise_error << "assume-console: can't press " << key << '\n' << end(); if (get_or_insert(Memory, Current_routine->alloc+1) < 256) // these keys are in ascii put(Memory, Current_routine->alloc, /*tag for 'text' variant of 'event' exclusive-container*/0); else { // distinguish from unicode put(Memory, Current_routine->alloc, /*tag for 'keycode' variant of 'event' exclusive-container*/1); } Current_routine->alloc += size_of_event(); } // End Event Handlers else { // keyboard input assert(curr.name == "type"); trace(9999, "mem") << "storing 'type' event starting at " << Current_routine->alloc << end(); const string& contents = curr.ingredients.at(0).name; const char* raw_contents = contents.c_str(); long long int num_keyboard_events = unicode_length(contents); long long int curr = 0; for (long long int i = 0; i < num_keyboard_events; ++i) { trace(9999, "mem") << "storing 'text' tag at " << Current_routine->alloc << end(); put(Memory, Current_routine->alloc, /*tag for 'text' variant of 'event' exclusive-container*/0); uint32_t curr_character; assert(curr < SIZE(contents)); tb_utf8_char_to_unicode(&curr_character, &raw_contents[curr]); trace(9999, "mem") << "storing character " << curr_character << " at " << Current_routine->alloc+1 << end(); put(Memory, Current_routine->alloc+/*skip exclusive container tag*/1, curr_character); curr += tb_utf8_char_length(raw_contents[curr]); Current_routine->alloc += size_of_event(); } } } assert(Current_routine->alloc == event_data_address+size); // wrap the array of events in a console object ensure_space(size_of_console()); put(Memory, CONSOLE, Current_routine->alloc); trace(9999, "mem") << "storing console in " << Current_routine->alloc << end(); Current_routine->alloc += size_of_console(); long long int console_address = get_or_insert(Memory, CONSOLE); trace(9999, "mem") << "storing console data in " << console_address+2 << end(); put(Memory, console_address+/*skip refcount*/1+/*offset of 'data' in container 'events'*/1, event_data_address); break; } case REPLACE_IN_CONSOLE: { assert(scalar(ingredients.at(0))); if (!get_or_insert(Memory, CONSOLE)) { raise_error << "console not initialized\n" << end(); break; } long long int console_address = get_or_insert(Memory, CONSOLE); long long int console_data = get_or_insert(Memory, console_address+1); long long int size = get_or_insert(Memory, console_data); // array size for (long long int i = 0, curr = console_data+1; i < size; ++i, curr+=size_of_event()) { if (get_or_insert(Memory, curr) != /*text*/0) continue; if (get_or_insert(Memory, curr+1) != ingredients.at(0).at(0)) continue; for (long long int n = 0; n < size_of_event(); ++n) put(Memory, curr+n, ingredients.at(1).at(n)); } break; } case _BROWSE_TRACE: { start_trace_browser(); break; } case RUN_INTERACTIVE: { bool new_code_pushed_to_stack = run_interactive(ingredients.at(0).at(0)); if (!new_code_pushed_to_stack) { products.resize(5); products.at(0).push_back(0); products.at(1).push_back(trace_error_warning_contents()); products.at(2).push_back(0); products.at(3).push_back(trace_app_contents()); products.at(4).push_back(1); // completed run_code_end(); break; // done with this instruction } else { continue; // not done with caller; don't increment current_step_index() } } case _START_TRACKING_PRODUCTS: { Track_most_recent_products = true; break; } case _STOP_TRACKING_PRODUCTS: { Track_most_recent_products = false; break; } case _MOST_RECENT_PRODUCTS: { products.resize(1); products.at(0).push_back(new_mu_string(Most_recent_products)); break; } case SAVE_ERRORS_WARNINGS: { products.resize(1); products.at(0).push_back(trace_error_warning_contents()); break; } case SAVE_APP_TRACE: { products.resize(1); products.at(0).push_back(trace_app_contents()); break; } case _CLEANUP_RUN_INTERACTIVE: { run_code_end(); break; } case RELOAD: { //? cerr << "== reload\n"; // clear any containers in advance for (long long int i = 0; i < SIZE(Recently_added_types); ++i) { if (!contains_key(Type, Recently_added_types.at(i))) continue; Type_ordinal.erase(get(Type, Recently_added_types.at(i)).name); Type.erase(Recently_added_types.at(i)); } for (map >::iterator p = Recipe_variants.begin(); p != Recipe_variants.end(); ++p) { //? cerr << p->first << ":\n"; vector& variants = p->second; for (long long int i = 0; i < SIZE(p->second); ++i) { if (variants.at(i) == -1) continue; if (find(Recently_added_shape_shifting_recipes.begin(), Recently_added_shape_shifting_recipes.end(), variants.at(i)) != Recently_added_shape_shifting_recipes.end()) { //? cerr << " " << variants.at(i) << ' ' << get(Recipe, variants.at(i)).name << '\n'; variants.at(i) = -1; // ghost } } } for (long long int i = 0; i < SIZE(Recently_added_shape_shifting_recipes); ++i) { Recipe_ordinal.erase(get(Recipe, Recently_added_shape_shifting_recipes.at(i)).name); Recipe.erase(Recently_added_shape_shifting_recipes.at(i)); } Recently_added_shape_shifting_recipes.clear(); string code = read_mu_string(ingredients.at(0).at(0)); run_code_begin(/*snapshot_recently_added_recipes*/false); routine* save_current_routine = Current_routine; Current_routine = NULL; vector recipes_reloaded = load(code); // clear a few things from previous runs // ad hoc list; we've probably missed a few for (long long int i = 0; i < SIZE(recipes_reloaded); ++i) Name.erase(recipes_reloaded.at(i)); transform_all(); Trace_stream->newline(); // flush trace Current_routine = save_current_routine; products.resize(1); products.at(0).push_back(trace_error_warning_contents()); run_code_end(); // wait until we're done with the trace contents //? cerr << "reload done\n"; break; } case RESTORE: { string filename; if (is_literal_string(current_instruction().ingredients.at(0))) { filename = current_instruction().ingredients.at(0).name; } else if (is_mu_string(current_instruction().ingredients.at(0))) { filename = read_mu_string(ingredients.at(0).at(0)); } if (Current_scenario) { // do nothing in tests products.resize(1); products.at(0).push_back(0); break; } string contents = slurp("lesson/"+filename); products.resize(1); if (contents.empty()) products.at(0).push_back(0); else products.at(0).push_back(new_mu_string(contents)); break; } case SAVE: { if (Current_scenario) break; // do nothing in tests string filename; if (is_literal_string(current_instruction().ingredients.at(0))) { filename = current_instruction().ingredients.at(0).name; } else if (is_mu_string(current_instruction().ingredients.at(0))) { filename = read_mu_string(ingredients.at(0).at(0)); } ofstream fout(("lesson/"+filename).c_str()); string contents = read_mu_string(ingredients.at(1).at(0)); fout << contents; fout.close(); if (!exists("lesson/.git")) break; // bug in git: git diff -q messes up --exit-code // explicitly say '--all' for git 1.9 int status = system("cd lesson; git add --all .; git diff HEAD --exit-code >/dev/null || git commit -a -m . >/dev/null"); if (status != 0) raise_error << "error in commit: contents " << contents << '\n' << end(); break; } // End Primitive Recipe Implementations default: { const instruction& call_instruction = current_instruction(); if (Recipe.find(current_instruction().operation) == Recipe.end()) { // duplicate from Checks // stop running this instruction immediately ++current_step_index(); continue; } // not a primitive; look up the book of recipes if (Trace_stream) { ++Trace_stream->callstack_depth; trace(9999, "trace") << "incrementing callstack depth to " << Trace_stream->callstack_depth << end(); assert(Trace_stream->callstack_depth < 9000); // 9998-101 plus cushion } Current_routine->calls.push_front(call(current_instruction().operation)); finish_call_housekeeping(call_instruction, ingredients); continue; // not done with caller; don't increment step_index of caller } } if (SIZE(products) < SIZE(current_instruction().products)) { raise_error << SIZE(products) << " vs " << SIZE(current_instruction().products) << ": failed to write to all products! " << to_string(current_instruction()) << '\n' << end(); } else { for (long long int i = 0; i < SIZE(current_instruction().products); ++i) { write_memory(current_instruction().products.at(i), products.at(i)); } } if (Track_most_recent_products) { track_most_recent_products(current_instruction(), products); } // End of Instruction ++current_step_index(); } stop_running_current_routine:; } bool should_copy_ingredients() { recipe_ordinal r = current_instruction().operation; if (r == CREATE_ARRAY || r == INDEX || r == INDEX_ADDRESS || r == LENGTH) return false; // End should_copy_ingredients Special-cases return true; } inline long long int& current_step_index() { assert(!Current_routine->calls.empty()); return current_call().running_step_index; } inline const string& current_recipe_name() { assert(!Current_routine->calls.empty()); return get(Recipe, current_call().running_recipe).name; } inline const instruction& current_instruction() { assert(!Current_routine->calls.empty()); return to_instruction(current_call()); } inline bool routine::completed() const { return calls.empty(); } inline const vector& routine::steps() const { assert(!calls.empty()); return get(Recipe, calls.front().running_recipe).steps; } void run_main(int argc, char* argv[]) { recipe_ordinal r = get(Recipe_ordinal, "main"); assert(r); routine* main_routine = new routine(r); // pass in commandline args as ingredients to main // todo: test this Current_routine = main_routine; for (long long int i = 1; i < argc; ++i) { vector arg; arg.push_back(new_mu_string(argv[i])); current_call().ingredient_atoms.push_back(arg); } run(main_routine); } void dump_profile() { for (map::iterator p = Instructions_running.begin(); p != Instructions_running.end(); ++p) { cerr << p->first << ": " << p->second << '\n'; } cerr << "== locations read\n"; for (map::iterator p = Locations_read.begin(); p != Locations_read.end(); ++p) { cerr << p->first << ": " << p->second << '\n'; } cerr << "== locations read by instruction\n"; for (map::iterator p = Locations_read_by_instruction.begin(); p != Locations_read_by_instruction.end(); ++p) { cerr << p->first << ": " << p->second << '\n'; } } void cleanup_main() { if (!Trace_file.empty() && Trace_stream) { ofstream fout((Trace_dir+Trace_file).c_str()); fout << Trace_stream->readable_contents(""); fout.close(); } } void load_permanently(string filename) { if (is_directory(filename)) { load_all_permanently(filename); return; } ifstream fin(filename.c_str()); fin.peek(); if (!fin) { raise_error << "no such file " << filename << '\n' << end(); return; } fin >> std::noskipws; trace(9990, "load") << "=== " << filename << end(); load(fin); fin.close(); // freeze everything so it doesn't get cleared by tests Recently_added_recipes.clear(); Recently_added_types.clear(); // End load_permanently. } bool is_directory(string path) { struct stat info; if (stat(path.c_str(), &info)) return false; // error return info.st_mode & S_IFDIR; } void load_all_permanently(string dir) { dirent** files; int num_files = scandir(dir.c_str(), &files, NULL, alphasort); for (int i = 0; i < num_files; ++i) { string curr_file = files[i]->d_name; if (!isdigit(curr_file.at(0))) continue; load_permanently(dir+'/'+curr_file); free(files[i]); files[i] = NULL; } free(files); } vector read_memory(reagent x) { if (x.name == "number-of-locals") { vector result; result.push_back(Name[get(Recipe_ordinal, current_recipe_name())][""]); if (result.back() == 0) raise_error << "no space allocated for default-space in recipe " << current_recipe_name() << "; are you using names?\n" << end(); return result; } if (x.name == "default-space") { vector result; result.push_back(current_call().default_space); return result; } vector result; if (is_literal(x)) { result.push_back(x.value); return result; } canonize(x); long long int base = x.value; long long int size = size_of(x); for (long long int offset = 0; offset < size; ++offset) { double val = get_or_insert(Memory, base+offset); trace(9999, "mem") << "location " << base+offset << " is " << no_scientific(val) << end(); result.push_back(val); } return result; } void write_memory(reagent x, vector data) { if (x.name == "global-space") { if (!scalar(data) || !x.type || x.type->value != get(Type_ordinal, "address") || !x.type->right || x.type->right->value != get(Type_ordinal, "shared") || !x.type->right->right || x.type->right->right->value != get(Type_ordinal, "array") || !x.type->right->right->right || x.type->right->right->right->value != get(Type_ordinal, "location") || x.type->right->right->right->right) { raise_error << maybe(current_recipe_name()) << "'global-space' should be of type address:shared:array:location, but tried to write " << to_string(data) << '\n' << end(); } if (Current_routine->global_space) raise_error << "routine already has a global-space; you can't over-write your globals" << end(); Current_routine->global_space = data.at(0); return; } if (x.name == "number-of-locals") { raise_error << maybe(current_recipe_name()) << "can't write to special name 'number-of-locals'\n" << end(); return; } if (x.name == "default-space") { if (!scalar(data) || !x.type || x.type->value != get(Type_ordinal, "address") || !x.type->right || x.type->right->value != get(Type_ordinal, "shared") || !x.type->right->right || x.type->right->right->value != get(Type_ordinal, "array") || !x.type->right->right->right || x.type->right->right->right->value != get(Type_ordinal, "location") || x.type->right->right->right->right) { raise_error << maybe(current_recipe_name()) << "'default-space' should be of type address:shared:array:location, but tried to write " << to_string(data) << '\n' << end(); } current_call().default_space = data.at(0); return; } if (!x.type) { raise_error << "can't write to " << to_string(x) << "; no type\n" << end(); return; } if (is_dummy(x)) return; if (is_literal(x)) return; canonize(x); if (x.value == 0) { raise_error << "can't write to location 0 in '" << to_string(current_instruction()) << "'\n" << end(); return; } long long int base = x.value; if (base == 0) return; if (size_mismatch(x, data)) { raise_error << maybe(current_recipe_name()) << "size mismatch in storing to " << x.original_string << " (" << size_of(x.type) << " vs " << SIZE(data) << ") at '" << to_string(current_instruction()) << "'\n" << end(); return; } if (x.type->value == get(Type_ordinal, "address") && x.type->right && x.type->right->value == get(Type_ordinal, "shared")) { // compute old address of x, as well as new address we want to write in long long int old_address = get_or_insert(Memory, x.value); assert(scalar(data)); long long int new_address = data.at(0); // decrement refcount of old address if (old_address) { long long int old_refcount = get_or_insert(Memory, old_address); trace(9999, "mem") << "decrementing refcount of " << old_address << ": " << old_refcount << " -> " << (old_refcount-1) << end(); put(Memory, old_address, old_refcount-1); } // perform the write trace(9999, "mem") << "storing " << no_scientific(data.at(0)) << " in location " << base << end(); put(Memory, base, new_address); // increment refcount of new address if (new_address) { long long int new_refcount = get_or_insert(Memory, new_address); assert(new_refcount >= 0); // == 0 only when new_address == old_address trace(9999, "mem") << "incrementing refcount of " << new_address << ": " << new_refcount << " -> " << (new_refcount+1) << end(); put(Memory, new_address, new_refcount+1); } // abandon old address if necessary // do this after all refcount updates are done just in case old and new are identical assert(get_or_insert(Memory, old_address) >= 0); if (old_address && get_or_insert(Memory, old_address) == 0) { // lookup_memory without drop_one_lookup { trace(9999, "mem") << "automatically abandoning " << old_address << end(); trace(9999, "mem") << "computing size to abandon at " << x.value << end(); x.set_value(get_or_insert(Memory, x.value)+/*skip refcount*/1); drop_from_type(x, "address"); drop_from_type(x, "shared"); // } abandon(old_address, size_of(x)+/*refcount*/1); } return; } // End write_memory(reagent x, long long int base) Special-cases for (long long int offset = 0; offset < SIZE(data); ++offset) { assert(base+offset > 0); trace(9999, "mem") << "storing " << no_scientific(data.at(offset)) << " in location " << base+offset << end(); put(Memory, base+offset, data.at(offset)); } } long long int size_of(const reagent& r) { if (r.type == NULL) return 0; if (r.type && r.type->value == get(Type_ordinal, "array")) { if (!r.type->right) { raise_error << maybe(current_recipe_name()) << "'" << r.original_string << "' is an array of what?\n" << end(); return 1; } //? trace(9999, "mem") << "computing size of array starting at " << r.value << end(); return 1 + get_or_insert(Memory, r.value)*size_of(array_element(r.type)); } // End size_of(reagent) Cases return size_of(r.type); } long long int size_of(const type_tree* type) { if (type == NULL) return 0; if (type->value == -1) { // error value, but we'll raise it elsewhere return 1; } if (type->value == 0) { assert(!type->left && !type->right); return 1; } if (!contains_key(Type, type->value)) { raise_error << "no such type " << type->value << '\n' << end(); return 0; } type_info t = get(Type, type->value); if (t.kind == CONTAINER) { // size of a container is the sum of the sizes of its elements long long int result = 0; for (long long int i = 0; i < SIZE(t.elements); ++i) { // todo: strengthen assertion to disallow mutual type recursion if (t.elements.at(i).type->value == type->value) { raise_error << "container " << t.name << " can't include itself as a member\n" << end(); return 0; } reagent tmp; tmp.type = new type_tree(*type); result += size_of(element_type(tmp, i)); } return result; } if (t.kind == EXCLUSIVE_CONTAINER) { // size of an exclusive container is the size of its largest variant // (So like containers, it can't contain arrays.) long long int result = 0; for (long long int i = 0; i < t.size; ++i) { reagent tmp; tmp.type = new type_tree(*type); long long int size = size_of(variant_type(tmp, i)); if (size > result) result = size; } // ...+1 for its tag. return result+1; } // End size_of(type) Cases return 1; } bool size_mismatch(const reagent& x, const vector& data) { if (x.type == NULL) return true; if (x.type && x.type->value == get(Type_ordinal, "array")) return false; if (current_instruction().operation == MERGE && !current_instruction().products.empty() && current_instruction().products.at(0).type) { reagent x = current_instruction().products.at(0); canonize(x); if (get(Type, x.type->value).kind == EXCLUSIVE_CONTAINER) { return size_of(x) < SIZE(data); } } // End size_mismatch(x) Cases //? if (size_of(x) != SIZE(data)) cerr << size_of(x) << " vs " << SIZE(data) << '\n'; return size_of(x) != SIZE(data); } inline bool is_dummy(const reagent& x) { return x.name == "_"; } inline bool is_literal(const reagent& r) { if (!r.type) return false; if (r.type->value == 0) assert(!r.type->left && !r.type->right); return r.type->value == 0; } inline bool scalar(const vector& x) { return SIZE(x) == 1; } inline bool scalar(const vector& x) { return SIZE(x) == 1; } // helper for tests void run(string form) { vector tmp = load(form); transform_all(); if (tmp.empty()) return; if (trace_count("error") > 0) return; run(tmp.front()); } void test_run_label() { Trace_file = "run_label"; run("recipe main [\n +foo\n 1:number <- copy 23\n 2:number <- copy 1:number\n]\n"); CHECK_TRACE_CONTENTS("run: 1:number <- copy 23run: 2:number <- copy 1:number"); CHECK_TRACE_DOESNT_CONTAIN("run: +foo"); } void test_run_dummy() { Trace_file = "run_dummy"; run("recipe main [\n _ <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("run: _ <- copy 0"); } void test_write_to_0_disallowed() { Trace_file = "write_to_0_disallowed"; Hide_errors = true; run("recipe main [\n 0:number <- copy 34\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 34 in location 0"); } void test_comma_without_space() { Trace_file = "comma_without_space"; run("recipe main [\n 1:number, 2:number <- copy 2,2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_space_without_comma() { Trace_file = "space_without_comma"; run("recipe main [\n 1:number, 2:number <- copy 2 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_comma_before_space() { Trace_file = "comma_before_space"; run("recipe main [\n 1:number, 2:number <- copy 2, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_comma_after_space() { Trace_file = "comma_after_space"; run("recipe main [\n 1:number, 2:number <- copy 2 ,2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void check_instruction(const recipe_ordinal r) { trace(9991, "transform") << "--- perform checks for recipe " << get(Recipe, r).name << end(); //? cerr << "--- perform checks for recipe " << get(Recipe, r).name << '\n'; map > metadata; for (long long int i = 0; i < SIZE(get(Recipe, r).steps); ++i) { instruction& inst = get(Recipe, r).steps.at(i); if (inst.is_label) continue; switch (inst.operation) { // Primitive Recipe Checks case COPY: { if (SIZE(inst.products) != SIZE(inst.ingredients)) { raise_error << "ingredients and products should match in '" << to_string(inst) << "'\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!types_coercible(inst.products.at(i), inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "can't copy " << inst.ingredients.at(i).original_string << " to " << inst.products.at(i).original_string << "; types don't match\n" << end(); goto finish_checking_instruction; } } break; } case ADD: { // primary goal of these checks is to forbid address arithmetic for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_number(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'add' requires number ingredients, but got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'add' yields exactly one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'add' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); break; } break; } case SUBTRACT: { if (inst.ingredients.empty()) { raise_error << maybe(get(Recipe, r).name) << "'subtract' has no ingredients\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (is_raw(inst.ingredients.at(i))) continue; // permit address offset computations in tests if (!is_mu_number(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'subtract' requires number ingredients, but got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'subtract' yields exactly one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'subtract' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); break; } break; } case MULTIPLY: { for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_number(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'multiply' requires number ingredients, but got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'multiply' yields exactly one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'multiply' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); break; } break; } case DIVIDE: { if (inst.ingredients.empty()) { raise_error << maybe(get(Recipe, r).name) << "'divide' has no ingredients\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_number(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'divide' requires number ingredients, but got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'divide' yields exactly one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'divide' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); break; } break; } case DIVIDE_WITH_REMAINDER: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'divide-with-remainder' requires exactly two ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'divide-with-remainder' requires number ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (SIZE(inst.products) > 2) { raise_error << maybe(get(Recipe, r).name) << "'divide-with-remainder' yields two products in '" << to_string(inst) << "'\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.products); ++i) { if (!is_dummy(inst.products.at(i)) && !is_mu_number(inst.products.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'divide-with-remainder' should yield a number, but got " << inst.products.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } break; } case SHIFT_LEFT: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'shift-left' requires exactly two ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'shift-left' requires number ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'shift-left' yields one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'shift-left' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); goto finish_checking_instruction; } break; } case SHIFT_RIGHT: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'shift-right' requires exactly two ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'shift-right' requires number ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'shift-right' yields one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'shift-right' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); goto finish_checking_instruction; } break; } case AND_BITS: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'and-bits' requires exactly two ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'and-bits' requires number ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'and-bits' yields one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'and-bits' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); goto finish_checking_instruction; } break; } case OR_BITS: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'or-bits' requires exactly two ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'or-bits' requires number ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'or-bits' yields one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'or-bits' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); goto finish_checking_instruction; } break; } case XOR_BITS: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'xor-bits' requires exactly two ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'xor-bits' requires number ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'xor-bits' yields one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'xor-bits' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); goto finish_checking_instruction; } break; } case FLIP_BITS: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'flip-bits' requires exactly one ingredient, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'flip-bits' requires a number ingredient, but got '" << to_string(inst) << "'\n" << end(); break; } if (SIZE(inst.products) > 1) { raise_error << maybe(get(Recipe, r).name) << "'flip-bits' yields one product in '" << to_string(inst) << "'\n" << end(); break; } if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'flip-bits' should yield a number, but got " << inst.products.at(0).original_string << '\n' << end(); goto finish_checking_instruction; } break; } case AND: { for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_scalar(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'and' requires boolean ingredients, but got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } break; } case OR: { for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_scalar(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'and' requires boolean ingredients, but got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } break; } case NOT: { if (SIZE(inst.products) > SIZE(inst.ingredients)) { raise_error << maybe(get(Recipe, r).name) << "'not' cannot have fewer ingredients than products in '" << to_string(inst) << "'\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_scalar(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'not' requires boolean ingredients, but got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } break; } case JUMP: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'jump' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_scalar(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'jump' should be a label or offset, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case JUMP_IF: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'jump-if' requires exactly two ingredients, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_scalar(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'jump-if' requires a boolean for its first ingredient, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } if (!is_mu_scalar(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'jump-if' requires a label or offset for its second ingredient, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case JUMP_UNLESS: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'jump-unless' requires exactly two ingredients, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_scalar(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'jump-unless' requires a boolean for its first ingredient, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } if (!is_mu_scalar(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'jump-unless' requires a label or offset for its second ingredient, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case EQUAL: { if (SIZE(inst.ingredients) <= 1) { raise_error << maybe(get(Recipe, r).name) << "'equal' needs at least two ingredients to compare in '" << to_string(inst) << "'\n" << end(); break; } break; } case GREATER_THAN: { if (SIZE(inst.ingredients) <= 1) { raise_error << maybe(get(Recipe, r).name) << "'greater-than' needs at least two ingredients to compare in '" << to_string(inst) << "'\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_number(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'greater-than' can only compare numbers; got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } break; } case LESSER_THAN: { if (SIZE(inst.ingredients) <= 1) { raise_error << maybe(get(Recipe, r).name) << "'lesser-than' needs at least two ingredients to compare in '" << to_string(inst) << "'\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_number(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'lesser-than' can only compare numbers; got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } break; } case GREATER_OR_EQUAL: { if (SIZE(inst.ingredients) <= 1) { raise_error << maybe(get(Recipe, r).name) << "'greater-or-equal' needs at least two ingredients to compare in '" << to_string(inst) << "'\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_number(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'greater-or-equal' can only compare numbers; got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } break; } case LESSER_OR_EQUAL: { if (SIZE(inst.ingredients) <= 1) { raise_error << maybe(get(Recipe, r).name) << "'lesser-or-equal' needs at least two ingredients to compare in '" << to_string(inst) << "'\n" << end(); break; } for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (!is_mu_number(inst.ingredients.at(i))) { raise_error << maybe(get(Recipe, r).name) << "'lesser-or-equal' can only compare numbers; got " << inst.ingredients.at(i).original_string << '\n' << end(); goto finish_checking_instruction; } } break; } case TRACE: { if (SIZE(inst.ingredients) < 3) { raise_error << maybe(get(Recipe, r).name) << "'trace' takes three or more ingredients rather than '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'trace' should be a number (depth), but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } if (!is_literal_string(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "second ingredient of 'trace' should be a literal string (label), but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } break; } case STASH: { break; } case HIDE_ERRORS: { break; } case SHOW_ERRORS: { break; } case TRACE_UNTIL: { break; } case _DUMP_TRACE: { break; } case _CLEAR_TRACE: { break; } case _SAVE_TRACE: { break; } case ASSERT: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'assert' takes exactly two ingredients rather than '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_scalar(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'assert' requires a boolean for its first ingredient, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } if (!is_literal_string(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "'assert' requires a literal string for its second ingredient, but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } break; } case _PRINT: { break; } case _EXIT: { break; } case _SYSTEM: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'$system' requires exactly one ingredient, but got none\n" << end(); break; } break; } case _DUMP_MEMORY: { break; } case _LOG: { break; } case GET: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'get' expects exactly 2 ingredients in '" << to_string(inst) << "'\n" << end(); break; } reagent base = inst.ingredients.at(0); // new copy for every invocation // Update GET base in Check if (!canonize_type(base)) break; if (!base.type || !base.type->value || !contains_key(Type, base.type->value) || get(Type, base.type->value).kind != CONTAINER) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'get' should be a container, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } type_ordinal base_type = base.type->value; reagent offset = inst.ingredients.at(1); if (!is_literal(offset) || !is_mu_scalar(offset)) { raise_error << maybe(get(Recipe, r).name) << "second ingredient of 'get' should have type 'offset', but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } long long int offset_value = 0; if (is_integer(offset.name)) // later layers permit non-integer offsets offset_value = to_integer(offset.name); else offset_value = offset.value; if (offset_value < 0 || offset_value >= SIZE(get(Type, base_type).elements)) { raise_error << maybe(get(Recipe, r).name) << "invalid offset " << offset_value << " for " << get(Type, base_type).name << '\n' << end(); break; } if (inst.products.empty()) break; reagent product = inst.products.at(0); // Update GET product in Check if (!canonize_type(product)) break; const reagent element = element_type(base, offset_value); if (!types_coercible(product, element)) { raise_error << maybe(get(Recipe, r).name) << "'get " << base.original_string << ", " << offset.original_string << "' should write to " << names_to_string_without_quotes(element.type) << " but " << product.name << " has type " << names_to_string_without_quotes(product.type) << '\n' << end(); break; } break; } case GET_ADDRESS: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'get-address' expects exactly 2 ingredients in '" << to_string(inst) << "'\n" << end(); break; } reagent base = inst.ingredients.at(0); // Update GET_ADDRESS base in Check if (!canonize_type(base)) break; if (!base.type || !base.type->value || !contains_key(Type, base.type->value) || get(Type, base.type->value).kind != CONTAINER) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'get-address' should be a container, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } type_ordinal base_type = base.type->value; reagent offset = inst.ingredients.at(1); if (!is_literal(offset) || !is_mu_scalar(offset)) { raise_error << maybe(get(Recipe, r).name) << "second ingredient of 'get' should have type 'offset', but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } long long int offset_value = 0; if (is_integer(offset.name)) { // later layers permit non-integer offsets offset_value = to_integer(offset.name); if (offset_value < 0 || offset_value >= SIZE(get(Type, base_type).elements)) { raise_error << maybe(get(Recipe, r).name) << "invalid offset " << offset_value << " for " << get(Type, base_type).name << '\n' << end(); break; } } else { offset_value = offset.value; } reagent product = inst.products.at(0); // Update GET_ADDRESS product in Check if (!canonize_type(base)) break; // same type as for GET.. reagent element = element_type(base, offset_value); // ..except for an address at the start element.type = new type_tree("address", get(Type_ordinal, "address"), element.type); if (!types_coercible(product, element)) { raise_error << maybe(get(Recipe, r).name) << "'get-address " << base.original_string << ", " << offset.original_string << "' should write to " << names_to_string_without_quotes(element.type) << " but " << product.name << " has type " << names_to_string_without_quotes(product.type) << '\n' << end(); break; } break; } case MERGE: { // type-checking in a separate transform below break; } case CREATE_ARRAY: { if (inst.products.empty()) { raise_error << maybe(get(Recipe, r).name) << "'create-array' needs one product and no ingredients but got '" << to_string(inst) << '\n' << end(); break; } reagent product = inst.products.at(0); canonize_type(product); if (!is_mu_array(product)) { raise_error << maybe(get(Recipe, r).name) << "'create-array' cannot create non-array " << product.original_string << '\n' << end(); break; } if (!product.type->right) { raise_error << maybe(get(Recipe, r).name) << "create array of what? " << to_string(inst) << '\n' << end(); break; } // 'create-array' will need to check properties rather than types if (!product.type->right->right) { raise_error << maybe(get(Recipe, r).name) << "create array of what size? " << to_string(inst) << '\n' << end(); break; } if (!is_integer(product.type->right->right->name)) { raise_error << maybe(get(Recipe, r).name) << "'create-array' product should specify size of array after its element type, but got " << product.type->right->right->name << '\n' << end(); break; } break; } case INDEX: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'index' expects exactly 2 ingredients in '" << to_string(inst) << "'\n" << end(); break; } reagent base = inst.ingredients.at(0); canonize_type(base); if (!is_mu_array(base)) { raise_error << maybe(get(Recipe, r).name) << "'index' on a non-array " << base.original_string << '\n' << end(); break; } if (inst.products.empty()) break; reagent product = inst.products.at(0); canonize_type(product); reagent element; element.type = new type_tree(*array_element(base.type)); if (!types_coercible(product, element)) { raise_error << maybe(get(Recipe, r).name) << "'index' on " << base.original_string << " can't be saved in " << product.original_string << "; type should be " << names_to_string_without_quotes(element.type) << '\n' << end(); break; } break; } case INDEX_ADDRESS: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'index-address' expects exactly 2 ingredients in '" << to_string(inst) << "'\n" << end(); break; } reagent base = inst.ingredients.at(0); canonize_type(base); if (!is_mu_array(base)) { raise_error << maybe(get(Recipe, r).name) << "'index-address' on a non-array " << base.original_string << '\n' << end(); break; } if (inst.products.empty()) break; reagent product = inst.products.at(0); canonize_type(product); reagent element; element.type = new type_tree("address", get(Type_ordinal, "address"), new type_tree(*array_element(base.type))); if (!types_coercible(product, element)) { raise_error << maybe(get(Recipe, r).name) << "'index' on " << base.original_string << " can't be saved in " << product.original_string << "; type should be " << names_to_string_without_quotes(element.type) << '\n' << end(); break; } break; } case LENGTH: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'length' expects exactly 2 ingredients in '" << to_string(inst) << "'\n" << end(); break; } reagent x = inst.ingredients.at(0); canonize_type(x); if (!is_mu_array(x)) { raise_error << "tried to calculate length of non-array " << x.original_string << '\n' << end(); break; } break; } case MAYBE_CONVERT: { const recipe& caller = get(Recipe, r); if (SIZE(inst.ingredients) != 2) { raise_error << maybe(caller.name) << "'maybe-convert' expects exactly 2 ingredients in '" << to_string(inst) << "'\n" << end(); break; } reagent base = inst.ingredients.at(0); canonize_type(base); if (!base.type || !base.type->value || get(Type, base.type->value).kind != EXCLUSIVE_CONTAINER) { raise_error << maybe(caller.name) << "first ingredient of 'maybe-convert' should be an exclusive-container, but got " << base.original_string << '\n' << end(); break; } if (!is_literal(inst.ingredients.at(1))) { raise_error << maybe(caller.name) << "second ingredient of 'maybe-convert' should have type 'variant', but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } if (inst.products.empty()) break; reagent product = inst.products.at(0); if (!canonize_type(product)) break; reagent& offset = inst.ingredients.at(1); populate_value(offset); if (offset.value >= SIZE(get(Type, base.type->value).elements)) { raise_error << maybe(caller.name) << "invalid tag " << offset.value << " in '" << to_string(inst) << '\n' << end(); break; } reagent variant = variant_type(base, offset.value); variant.type = new type_tree("address", get(Type_ordinal, "address"), variant.type); if (!types_coercible(product, variant)) { raise_error << maybe(caller.name) << "'maybe-convert " << base.original_string << ", " << inst.ingredients.at(1).original_string << "' should write to " << to_string(variant.type) << " but " << product.name << " has type " << to_string(product.type) << '\n' << end(); break; } break; } case NEXT_INGREDIENT: { if (!inst.ingredients.empty()) { raise_error << maybe(get(Recipe, r).name) << "'next-ingredient' didn't expect any ingredients in '" << to_string(inst) << "'\n" << end(); break; } break; } case REWIND_INGREDIENTS: { break; } case INGREDIENT: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'ingredient' expects exactly one ingredient, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_literal(inst.ingredients.at(0)) && !is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'ingredient' expects a literal ingredient, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case REPLY: { break; // checks will be performed by a transform below } case NEW: { const recipe& caller = get(Recipe, r); if (inst.ingredients.empty() || SIZE(inst.ingredients) > 2) { raise_error << maybe(caller.name) << "'new' requires one or two ingredients, but got " << to_string(inst) << '\n' << end(); break; } if (is_literal_string(inst.ingredients.at(0))) break; // End NEW Check Special-cases reagent type = inst.ingredients.at(0); if (!is_mu_type_literal(type)) { raise_error << maybe(caller.name) << "first ingredient of 'new' should be a type, but got " << type.original_string << '\n' << end(); break; } if (inst.products.empty()) { raise_error << maybe(caller.name) << "result of 'new' should never be ignored\n" << end(); break; } if (!product_of_new_is_valid(inst)) { raise_error << maybe(caller.name) << "product of 'new' has incorrect type: " << to_string(inst) << '\n' << end(); break; } break; } case ALLOCATE: { raise << "never call 'allocate' directly'; always use 'new'\n" << end(); break; } case ABANDON: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'abandon' requires one ingredient, but got '" << to_string(inst) << "'\n" << end(); break; } reagent types = inst.ingredients.at(0); canonize_type(types); if (!types.type || types.type->value != get(Type_ordinal, "address") || types.type->right->value != get(Type_ordinal, "shared")) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'abandon' should be an address:shared:___, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case TO_LOCATION_ARRAY: { const recipe& caller = get(Recipe, r); if (!is_shared_address_of_array_of_numbers(inst.products.at(0))) { raise_error << maybe(caller.name) << "product of 'to-location-array' has incorrect type: " << to_string(inst) << '\n' << end(); break; } break; } case BREAK: break; case BREAK_IF: break; case BREAK_UNLESS: break; case LOOP: break; case LOOP_IF: break; case LOOP_UNLESS: break; case RUN: { break; } case MEMORY_SHOULD_CONTAIN: { break; } case TRACE_SHOULD_CONTAIN: { break; } case TRACE_SHOULD_NOT_CONTAIN: { break; } case CHECK_TRACE_COUNT_FOR_LABEL: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'check-trace-for-label' requires exactly two ingredients, but got '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'check-trace-for-label' should be a number (count), but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } if (!is_literal_string(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "second ingredient of 'check-trace-for-label' should be a literal string (label), but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } break; } case NEXT_INGREDIENT_WITHOUT_TYPECHECKING: { break; } case CALL: { if (inst.ingredients.empty()) { raise_error << maybe(get(Recipe, r).name) << "'call' requires at least one ingredient (the recipe to call)\n" << end(); break; } if (!is_mu_recipe(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'call' should be a recipe, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case START_RUNNING: { if (inst.ingredients.empty()) { raise_error << maybe(get(Recipe, r).name) << "'start-running' requires at least one ingredient: the recipe to start running\n" << end(); break; } if (!is_mu_recipe(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'start-running' should be a recipe, but got " << to_string(inst.ingredients.at(0)) << '\n' << end(); break; } break; } case ROUTINE_STATE: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'routine-state' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'routine-state' should be a routine id generated by 'start-running', but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case RESTART: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'restart' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'restart' should be a routine id generated by 'start-running', but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case STOP: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'stop' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'stop' should be a routine id generated by 'start-running', but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case _DUMP_ROUTINES: { break; } case LIMIT_TIME: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'limit-time' requires exactly two ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'limit-time' should be a routine id generated by 'start-running', but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "second ingredient of 'limit-time' should be a number (of instructions to run for), but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } break; } case WAIT_FOR_LOCATION: { break; } case WAIT_FOR_ROUTINE: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'wait-for-routine' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'wait-for-routine' should be a routine id generated by 'start-running', but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case SWITCH: { break; } case RANDOM: { break; } case MAKE_RANDOM_NONDETERMINISTIC: { break; } case ROUND: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'round' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'round' should be a number, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case HASH: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'hash' takes exactly one ingredient rather than '" << to_string(inst) << "'\n" << end(); break; } break; } case HASH_OLD: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'hash_old' takes exactly one ingredient rather than '" << to_string(inst) << "'\n" << end(); break; } if (!is_mu_string(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "'hash_old' currently only supports strings (address:shared:array:character), but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case OPEN_CONSOLE: { break; } case CLOSE_CONSOLE: { break; } case CLEAR_DISPLAY: { break; } case SYNC_DISPLAY: { break; } case CLEAR_LINE_ON_DISPLAY: { break; } case PRINT_CHARACTER_TO_DISPLAY: { if (inst.ingredients.empty()) { raise_error << maybe(get(Recipe, r).name) << "'print-character-to-display' requires at least one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'print-character-to-display' should be a character, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } if (SIZE(inst.ingredients) > 1) { if (!is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "second ingredient of 'print-character-to-display' should be a foreground color number, but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } } if (SIZE(inst.ingredients) > 2) { if (!is_mu_number(inst.ingredients.at(2))) { raise_error << maybe(get(Recipe, r).name) << "third ingredient of 'print-character-to-display' should be a background color number, but got " << inst.ingredients.at(2).original_string << '\n' << end(); break; } } break; } case CURSOR_POSITION_ON_DISPLAY: { break; } case MOVE_CURSOR_ON_DISPLAY: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'move-cursor-on-display' requires two ingredients, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'move-cursor-on-display' should be a row number, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } if (!is_mu_number(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "second ingredient of 'move-cursor-on-display' should be a column number, but got " << inst.ingredients.at(1).original_string << '\n' << end(); break; } break; } case MOVE_CURSOR_DOWN_ON_DISPLAY: { break; } case MOVE_CURSOR_UP_ON_DISPLAY: { break; } case MOVE_CURSOR_RIGHT_ON_DISPLAY: { break; } case MOVE_CURSOR_LEFT_ON_DISPLAY: { break; } case DISPLAY_WIDTH: { break; } case DISPLAY_HEIGHT: { break; } case HIDE_CURSOR_ON_DISPLAY: { break; } case SHOW_CURSOR_ON_DISPLAY: { break; } case HIDE_DISPLAY: { break; } case SHOW_DISPLAY: { break; } case WAIT_FOR_SOME_INTERACTION: { break; } case CHECK_FOR_INTERACTION: { break; } case INTERACTIONS_LEFT: { break; } case CLEAR_DISPLAY_FROM: { break; } case SCREEN_SHOULD_CONTAIN: { break; } case SCREEN_SHOULD_CONTAIN_IN_COLOR: { break; } case _DUMP_SCREEN: { break; } case ASSUME_CONSOLE: { break; } case REPLACE_IN_CONSOLE: { break; } case _BROWSE_TRACE: { break; } case RUN_INTERACTIVE: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'run-interactive' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_string(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'run-interactive' should be a string, but got " << to_string(inst.ingredients.at(0)) << '\n' << end(); break; } break; } case _START_TRACKING_PRODUCTS: { break; } case _STOP_TRACKING_PRODUCTS: { break; } case _MOST_RECENT_PRODUCTS: { break; } case SAVE_ERRORS_WARNINGS: { break; } case SAVE_APP_TRACE: { break; } case _CLEANUP_RUN_INTERACTIVE: { break; } case RELOAD: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'reload' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } if (!is_mu_string(inst.ingredients.at(0))) { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'reload' should be a string, but got " << inst.ingredients.at(0).original_string << '\n' << end(); break; } break; } case RESTORE: { if (SIZE(inst.ingredients) != 1) { raise_error << maybe(get(Recipe, r).name) << "'restore' requires exactly one ingredient, but got " << to_string(inst) << '\n' << end(); break; } string filename; if (is_literal_string(inst.ingredients.at(0))) { ; } else if (is_mu_string(inst.ingredients.at(0))) { ; } else { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'restore' should be a string, but got " << to_string(inst.ingredients.at(0)) << '\n' << end(); break; } break; } case SAVE: { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "'save' requires exactly two ingredients, but got " << to_string(inst) << '\n' << end(); break; } if (is_literal_string(inst.ingredients.at(0))) { ; } else if (is_mu_string(inst.ingredients.at(0))) { ; } else { raise_error << maybe(get(Recipe, r).name) << "first ingredient of 'save' should be a string, but got " << to_string(inst.ingredients.at(0)) << '\n' << end(); break; } if (!is_mu_string(inst.ingredients.at(1))) { raise_error << maybe(get(Recipe, r).name) << "second ingredient of 'save' should be an address:array:character, but got " << to_string(inst.ingredients.at(1)) << '\n' << end(); break; } break; } // End Primitive Recipe Checks default: { // Defined Recipe Checks // not a primitive; check that it's present in the book of recipes if (!contains_key(Recipe, inst.operation)) { raise_error << maybe(get(Recipe, r).name) << "undefined operation in '" << to_string(inst) << "'\n" << end(); break; } // End Defined Recipe Checks } } finish_checking_instruction:; } } void test_copy_checks_reagent_count() { Trace_file = "copy_checks_reagent_count"; Hide_errors = true; run("recipe main [\n 1:number <- copy 34, 35\n]\n"); CHECK_TRACE_CONTENTS("error: ingredients and products should match in '1:number <- copy 34, 35'"); } void test_write_scalar_to_array_disallowed() { Trace_file = "write_scalar_to_array_disallowed"; Hide_errors = true; run("recipe main [\n 1:array:number <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("error: main: can't copy 34 to 1:array:number; types don't match"); } void test_write_scalar_to_array_disallowed_2() { Trace_file = "write_scalar_to_array_disallowed_2"; Hide_errors = true; run("recipe main [\n 1:number, 2:array:number <- copy 34, 35\n]\n"); CHECK_TRACE_CONTENTS("error: main: can't copy 35 to 2:array:number; types don't match"); } void test_write_scalar_to_address_disallowed() { Trace_file = "write_scalar_to_address_disallowed"; Hide_errors = true; run("recipe main [\n 1:address:number <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("error: main: can't copy 34 to 1:address:number; types don't match"); } void test_write_address_to_number_allowed() { Trace_file = "write_address_to_number_allowed"; Hide_errors = true; run("recipe main [\n 1:address:number <- copy 12/unsafe\n 2:number <- copy 1:address:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 12 in location 2"); CHECK_TRACE_COUNT("error", 0); } void test_write_boolean_to_number_allowed() { Trace_file = "write_boolean_to_number_allowed"; Hide_errors = true; run("recipe main [\n 1:boolean <- copy 1/true\n 2:number <- copy 1:boolean\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 2"); CHECK_TRACE_COUNT("error", 0); } // types_match with some leniency bool types_coercible(const reagent& to, const reagent& from) { if (types_match(to, from)) return true; if (is_mu_address(from) && is_mu_number(to)) return true; if (is_mu_boolean(from) && is_mu_number(to)) return true; // End types_coercible Special-cases return false; } bool types_match(const reagent& to, const reagent& from) { // to sidestep type-checking, use /unsafe in the source. // this will be highlighted in red inside vim. just for setting up some tests. if (is_unsafe(from)) return true; if (is_literal(from)) { if (is_mu_array(to)) return false; if (contains_type_ingredient_name(to)) return false; if (is_mu_recipe(to)) { if (!contains_key(Recipe, from.value)) { raise_error << "trying to store recipe " << from.name << " into " << to_string(to) << " but there's no such recipe\n" << end(); return false; } const recipe& rrhs = get(Recipe, from.value); const recipe& rlhs = from_reagent(to); for (long int i = 0; i < min(SIZE(rlhs.ingredients), SIZE(rrhs.ingredients)); ++i) { if (!types_match(rlhs.ingredients.at(i), rrhs.ingredients.at(i))) return false; } for (long int i = 0; i < min(SIZE(rlhs.products), SIZE(rrhs.products)); ++i) { if (!types_match(rlhs.products.at(i), rrhs.products.at(i))) return false; } return true; } // End Matching Types For Literal(to) // allow writing 0 to any address if (is_mu_address(to)) return from.name == "0"; if (!to.type) return false; if (to.type->value == get(Type_ordinal, "boolean")) return boolean_matches_literal(to, from); return size_of(to) == 1; // literals are always scalars } return types_strictly_match(to, from); } bool types_strictly_match_except_literal_against_boolean(const reagent& to, const reagent& from) { // to sidestep type-checking, use /unsafe in the source. // this will be highlighted in red inside vim. just for setting up some tests. if (is_literal(from) && to.type && to.type->value == get(Type_ordinal, "boolean")) return boolean_matches_literal(to, from); return types_strictly_match(to, from); } bool boolean_matches_literal(const reagent& to, const reagent& from) { if (!is_literal(from)) return false; if (!to.type) return false; if (to.type->value != get(Type_ordinal, "boolean")) return false; return from.name == "0" || from.name == "1"; } // copy arguments because later layers will want to make changes to them // without perturbing the caller bool types_strictly_match(reagent to, reagent from) { if (!canonize_type(to)) return false; if (!canonize_type(from)) return false; if (is_literal(from) && to.type->value == get(Type_ordinal, "number")) return true; // to sidestep type-checking, use /unsafe in the source. // this will be highlighted in red inside vim. just for setting up some tests. if (is_unsafe(from)) return true; // '_' never raises type error if (is_dummy(to)) return true; if (!to.type) return !from.type; return types_strictly_match(to.type, from.type); } // two types match if the second begins like the first // (trees perform the same check recursively on each subtree) bool types_strictly_match(type_tree* to, type_tree* from) { if (!to) return true; if (!from) return to->value == 0; if (to->value != from->value) return false; return types_strictly_match(to->left, from->left) && types_strictly_match(to->right, from->right); } bool is_unsafe(const reagent& r) { return has_property(r, "unsafe"); } bool is_mu_array(reagent r) { if (!canonize_type(r)) return false; if (!r.type) return false; if (is_literal(r)) return false; return r.type->value == get(Type_ordinal, "array"); } bool is_mu_address(reagent r) { if (!canonize_type(r)) return false; if (!r.type) return false; if (is_literal(r)) return false; return r.type->value == get(Type_ordinal, "address"); } bool is_mu_boolean(reagent r) { if (!r.type) return false; if (is_literal(r)) return false; return r.type->value == get(Type_ordinal, "boolean"); } bool is_mu_number(reagent r) { if (!canonize_type(r)) return false; if (!r.type) return false; if (is_literal(r)) { if (!r.type) return false; return r.type->name == "literal-fractional-number" || r.type->name == "literal"; } if (r.type->value == get(Type_ordinal, "character")) return true; // permit arithmetic on unicode code points return r.type->value == get(Type_ordinal, "number"); } bool is_mu_scalar(reagent r) { if (!r.type) return false; if (is_literal(r)) return !r.type || r.type->name != "literal-string"; if (is_mu_array(r)) return false; return size_of(r) == 1; } void test_add_literal() { Trace_file = "add_literal"; run("recipe main [\n 1:number <- add 23, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 57 in location 1"); } void test_add() { Trace_file = "add"; run("recipe main [\n 1:number <- copy 23\n 2:number <- copy 34\n 3:number <- add 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 57 in location 3"); } void test_add_multiple() { Trace_file = "add_multiple"; run("recipe main [\n 1:number <- add 3, 4, 5\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 12 in location 1"); } void test_add_checks_type() { Trace_file = "add_checks_type"; Hide_errors = true; run("recipe main [\n 1:number <- add 2:boolean, 1\n]\n"); CHECK_TRACE_CONTENTS("error: main: 'add' requires number ingredients, but got 2:boolean"); } void test_add_checks_return_type() { Trace_file = "add_checks_return_type"; Hide_errors = true; run("recipe main [\n 1:address:number <- add 2, 2\n]\n"); CHECK_TRACE_CONTENTS("error: main: 'add' should yield a number, but got 1:address:number"); } bool is_raw(const reagent& r) { return has_property(r, "raw"); } void test_subtract_literal() { Trace_file = "subtract_literal"; run("recipe main [\n 1:number <- subtract 5, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 3 in location 1"); } void test_subtract() { Trace_file = "subtract"; run("recipe main [\n 1:number <- copy 23\n 2:number <- copy 34\n 3:number <- subtract 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing -11 in location 3"); } void test_subtract_multiple() { Trace_file = "subtract_multiple"; run("recipe main [\n 1:number <- subtract 6, 3, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_multiply_literal() { Trace_file = "multiply_literal"; run("recipe main [\n 1:number <- multiply 2, 3\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 6 in location 1"); } void test_multiply() { Trace_file = "multiply"; run("recipe main [\n 1:number <- copy 4\n 2:number <- copy 6\n 3:number <- multiply 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 24 in location 3"); } void test_multiply_multiple() { Trace_file = "multiply_multiple"; run("recipe main [\n 1:number <- multiply 2, 3, 4\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 24 in location 1"); } void test_divide_literal() { Trace_file = "divide_literal"; run("recipe main [\n 1:number <- divide 8, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 1"); } void test_divide() { Trace_file = "divide"; run("recipe main [\n 1:number <- copy 27\n 2:number <- copy 3\n 3:number <- divide 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 9 in location 3"); } void test_divide_multiple() { Trace_file = "divide_multiple"; run("recipe main [\n 1:number <- divide 12, 3, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_divide_with_remainder_literal() { Trace_file = "divide_with_remainder_literal"; run("recipe main [\n 1:number, 2:number <- divide-with-remainder 9, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 1mem: storing 1 in location 2"); } void test_divide_with_remainder() { Trace_file = "divide_with_remainder"; run("recipe main [\n 1:number <- copy 27\n 2:number <- copy 11\n 3:number, 4:number <- divide-with-remainder 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 3mem: storing 5 in location 4"); } void test_divide_with_decimal_point() { Trace_file = "divide_with_decimal_point"; run("recipe main [\n 1:number <- divide 5, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2.5 in location 1"); } void test_divide_by_zero() { Trace_file = "divide_by_zero"; run("recipe main [\n 1:number <- divide 4, 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing inf in location 1"); } void test_divide_by_zero_2() { Trace_file = "divide_by_zero_2"; Hide_errors = true; run("recipe main [\n 1:number <- divide-with-remainder 4, 0\n]\n# integer division can't return floating-point infinity\n"); CHECK_TRACE_CONTENTS("error: main: divide by zero in '1:number <- divide-with-remainder 4, 0'"); } void test_shift_left_by_zero() { Trace_file = "shift_left_by_zero"; run("recipe main [\n 1:number <- shift-left 1, 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_shift_left_1() { Trace_file = "shift_left_1"; run("recipe main [\n 1:number <- shift-left 1, 4\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 16 in location 1"); } void test_shift_left_2() { Trace_file = "shift_left_2"; run("recipe main [\n 1:number <- shift-left 3, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 12 in location 1"); } void test_shift_left_by_negative() { Trace_file = "shift_left_by_negative"; Hide_errors = true; run("recipe main [\n 1:number <- shift-left 3, -1\n]\n"); CHECK_TRACE_CONTENTS("error: main: second ingredient can't be negative in '1:number <- shift-left 3, -1'"); } void test_shift_left_ignores_fractional_part() { Trace_file = "shift_left_ignores_fractional_part"; run("recipe main [\n 1:number <- divide 3, 2\n 2:number <- shift-left 1:number, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 2"); } void test_shift_right_by_zero() { Trace_file = "shift_right_by_zero"; run("recipe main [\n 1:number <- shift-right 1, 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_shift_right_1() { Trace_file = "shift_right_1"; run("recipe main [\n 1:number <- shift-right 1024, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 512 in location 1"); } void test_shift_right_2() { Trace_file = "shift_right_2"; run("recipe main [\n 1:number <- shift-right 3, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_shift_right_by_negative() { Trace_file = "shift_right_by_negative"; Hide_errors = true; run("recipe main [\n 1:number <- shift-right 4, -1\n]\n"); CHECK_TRACE_CONTENTS("error: main: second ingredient can't be negative in '1:number <- shift-right 4, -1'"); } void test_shift_right_ignores_fractional_part() { Trace_file = "shift_right_ignores_fractional_part"; run("recipe main [\n 1:number <- divide 3, 2\n 2:number <- shift-right 1:number, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 2"); } void test_and_bits_1() { Trace_file = "and_bits_1"; run("recipe main [\n 1:number <- and-bits 8, 3\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_and_bits_2() { Trace_file = "and_bits_2"; run("recipe main [\n 1:number <- and-bits 3, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_and_bits_3() { Trace_file = "and_bits_3"; run("recipe main [\n 1:number <- and-bits 14, 3\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_and_bits_negative() { Trace_file = "and_bits_negative"; run("recipe main [\n 1:number <- and-bits -3, 4\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 1"); } void test_or_bits_1() { Trace_file = "or_bits_1"; run("recipe main [\n 1:number <- or-bits 3, 8\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 11 in location 1"); } void test_or_bits_2() { Trace_file = "or_bits_2"; run("recipe main [\n 1:number <- or-bits 3, 10\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 11 in location 1"); } void test_or_bits_3() { Trace_file = "or_bits_3"; run("recipe main [\n 1:number <- or-bits 4, 6\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 6 in location 1"); } void test_xor_bits_1() { Trace_file = "xor_bits_1"; run("recipe main [\n 1:number <- xor-bits 3, 8\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 11 in location 1"); } void test_xor_bits_2() { Trace_file = "xor_bits_2"; run("recipe main [\n 1:number <- xor-bits 3, 10\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 9 in location 1"); } void test_xor_bits_3() { Trace_file = "xor_bits_3"; run("recipe main [\n 1:number <- xor-bits 4, 6\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_flip_bits_zero() { Trace_file = "flip_bits_zero"; run("recipe main [\n 1:number <- flip-bits 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing -1 in location 1"); } void test_flip_bits_negative() { Trace_file = "flip_bits_negative"; run("recipe main [\n 1:number <- flip-bits -1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_flip_bits_1() { Trace_file = "flip_bits_1"; run("recipe main [\n 1:number <- flip-bits 3\n]\n"); CHECK_TRACE_CONTENTS("mem: storing -4 in location 1"); } void test_flip_bits_2() { Trace_file = "flip_bits_2"; run("recipe main [\n 1:number <- flip-bits 12\n]\n"); CHECK_TRACE_CONTENTS("mem: storing -13 in location 1"); } void test_and() { Trace_file = "and"; run("recipe main [\n 1:boolean <- copy 1\n 2:boolean <- copy 0\n 3:boolean <- and 1:boolean, 2:boolean\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3"); } void test_and_2() { Trace_file = "and_2"; run("recipe main [\n 1:boolean <- and 1, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_and_multiple() { Trace_file = "and_multiple"; run("recipe main [\n 1:boolean <- and 1, 1, 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_and_multiple_2() { Trace_file = "and_multiple_2"; run("recipe main [\n 1:boolean <- and 1, 1, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_or() { Trace_file = "or"; run("recipe main [\n 1:boolean <- copy 1\n 2:boolean <- copy 0\n 3:boolean <- or 1:boolean, 2:boolean\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void test_or_2() { Trace_file = "or_2"; run("recipe main [\n 1:boolean <- or 0, 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_or_multiple() { Trace_file = "or_multiple"; run("recipe main [\n 1:boolean <- and 0, 0, 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_or_multiple_2() { Trace_file = "or_multiple_2"; run("recipe main [\n 1:boolean <- or 0, 0, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_not() { Trace_file = "not"; run("recipe main [\n 1:boolean <- copy 1\n 2:boolean <- not 1:boolean\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 2"); } void test_not_multiple() { Trace_file = "not_multiple"; run("recipe main [\n 1:boolean, 2:boolean, 3:boolean <- not 1, 0, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1mem: storing 1 in location 2mem: storing 0 in location 3"); } void test_jump_can_skip_instructions() { Trace_file = "jump_can_skip_instructions"; run("recipe main [\n jump 1:offset\n 1:number <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("run: jump 1:offset"); CHECK_TRACE_DOESNT_CONTAIN("run: 1:number <- copy 1"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 1 in location 1"); } void test_jump_backward() { Trace_file = "jump_backward"; run("recipe main [\n jump 1:offset # 0 -+\n jump 3:offset # | +-+ 1\n # \\/ /\\ |\n jump -2:offset # 2 +-->+ |\n] # \\/ 3\n"); CHECK_TRACE_CONTENTS("run: jump 1:offsetrun: jump -2:offsetrun: jump 3:offset"); } void test_jump_if() { Trace_file = "jump_if"; run("recipe main [\n jump-if 999, 1:offset\n 123:number <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("run: jump-if 999, 1:offsetrun: jumping to instruction 2"); CHECK_TRACE_DOESNT_CONTAIN("run: 1:number <- copy 1"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 1 in location 123"); } void test_jump_if_fallthrough() { Trace_file = "jump_if_fallthrough"; run("recipe main [\n jump-if 0, 1:offset\n 123:number <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("run: jump-if 0, 1:offsetrun: jump-if fell throughrun: 123:number <- copy 1mem: storing 1 in location 123"); } void test_jump_unless() { Trace_file = "jump_unless"; run("recipe main [\n jump-unless 0, 1:offset\n 123:number <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("run: jump-unless 0, 1:offsetrun: jumping to instruction 2"); CHECK_TRACE_DOESNT_CONTAIN("run: 123:number <- copy 1"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 1 in location 123"); } void test_jump_unless_fallthrough() { Trace_file = "jump_unless_fallthrough"; run("recipe main [\n jump-unless 999, 1:offset\n 123:number <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("run: jump-unless 999, 1:offsetrun: jump-unless fell throughrun: 123:number <- copy 1mem: storing 1 in location 123"); } void test_equal() { Trace_file = "equal"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 33\n 3:number <- equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: location 1 is 34mem: location 2 is 33mem: storing 0 in location 3"); } void test_equal_2() { Trace_file = "equal_2"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 34\n 3:number <- equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: location 1 is 34mem: location 2 is 34mem: storing 1 in location 3"); } void test_equal_multiple() { Trace_file = "equal_multiple"; run("recipe main [\n 1:number <- equal 34, 34, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_equal_multiple_2() { Trace_file = "equal_multiple_2"; run("recipe main [\n 1:number <- equal 34, 34, 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_greater_than() { Trace_file = "greater_than"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 33\n 3:boolean <- greater-than 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void test_greater_than_2() { Trace_file = "greater_than_2"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 34\n 3:boolean <- greater-than 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3"); } void test_greater_than_multiple() { Trace_file = "greater_than_multiple"; run("recipe main [\n 1:boolean <- greater-than 36, 35, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_greater_than_multiple_2() { Trace_file = "greater_than_multiple_2"; run("recipe main [\n 1:boolean <- greater-than 36, 35, 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_lesser_than() { Trace_file = "lesser_than"; run("recipe main [\n 1:number <- copy 32\n 2:number <- copy 33\n 3:boolean <- lesser-than 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void test_lesser_than_2() { Trace_file = "lesser_than_2"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 33\n 3:boolean <- lesser-than 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3"); } void test_lesser_than_multiple() { Trace_file = "lesser_than_multiple"; run("recipe main [\n 1:boolean <- lesser-than 34, 35, 36\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_lesser_than_multiple_2() { Trace_file = "lesser_than_multiple_2"; run("recipe main [\n 1:boolean <- lesser-than 34, 35, 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_greater_or_equal() { Trace_file = "greater_or_equal"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 33\n 3:boolean <- greater-or-equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void test_greater_or_equal_2() { Trace_file = "greater_or_equal_2"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 34\n 3:boolean <- greater-or-equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void test_greater_or_equal_3() { Trace_file = "greater_or_equal_3"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 35\n 3:boolean <- greater-or-equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3"); } void test_greater_or_equal_multiple() { Trace_file = "greater_or_equal_multiple"; run("recipe main [\n 1:boolean <- greater-or-equal 36, 35, 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_greater_or_equal_multiple_2() { Trace_file = "greater_or_equal_multiple_2"; run("recipe main [\n 1:boolean <- greater-or-equal 36, 35, 36\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_lesser_or_equal() { Trace_file = "lesser_or_equal"; run("recipe main [\n 1:number <- copy 32\n 2:number <- copy 33\n 3:boolean <- lesser-or-equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void test_lesser_or_equal_2() { Trace_file = "lesser_or_equal_2"; run("recipe main [\n 1:number <- copy 33\n 2:number <- copy 33\n 3:boolean <- lesser-or-equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void test_lesser_or_equal_3() { Trace_file = "lesser_or_equal_3"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 33\n 3:boolean <- lesser-or-equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3"); } void test_lesser_or_equal_multiple() { Trace_file = "lesser_or_equal_multiple"; run("recipe main [\n 1:boolean <- lesser-or-equal 34, 35, 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1"); } void test_lesser_or_equal_multiple_2() { Trace_file = "lesser_or_equal_multiple_2"; run("recipe main [\n 1:boolean <- lesser-or-equal 34, 35, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); } void test_trace() { Trace_file = "trace"; run("recipe main [\n trace 1, [foo], [this is a trace in mu]\n]\n"); CHECK_TRACE_CONTENTS("foo: this is a trace in mu"); } void test_stash_literal_string() { Trace_file = "stash_literal_string"; run("recipe main [\n stash [foo]\n]\n"); CHECK_TRACE_CONTENTS("app: foo"); } void test_stash_literal_number() { Trace_file = "stash_literal_number"; run("recipe main [\n stash [foo:], 4\n]\n"); CHECK_TRACE_CONTENTS("app: foo: 4"); } void test_stash_number() { Trace_file = "stash_number"; run("recipe main [\n 1:number <- copy 34\n stash [foo:], 1:number\n]\n"); CHECK_TRACE_CONTENTS("app: foo: 34"); } string print_mu(const reagent& r, const vector& data) { if (is_literal(r)) return r.name+' '; if (is_mu_string(r)) { assert(scalar(data)); return read_mu_string(data.at(0))+' '; } // End print Special-cases(reagent r, data) ostringstream out; for (long long i = 0; i < SIZE(data); ++i) out << no_scientific(data.at(i)) << ' '; return out.str(); } void test_assert() { Trace_file = "assert"; Hide_errors = true; // '%' lines insert arbitrary C code into tests before calling 'run' with the lines below. Must be immediately after :(scenario) line. run("recipe main [\n assert 0, [this is an assert in mu]\n]\n"); CHECK_TRACE_CONTENTS("error: this is an assert in mu"); } void test_copy_multiple_locations() { Trace_file = "copy_multiple_locations"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 35\n 3:point <- copy 1:point/unsafe\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 3mem: storing 35 in location 4"); } void test_copy_checks_size() { Trace_file = "copy_checks_size"; Hide_errors = true; run("recipe main [\n 2:point <- copy 1:number\n]\n"); CHECK_TRACE_CONTENTS("error: main: can't copy 1:number to 2:point; types don't match"); } void test_copy_handles_nested_container_elements() { Trace_file = "copy_handles_nested_container_elements"; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 14:number <- copy 36\n 15:point-number <- copy 12:point-number/unsafe\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 36 in location 17"); } void test_compare_multiple_locations() { Trace_file = "compare_multiple_locations"; run("recipe main [\n 1:number <- copy 34 # first\n 2:number <- copy 35\n 3:number <- copy 36\n 4:number <- copy 34 # second\n 5:number <- copy 35\n 6:number <- copy 36\n 7:boolean <- equal 1:point-number/raw, 4:point-number/unsafe\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 7"); } void test_compare_multiple_locations_2() { Trace_file = "compare_multiple_locations_2"; run("recipe main [\n 1:number <- copy 34 # first\n 2:number <- copy 35\n 3:number <- copy 36\n 4:number <- copy 34 # second\n 5:number <- copy 35\n 6:number <- copy 37 # different\n 7:boolean <- equal 1:point-number/raw, 4:point-number/unsafe\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 7"); } void test_stash_container() { Trace_file = "stash_container"; run("recipe main [\n 1:number <- copy 34 # first\n 2:number <- copy 35\n 3:number <- copy 36\n stash [foo:], 1:point-number/raw\n]\n"); CHECK_TRACE_CONTENTS("app: foo: 34 35 36"); } void test_get() { Trace_file = "get"; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 15:number <- get 12:point/raw, 1:offset # unsafe\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 35 in location 15"); } const reagent element_type(const reagent& canonized_base, long long int offset_value) { assert(offset_value >= 0); assert(contains_key(Type, canonized_base.type->value)); assert(!get(Type, canonized_base.type->value).name.empty()); const type_info& info = get(Type, canonized_base.type->value); assert(info.kind == CONTAINER); reagent element = info.elements.at(offset_value); if (contains_type_ingredient(element)) { if (!canonized_base.type->right) raise_error << "illegal type " << names_to_string(canonized_base.type) << " seems to be missing a type ingredient or three\n" << end(); replace_type_ingredients(element.type, canonized_base.type->right, info); } // End element_type Special-cases return element; } void test_get_handles_nested_container_elements() { Trace_file = "get_handles_nested_container_elements"; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 14:number <- copy 36\n 15:number <- get 12:point-number/raw, 1:offset # unsafe\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 36 in location 15"); } void test_get_out_of_bounds() { Trace_file = "get_out_of_bounds"; Hide_errors = true; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 14:number <- copy 36\n get 12:point-number/raw, 2:offset # point-number occupies 3 locations but has only 2 fields; out of bounds\n]\n"); CHECK_TRACE_CONTENTS("error: main: invalid offset 2 for point-number"); } void test_get_out_of_bounds_2() { Trace_file = "get_out_of_bounds_2"; Hide_errors = true; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 14:number <- copy 36\n get 12:point-number/raw, -1:offset\n]\n"); CHECK_TRACE_CONTENTS("error: main: invalid offset -1 for point-number"); } void test_get_product_type_mismatch() { Trace_file = "get_product_type_mismatch"; Hide_errors = true; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 14:number <- copy 36\n 15:address:number <- get 12:point-number/raw, 1:offset\n]\n"); CHECK_TRACE_CONTENTS("error: main: 'get 12:point-number/raw, 1:offset' should write to number but 15 has type (address number)"); } void test_get_without_product() { Trace_file = "get_without_product"; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n get 12:point/raw, 1:offset # unsafe\n]\n# just don't die\n"); } void test_get_address() { Trace_file = "get_address"; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 15:address:number <- get-address 12:point/raw, 1:offset # unsafe\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 13 in location 15"); } void test_get_address_out_of_bounds() { Trace_file = "get_address_out_of_bounds"; Hide_errors = true; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 14:number <- copy 36\n get-address 12:point-number/raw, 2:offset # point-number occupies 3 locations but has only 2 fields; out of bounds\n]\n"); CHECK_TRACE_CONTENTS("error: main: invalid offset 2 for point-number"); } void test_get_address_out_of_bounds_2() { Trace_file = "get_address_out_of_bounds_2"; Hide_errors = true; run("recipe main [\n 12:number <- copy 34\n 13:number <- copy 35\n 14:number <- copy 36\n get-address 12:point-number/raw, -1:offset\n]\n"); CHECK_TRACE_CONTENTS("error: main: invalid offset -1 for point-number"); } void test_get_address_product_type_mismatch() { Trace_file = "get_address_product_type_mismatch"; Hide_errors = true; run("container boolbool [\n x:boolean\n y:boolean\n]\nrecipe main [\n 12:boolean <- copy 1\n 13:boolean <- copy 0\n 15:boolean <- get-address 12:boolbool, 1:offset\n]\n"); CHECK_TRACE_CONTENTS("error: main: 'get-address 12:boolbool, 1:offset' should write to (address boolean) but 15 has type boolean"); } void test_container() { Trace_file = "container"; load("container foo [\n x:number\n y:number\n]\n"); CHECK_TRACE_CONTENTS("parse: --- defining container fooparse: element: x: \"number\"parse: element: y: \"number\""); } void test_container_use_before_definition() { Trace_file = "container_use_before_definition"; load("container foo [\n x:number\n y:bar\n]\ncontainer bar [\n x:number\n y:number\n]\n"); CHECK_TRACE_CONTENTS("parse: --- defining container fooparse: type number: 1000parse: element: x: \"number\"parse: element: y: \"bar\"parse: --- defining container barparse: type number: 1001parse: element: x: \"number\"parse: element: y: \"number\""); } void insert_container(const string& command, kind_of_type kind, istream& in) { skip_whitespace_but_not_newline(in); string name = next_word(in); if (name.find(':') != string::npos) { trace(9999, "parse") << "container has type ingredients; parsing" << end(); read_type_ingredients(name); } // End container Name Refinements trace(9991, "parse") << "--- defining " << command << ' ' << name << end(); if (!contains_key(Type_ordinal, name) || get(Type_ordinal, name) == 0) { put(Type_ordinal, name, Next_type_ordinal++); } trace(9999, "parse") << "type number: " << get(Type_ordinal, name) << end(); skip_bracket(in, "'container' must begin with '['"); type_info& info = get_or_insert(Type, get(Type_ordinal, name)); Recently_added_types.push_back(get(Type_ordinal, name)); info.name = name; info.kind = kind; while (has_data(in)) { skip_whitespace_and_comments(in); string element = next_word(in); if (element == "]") break; info.elements.push_back(reagent(element)); replace_unknown_types_with_unique_ordinals(info.elements.back().type, info); trace(9993, "parse") << " element: " << to_string(info.elements.back()) << end(); { const type_tree* type = info.elements.back().type; if (type->name == "array") { if (!type->right) { raise_error << "container '" << name << "' doesn't specify type of array elements for " << info.elements.back().name << '\n' << end(); continue; } if (!type->right->right) { // array has no length raise_error << "container '" << name << "' cannot determine size of element " << info.elements.back().name << '\n' << end(); continue; } } } // End Load Container Element Definition } info.size = SIZE(info.elements); } void replace_unknown_types_with_unique_ordinals(type_tree* type, const type_info& info) { if (!type) return; if (!type->name.empty()) { if (contains_key(Type_ordinal, type->name)) { type->value = get(Type_ordinal, type->name); } else if (is_integer(type->name)) { // sometimes types will contain non-type tags, like numbers for the size of an array type->value = 0; } // check for use of type ingredients else if (is_type_ingredient_name(type->name)) { type->value = get(info.type_ingredient_names, type->name); } // End insert_container Special-cases else if (type->name != "->") { // used in recipe types put(Type_ordinal, type->name, Next_type_ordinal++); type->value = get(Type_ordinal, type->name); } } replace_unknown_types_with_unique_ordinals(type->left, info); replace_unknown_types_with_unique_ordinals(type->right, info); } void skip_bracket(istream& in, string message) { skip_whitespace_and_comments(in); if (in.get() != '[') raise_error << message << '\n' << end(); } void test_container_define_twice() { Trace_file = "container_define_twice"; run("container foo [\n x:number\n]\ncontainer foo [\n y:number\n]\nrecipe main [\n 1:number <- copy 34\n 2:number <- copy 35\n 3:number <- get 1:foo, 0:offset\n 4:number <- get 1:foo, 1:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 3mem: storing 35 in location 4"); } void test_run_complains_on_unknown_types() { Trace_file = "run_complains_on_unknown_types"; Hide_errors = true; run("recipe main [\n # integer is not a type\n 1:integer <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("error: main: unknown type integer in '1:integer <- copy 0'"); } void test_run_allows_type_definition_after_use() { Trace_file = "run_allows_type_definition_after_use"; Hide_errors = true; run("recipe main [\n 1:bar <- copy 0/unsafe\n]\ncontainer bar [\n x:number\n]\n"); CHECK_TRACE_COUNT("error", 0); } void check_or_set_invalid_types(const recipe_ordinal r) { recipe& caller = get(Recipe, r); trace(9991, "transform") << "--- check for invalid types in recipe " << caller.name << end(); for (long long int index = 0; index < SIZE(caller.steps); ++index) { instruction& inst = caller.steps.at(index); for (long long int i = 0; i < SIZE(inst.ingredients); ++i) check_or_set_invalid_types(inst.ingredients.at(i).type, maybe(caller.name), "'"+to_string(inst)+"'"); for (long long int i = 0; i < SIZE(inst.products); ++i) check_or_set_invalid_types(inst.products.at(i).type, maybe(caller.name), "'"+to_string(inst)+"'"); } for (long long int i = 0; i < SIZE(caller.ingredients); ++i) check_or_set_invalid_types(caller.ingredients.at(i).type, maybe(caller.name), "recipe header ingredient"); for (long long int i = 0; i < SIZE(caller.products); ++i) check_or_set_invalid_types(caller.products.at(i).type, maybe(caller.name), "recipe header product"); // End check_or_set_invalid_types } void check_or_set_invalid_types(type_tree* type, const string& block, const string& name) { if (!type) return; // will throw a more precise error elsewhere if (type->value >= START_TYPE_INGREDIENTS && (type->value - START_TYPE_INGREDIENTS) < SIZE(get(Type, type->value).type_ingredient_names)) return; // End Container Type Checks if (type->value == 0) return; if (!contains_key(Type, type->value)) { assert(!type->name.empty()); if (contains_key(Type_ordinal, type->name)) type->value = get(Type_ordinal, type->name); else raise_error << block << "unknown type " << type->name << " in " << name << '\n' << end(); } check_or_set_invalid_types(type->left, block, name); check_or_set_invalid_types(type->right, block, name); } void test_container_unknown_field() { Trace_file = "container_unknown_field"; Hide_errors = true; run("container foo [\n x:number\n y:bar\n]\n"); CHECK_TRACE_CONTENTS("error: foo: unknown type in y"); } void test_read_container_with_bracket_in_comment() { Trace_file = "read_container_with_bracket_in_comment"; run("container foo [\n x:number\n # ']' in comment\n y:number\n]\n"); CHECK_TRACE_CONTENTS("parse: --- defining container fooparse: element: x: \"number\"parse: element: y: \"number\""); } void check_container_field_types() { for (map::iterator p = Type.begin(); p != Type.end(); ++p) { const type_info& info = p->second; if (!info.type_ingredient_names.empty()) continue; // Check Container Field Types(info) for (long long int i = 0; i < SIZE(info.elements); ++i) check_invalid_types(info.elements.at(i).type, maybe(info.name), info.elements.at(i).name); } } void check_invalid_types(const type_tree* type, const string& block, const string& name) { if (!type) return; // will throw a more precise error elsewhere if (type->value >= START_TYPE_INGREDIENTS && (type->value - START_TYPE_INGREDIENTS) < SIZE(get(Type, type->value).type_ingredient_names)) return; // End Container Type Checks2 if (type->value == 0) { assert(!type->left && !type->right); return; } if (!contains_key(Type, type->value)) raise_error << block << "unknown type in " << name << '\n' << end(); check_invalid_types(type->left, block, name); check_invalid_types(type->right, block, name); } void test_merge() { Trace_file = "merge"; run("container foo [\n x:number\n y:number\n]\nrecipe main [\n 1:foo <- merge 3, 4\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 3 in location 1mem: storing 4 in location 2"); } void test_merge_check() { Trace_file = "merge_check"; Hide_errors = true; run("recipe main [\n 1:point <- merge 3, 4\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_missing_element() { Trace_file = "merge_check_missing_element"; Hide_errors = true; run("recipe main [\n 1:point <- merge 3\n]\n"); CHECK_TRACE_CONTENTS("error: main: too few ingredients in '1:point <- merge 3'"); } void test_merge_check_extra_element() { Trace_file = "merge_check_extra_element"; Hide_errors = true; run("recipe main [\n 1:point <- merge 3, 4, 5\n]\n"); CHECK_TRACE_CONTENTS("error: main: too many ingredients in '1:point <- merge 3, 4, 5'"); } void test_merge_check_recursive_containers() { Trace_file = "merge_check_recursive_containers"; Hide_errors = true; run("recipe main [\n 1:point <- merge 3, 4\n 1:point-number <- merge 1:point, 5\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_recursive_containers_2() { Trace_file = "merge_check_recursive_containers_2"; Hide_errors = true; run("recipe main [\n 1:point <- merge 3, 4\n 2:point-number <- merge 1:point\n]\n"); CHECK_TRACE_CONTENTS("error: main: too few ingredients in '2:point-number <- merge 1:point'"); } void test_merge_check_recursive_containers_3() { Trace_file = "merge_check_recursive_containers_3"; Hide_errors = true; run("recipe main [\n 1:point-number <- merge 3, 4, 5\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_recursive_containers_4() { Trace_file = "merge_check_recursive_containers_4"; Hide_errors = true; run("recipe main [\n 1:point-number <- merge 3, 4\n]\n"); CHECK_TRACE_CONTENTS("error: main: too few ingredients in '1:point-number <- merge 3, 4'"); } void check_merge_calls(const recipe_ordinal r) { const recipe& caller = get(Recipe, r); trace(9991, "transform") << "--- type-check merge instructions in recipe " << caller.name << end(); for (long long int i = 0; i < SIZE(caller.steps); ++i) { const instruction& inst = caller.steps.at(i); if (inst.name != "merge") continue; if (SIZE(inst.products) != 1) { raise_error << maybe(caller.name) << "'merge' should yield a single product in '" << to_string(inst) << "'\n" << end(); continue; } reagent product = inst.products.at(0); // Update product While Type-checking Merge if (!canonize_type(product)) continue; type_ordinal product_type = product.type->value; if (product_type == 0 || !contains_key(Type, product_type)) { raise_error << maybe(caller.name) << "'merge' should yield a container in '" << to_string(inst) << "'\n" << end(); continue; } const type_info& info = get(Type, product_type); if (info.kind != CONTAINER && info.kind != EXCLUSIVE_CONTAINER) { raise_error << maybe(caller.name) << "'merge' should yield a container in '" << to_string(inst) << "'\n" << end(); continue; } check_merge_call(inst.ingredients, product, caller, inst); } } void check_merge_call(const vector& ingredients, const reagent& product, const recipe& caller, const instruction& inst) { long long int ingredient_index = 0; merge_check_state state; state.data.push(merge_check_point(product, 0)); while (true) { assert(!state.data.empty()); trace(9999, "transform") << ingredient_index << " vs " << SIZE(ingredients) << end(); if (ingredient_index >= SIZE(ingredients)) { raise_error << maybe(caller.name) << "too few ingredients in '" << to_string(inst) << "'\n" << end(); return; } reagent& container = state.data.top().container; type_info& container_info = get(Type, container.type->value); switch (container_info.kind) { case CONTAINER: { reagent expected_ingredient = element_type(container, state.data.top().container_element_index); trace(9999, "transform") << "checking container " << to_string(container) << " || " << to_string(expected_ingredient) << " vs ingredient " << ingredient_index << end(); // if the current element is the ingredient we expect, move on to the next element/ingredient if (types_coercible(expected_ingredient, ingredients.at(ingredient_index))) { ++ingredient_index; ++state.data.top().container_element_index; while (state.data.top().container_element_index >= SIZE(get(Type, state.data.top().container.type->value).elements)) { state.data.pop(); if (state.data.empty()) { if (ingredient_index < SIZE(ingredients)) raise_error << maybe(caller.name) << "too many ingredients in '" << to_string(inst) << "'\n" << end(); return; } ++state.data.top().container_element_index; } } // if not, maybe it's a field of the current element else { // no change to ingredient_index state.data.push(merge_check_point(expected_ingredient, 0)); } break; } case EXCLUSIVE_CONTAINER: { assert(state.data.top().container_element_index == 0); trace(9999, "transform") << "checking exclusive container " << to_string(container) << " vs ingredient " << ingredient_index << end(); if (!is_literal(ingredients.at(ingredient_index))) { raise_error << maybe(caller.name) << "ingredient " << ingredient_index << " of 'merge' should be a literal, for the tag of exclusive-container " << container_info.name << '\n' << end(); return; } reagent ingredient = ingredients.at(ingredient_index); // unnecessary copy just to keep this function from modifying caller populate_value(ingredient); if (ingredient.value >= SIZE(container_info.elements)) { raise_error << maybe(caller.name) << "invalid tag at " << ingredient_index << " for " << container_info.name << " in '" << to_string(inst) << '\n' << end(); return; } reagent variant = variant_type(container, ingredient.value); trace(9999, "transform") << "tag: " << ingredient.value << end(); // replace union with its variant state.data.pop(); state.data.push(merge_check_point(variant, 0)); ++ingredient_index; break; } // End valid_merge Cases default: { if (!types_coercible(container, ingredients.at(ingredient_index))) { raise_error << maybe(caller.name) << "incorrect type of ingredient " << ingredient_index << " in '" << to_string(inst) << "'\n" << end(); cerr << " expected " << debug_string(container) << '\n'; cerr << " got " << debug_string(ingredients.at(ingredient_index)) << '\n'; return; } ++ingredient_index; // ++state.data.top().container_element_index; // unnecessary, but wouldn't do any harm do { state.data.pop(); if (state.data.empty()) { if (ingredient_index < SIZE(ingredients)) raise_error << maybe(caller.name) << "too many ingredients in '" << to_string(inst) << "'\n" << end(); return; } ++state.data.top().container_element_index; } while (state.data.top().container_element_index >= SIZE(get(Type, state.data.top().container.type->value).elements)); } } } // never gets here assert(false); } void test_merge_check_product() { Trace_file = "merge_check_product"; Hide_errors = true; run("recipe main [\n 1:number <- merge 3\n]\n"); CHECK_TRACE_CONTENTS("error: main: 'merge' should yield a container in '1:number <- merge 3'"); } void test_copy_indirect() { Trace_file = "copy_indirect"; run("recipe main [\n 1:address:number <- copy 2/unsafe\n 2:number <- copy 34\n # This loads location 1 as an address and looks up *that* location.\n 3:number <- copy 1:address:number/lookup\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 3"); } void test_store_indirect() { Trace_file = "store_indirect"; run("recipe main [\n 1:address:number <- copy 2/unsafe\n 1:address:number/lookup <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 2"); } void test_store_to_0_fails() { Trace_file = "store_to_0_fails"; Hide_errors = true; run("recipe main [\n 1:address:number <- copy 0\n 1:address:number/lookup <- copy 34\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 34 in location 0"); run(""); CHECK_TRACE_CONTENTS("error: can't write to location 0 in '1:address:number/lookup <- copy 34'"); } void canonize(reagent& x) { if (is_literal(x)) return; absolutize(x); // End canonize(x) Special-cases while (has_property(x, "lookup")) lookup_memory(x); } void lookup_memory(reagent& x) { if (!x.type || x.type->value != get(Type_ordinal, "address")) { raise_error << maybe(current_recipe_name()) << "tried to /lookup " << x.original_string << " but it isn't an address\n" << end(); return; } // compute value if (x.value == 0) { raise_error << maybe(current_recipe_name()) << "tried to /lookup 0\n" << end(); return; } trace(9999, "mem") << "location " << x.value << " is " << no_scientific(get_or_insert(Memory, x.value)) << end(); x.set_value(get_or_insert(Memory, x.value)); drop_from_type(x, "address"); if (x.type->name == "shared") { trace(9999, "mem") << "skipping refcount at " << x.value << end(); x.set_value(x.value+1); // skip refcount drop_from_type(x, "shared"); } // End Drop Address In lookup_memory(x) drop_one_lookup(x); } void test_canonize_non_pointer_fails_without_crashing() { Trace_file = "canonize_non_pointer_fails_without_crashing"; Hide_errors = true; run("recipe foo [\n 1:address:number <- get-address *p, x:offset\n]\n# don't crash\n"); } bool canonize_type(reagent& r) { while (has_property(r, "lookup")) { if (!r.type || r.type->value != get(Type_ordinal, "address")) { raise_error << "can't lookup non-address: " << to_string(r) << ": " << to_string(r.type) << '\n' << end(); return false; } drop_from_type(r, "address"); if (r.type->name == "shared") { drop_from_type(r, "shared"); } // End Drop Address In canonize_type(r) drop_one_lookup(r); } return true; } void drop_from_type(reagent& r, string expected_type) { if (!r.type) { raise_error << "can't drop " << expected_type << " from " << to_string(r) << '\n' << end(); return; } if (r.type->name != expected_type) { raise_error << "can't drop2 " << expected_type << " from " << to_string(r) << '\n' << end(); return; } type_tree* tmp = r.type; r.type = tmp->right; tmp->right = NULL; delete tmp; } void drop_one_lookup(reagent& r) { for (vector >::iterator p = r.properties.begin(); p != r.properties.end(); ++p) { if (p->first == "lookup") { r.properties.erase(p); return; } } assert(false); } void test_get_indirect() { Trace_file = "get_indirect"; run("recipe main [\n 1:number <- copy 2\n 2:number <- copy 34\n 3:number <- copy 35\n 4:number <- get 1:address:point/lookup, 0:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 4"); } void test_get_indirect2() { Trace_file = "get_indirect2"; run("recipe main [\n 1:number <- copy 2\n 2:number <- copy 34\n 3:number <- copy 35\n 4:address:number <- copy 5/unsafe\n *4:address:number <- get 1:address:point/lookup, 0:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 5"); } void test_include_nonlookup_properties() { Trace_file = "include_nonlookup_properties"; run("recipe main [\n 1:number <- copy 2\n 2:number <- copy 34\n 3:number <- copy 35\n 4:number <- get 1:address:point/lookup/foo, 0:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 4"); } void test_get_address_indirect() { Trace_file = "get_address_indirect"; run("# 'get' can read from container address\nrecipe main [\n 1:number <- copy 2\n 2:number <- copy 34\n 3:number <- copy 35\n 4:address:number <- get-address 1:address:point/lookup, 0:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 4"); } void test_lookup_abbreviation() { Trace_file = "lookup_abbreviation"; run("recipe main [\n 1:address:number <- copy 2/unsafe\n 2:number <- copy 34\n 3:number <- copy *1:address:number\n]\n"); CHECK_TRACE_CONTENTS("parse: ingredient: 1: (\"address\" \"number\"), {\"lookup\": ()}mem: storing 34 in location 3"); } void test_create_array() { Trace_file = "create_array"; run("recipe main [\n # create an array occupying locations 1 (for the size) and 2-4 (for the elements)\n 1:array:number:3 <- create-array\n]\n"); CHECK_TRACE_CONTENTS("run: creating array of size 4"); } void test_copy_array() { Trace_file = "copy_array"; run("# Arrays can be copied around with a single instruction just like numbers,\n# no matter how large they are.\nrecipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:array:number <- copy 1:array:number:3\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 3 in location 5mem: storing 14 in location 6mem: storing 15 in location 7mem: storing 16 in location 8"); } void test_copy_array_indirect() { Trace_file = "copy_array_indirect"; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:address:array:number <- copy 1/unsafe\n 6:array:number <- copy *5:address:array:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 3 in location 6mem: storing 14 in location 7mem: storing 15 in location 8mem: storing 16 in location 9"); } void test_stash_array() { Trace_file = "stash_array"; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n stash [foo:], 1:array:number:3\n]\n"); CHECK_TRACE_CONTENTS("app: foo: 3 14 15 16"); } void test_container_contains_array() { Trace_file = "container_contains_array"; Hide_errors = true; run("container foo [\n x:array:number:3\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_container_warns_on_dynamic_array_element() { Trace_file = "container_warns_on_dynamic_array_element"; Hide_errors = true; run("container foo [\n x:array:number\n]\n"); CHECK_TRACE_CONTENTS("error: container 'foo' cannot determine size of element x"); } void test_index() { Trace_file = "index"; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- index 1:array:number:3, 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 14 in location 5"); } void test_index_direct_offset() { Trace_file = "index_direct_offset"; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- copy 0\n 6:number <- index 1:array:number, 5:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 14 in location 6"); } type_tree* array_element(const type_tree* type) { return type->right; } void test_index_indirect() { Trace_file = "index_indirect"; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:address:array:number <- copy 1/unsafe\n 6:number <- index *5:address:array:number, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 15 in location 6"); } void test_index_out_of_bounds() { Trace_file = "index_out_of_bounds"; Hide_errors = true; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- copy 14\n 6:number <- copy 15\n 7:number <- copy 16\n 8:address:array:point <- copy 1/unsafe\n index *8:address:array:point, 4 # less than size of array in locations, but larger than its length in elements\n]\n"); CHECK_TRACE_CONTENTS("error: main: invalid index 4"); } void test_index_out_of_bounds_2() { Trace_file = "index_out_of_bounds_2"; Hide_errors = true; run("recipe main [\n 1:array:point:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- copy 14\n 6:number <- copy 15\n 7:number <- copy 16\n 8:address:array:point <- copy 1/unsafe\n index *8:address:array:point, -1\n]\n"); CHECK_TRACE_CONTENTS("error: main: invalid index -1"); } void test_index_product_type_mismatch() { Trace_file = "index_product_type_mismatch"; Hide_errors = true; run("recipe main [\n 1:array:point:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- copy 14\n 6:number <- copy 15\n 7:number <- copy 16\n 8:address:array:point <- copy 1/unsafe\n 9:number <- index *8:address:array:point, 0\n]\n"); CHECK_TRACE_CONTENTS("error: main: 'index' on *8:address:array:point can't be saved in 9:number; type should be point"); } void test_index_without_product() { Trace_file = "index_without_product"; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n index 1:array:number:3, 0\n]\n# just don't die\n"); } void test_index_address() { Trace_file = "index_address"; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:address:number <- index-address 1:array:number, 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 5"); } void test_index_address_out_of_bounds() { Trace_file = "index_address_out_of_bounds"; Hide_errors = true; run("recipe main [\n 1:array:point:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- copy 14\n 6:number <- copy 15\n 7:number <- copy 16\n 8:address:array:point <- copy 1/unsafe\n index-address *8:address:array:point, 4 # less than size of array in locations, but larger than its length in elements\n]\n"); CHECK_TRACE_CONTENTS("error: main: invalid index 4"); } void test_index_address_out_of_bounds_2() { Trace_file = "index_address_out_of_bounds_2"; Hide_errors = true; run("recipe main [\n 1:array:point:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- copy 14\n 6:number <- copy 15\n 7:number <- copy 16\n 8:address:array:point <- copy 1/unsafe\n index-address *8:address:array:point, -1\n]\n"); CHECK_TRACE_CONTENTS("error: main: invalid index -1"); } void test_index_address_product_type_mismatch() { Trace_file = "index_address_product_type_mismatch"; Hide_errors = true; run("recipe main [\n 1:array:point:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- copy 14\n 6:number <- copy 15\n 7:number <- copy 16\n 8:address:array:point <- copy 1/unsafe\n 9:address:number <- index-address *8:address:array:point, 0\n]\n"); CHECK_TRACE_CONTENTS("error: main: 'index' on *8:address:array:point can't be saved in 9:address:number; type should be (address point)"); } void test_array_length() { Trace_file = "array_length"; run("recipe main [\n 1:array:number:3 <- create-array\n 2:number <- copy 14\n 3:number <- copy 15\n 4:number <- copy 16\n 5:number <- length 1:array:number:3\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 3 in location 5"); } bool is_mu_string(const reagent& x) { return x.type && x.type->value == get(Type_ordinal, "address") && x.type->right && x.type->right->value == get(Type_ordinal, "shared") && x.type->right->right && x.type->right->right->value == get(Type_ordinal, "array") && x.type->right->right->right && x.type->right->right->right->value == get(Type_ordinal, "character") && x.type->right->right->right->right == NULL; } void test_copy_exclusive_container() { Trace_file = "copy_exclusive_container"; run("# Copying exclusive containers copies all their contents and an extra location for the tag.\nrecipe main [\n 1:number <- copy 1 # 'point' variant\n 2:number <- copy 34\n 3:number <- copy 35\n 4:number-or-point <- copy 1:number-or-point/unsafe\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 4mem: storing 34 in location 5mem: storing 35 in location 6"); } void test_maybe_convert() { Trace_file = "maybe_convert"; run("recipe main [\n 12:number <- copy 1\n 13:number <- copy 35\n 14:number <- copy 36\n 20:address:point <- maybe-convert 12:number-or-point/unsafe, 1:variant\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 13 in location 20"); } void test_maybe_convert_fail() { Trace_file = "maybe_convert_fail"; run("recipe main [\n 12:number <- copy 1\n 13:number <- copy 35\n 14:number <- copy 36\n 20:address:number <- maybe-convert 12:number-or-point/unsafe, 0:variant\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 20"); } const reagent variant_type(const reagent& canonized_base, long long int tag) { assert(tag >= 0); assert(contains_key(Type, canonized_base.type->value)); assert(!get(Type, canonized_base.type->value).name.empty()); const type_info& info = get(Type, canonized_base.type->value); assert(info.kind == EXCLUSIVE_CONTAINER); reagent element = info.elements.at(tag); if (contains_type_ingredient(element)) { if (!canonized_base.type->right) raise_error << "illegal type '" << to_string(canonized_base.type) << "' seems to be missing a type ingredient or three\n" << end(); replace_type_ingredients(element.type, canonized_base.type->right, info); } // End variant_type Special-cases return element; } void test_maybe_convert_product_type_mismatch() { Trace_file = "maybe_convert_product_type_mismatch"; Hide_errors = true; run("recipe main [\n 12:number <- copy 1\n 13:number <- copy 35\n 14:number <- copy 36\n 20:address:number <- maybe-convert 12:number-or-point/unsafe, 1:variant\n]\n"); CHECK_TRACE_CONTENTS("error: main: 'maybe-convert 12:number-or-point/unsafe, 1:variant' should write to (address point) but 20 has type (address number)"); } void test_exclusive_container() { Trace_file = "exclusive_container"; run("exclusive-container foo [\n x:number\n y:number\n]\n"); CHECK_TRACE_CONTENTS("parse: --- defining exclusive-container fooparse: element: x: \"number\"parse: element: y: \"number\""); } void test_exclusive_container_contains_array() { Trace_file = "exclusive_container_contains_array"; Hide_errors = true; run("exclusive-container foo [\n x:array:number:3\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_exclusive_container_warns_on_dynamic_array_element() { Trace_file = "exclusive_container_warns_on_dynamic_array_element"; Hide_errors = true; run("exclusive-container foo [\n x:array:number\n]\n"); CHECK_TRACE_CONTENTS("error: container 'foo' cannot determine size of element x"); } void test_lift_to_exclusive_container() { Trace_file = "lift_to_exclusive_container"; run("exclusive-container foo [\n x:number\n y:number\n]\nrecipe main [\n 1:number <- copy 34\n 2:foo <- merge 0/x, 1:number # tag must be a literal when merging exclusive containers\n 4:foo <- merge 1/y, 1:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 2mem: storing 34 in location 3mem: storing 1 in location 4mem: storing 34 in location 5"); } void test_merge_handles_exclusive_container() { Trace_file = "merge_handles_exclusive_container"; Hide_errors = true; run("exclusive-container foo [\n x:number\n y:bar\n]\ncontainer bar [\n z:number\n]\nrecipe main [\n 1:foo <- merge 0/x, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1mem: storing 34 in location 2"); CHECK_TRACE_COUNT("error", 0); } void test_merge_requires_literal_tag_for_exclusive_container() { Trace_file = "merge_requires_literal_tag_for_exclusive_container"; Hide_errors = true; run("exclusive-container foo [\n x:number\n y:bar\n]\ncontainer bar [\n z:number\n]\nrecipe main [\n local-scope\n 1:number <- copy 0\n 2:foo <- merge 1:number, 34\n]\n"); CHECK_TRACE_CONTENTS("error: main: ingredient 0 of 'merge' should be a literal, for the tag of exclusive-container foo"); } void test_merge_check_container_containing_exclusive_container() { Trace_file = "merge_check_container_containing_exclusive_container"; Hide_errors = true; run("container foo [\n x:number\n y:bar\n]\nexclusive-container bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo <- merge 23, 1/y, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 23 in location 1mem: storing 1 in location 2mem: storing 34 in location 3"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_container_containing_exclusive_container_2() { Trace_file = "merge_check_container_containing_exclusive_container_2"; Hide_errors = true; run("container foo [\n x:number\n y:bar\n]\nexclusive-container bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo <- merge 23, 1/y, 34, 35\n]\n"); CHECK_TRACE_CONTENTS("error: main: too many ingredients in '1:foo <- merge 23, 1/y, 34, 35'"); } void test_merge_check_exclusive_container_containing_container() { Trace_file = "merge_check_exclusive_container_containing_container"; Hide_errors = true; run("exclusive-container foo [\n x:number\n y:bar\n]\ncontainer bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo <- merge 1/y, 23, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1mem: storing 23 in location 2mem: storing 34 in location 3"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_exclusive_container_containing_container_2() { Trace_file = "merge_check_exclusive_container_containing_container_2"; Hide_errors = true; run("exclusive-container foo [\n x:number\n y:bar\n]\ncontainer bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo <- merge 0/x, 23\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_exclusive_container_containing_container_3() { Trace_file = "merge_check_exclusive_container_containing_container_3"; Hide_errors = true; run("exclusive-container foo [\n x:number\n y:bar\n]\ncontainer bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo <- merge 1/y, 23\n]\n"); CHECK_TRACE_CONTENTS("error: main: too few ingredients in '1:foo <- merge 1/y, 23'"); } void test_merge_exclusive_container_with_mismatched_sizes() { Trace_file = "merge_exclusive_container_with_mismatched_sizes"; run("container foo [\n x:number\n y:number\n]\nexclusive-container bar [\n x:number\n y:foo\n]\nrecipe main [\n 1:number <- copy 34\n 2:number <- copy 35\n 3:bar <- merge 0/x, 1:number\n 6:bar <- merge 1/foo, 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3mem: storing 34 in location 4mem: storing 1 in location 6mem: storing 34 in location 7mem: storing 35 in location 8"); } void test_calling_recipe() { Trace_file = "calling_recipe"; run("recipe main [\n f\n]\nrecipe f [\n 3:number <- add 2, 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 3"); } void test_return_on_fallthrough() { Trace_file = "return_on_fallthrough"; run("recipe main [\n f\n 1:number <- copy 0\n 2:number <- copy 0\n 3:number <- copy 0\n]\nrecipe f [\n 4:number <- copy 0\n 5:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("run: frun: 4:number <- copy 0run: 5:number <- copy 0run: 1:number <- copy 0run: 2:number <- copy 0run: 3:number <- copy 0"); } routine::routine(recipe_ordinal r) { if (Trace_stream) { ++Trace_stream->callstack_depth; trace(9999, "trace") << "new routine; incrementing callstack depth to " << Trace_stream->callstack_depth << end(); assert(Trace_stream->callstack_depth < 9000); // 9998-101 plus cushion } calls.push_front(call(r)); alloc = Memory_allocated_until; Memory_allocated_until += Initial_memory_per_routine; alloc_max = Memory_allocated_until; trace(9999, "new") << "routine allocated memory from " << alloc << " to " << alloc_max << end(); global_space = 0; state = RUNNING; id = Next_routine_id; Next_routine_id++; parent_index = -1; limit = -1; /* no limit */ waiting_on_location = old_value_of_waiting_location = 0; waiting_on_routine = 0; // End routine Constructor } inline call& current_call() { return Current_routine->calls.front(); } inline const instruction& to_instruction(const call& call) { return get(Recipe, call.running_recipe).steps.at(call.running_step_index); } void finish_call_housekeeping(const instruction& call_instruction, const vector >& ingredients) { for (long long int i = 0; i < SIZE(ingredients); ++i) { current_call().ingredient_atoms.push_back(ingredients.at(i)); reagent ingredient = call_instruction.ingredients.at(i); canonize_type(ingredient); current_call().ingredients.push_back(ingredient); } // End Call Housekeeping } void test_calling_undefined_recipe_fails() { Trace_file = "calling_undefined_recipe_fails"; Hide_errors = true; run("recipe main [\n foo\n]\n"); CHECK_TRACE_CONTENTS("error: main: undefined operation in 'foo '"); } void test_calling_undefined_recipe_handles_missing_result() { Trace_file = "calling_undefined_recipe_handles_missing_result"; Hide_errors = true; run("recipe main [\n x:number <- foo\n]\n"); CHECK_TRACE_CONTENTS("error: main: undefined operation in 'x:number <- foo '"); } void test_next_ingredient() { Trace_file = "next_ingredient"; run("recipe main [\n f 2\n]\nrecipe f [\n 12:number <- next-ingredient\n 13:number <- add 1, 12:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 3 in location 13"); } void test_next_ingredient_missing() { Trace_file = "next_ingredient_missing"; run("recipe main [\n f\n]\nrecipe f [\n _, 12:number <- next-ingredient\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 12"); } void test_next_ingredient_fail_on_missing() { Trace_file = "next_ingredient_fail_on_missing"; Hide_errors = true; run("recipe main [\n f\n]\nrecipe f [\n 11:number <- next-ingredient\n]\n"); CHECK_TRACE_CONTENTS("error: f: no ingredient to save in 11:number"); } void test_rewind_ingredients() { Trace_file = "rewind_ingredients"; run("recipe main [\n f 2\n]\nrecipe f [\n 12:number <- next-ingredient # consume ingredient\n _, 1:boolean <- next-ingredient # will not find any ingredients\n rewind-ingredients\n 13:number, 2:boolean <- next-ingredient # will find ingredient again\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 12mem: storing 0 in location 1mem: storing 2 in location 13mem: storing 1 in location 2"); } void test_ingredient() { Trace_file = "ingredient"; run("recipe main [\n f 1, 2\n]\nrecipe f [\n 12:number <- ingredient 1 # consume second ingredient first\n 13:number, 1:boolean <- next-ingredient # next-ingredient tries to scan past that\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 12mem: storing 0 in location 1"); } void test_reply() { Trace_file = "reply"; run("recipe main [\n 1:number, 2:number <- f 34\n]\nrecipe f [\n 12:number <- next-ingredient\n 13:number <- add 1, 12:number\n reply 12:number, 13:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1mem: storing 35 in location 2"); } void test_reply_container() { Trace_file = "reply_container"; run("recipe main [\n 3:point <- f 2\n]\nrecipe f [\n 12:number <- next-ingredient\n 13:number <- copy 35\n reply 12:point/raw\n]\n"); CHECK_TRACE_CONTENTS("run: result 0 is [2, 35]mem: storing 2 in location 3mem: storing 35 in location 4"); } void check_types_of_reply_instructions(recipe_ordinal r) { const recipe& caller = get(Recipe, r); trace(9991, "transform") << "--- check types of reply instructions in recipe " << caller.name << end(); for (long long int i = 0; i < SIZE(caller.steps); ++i) { const instruction& caller_instruction = caller.steps.at(i); if (caller_instruction.is_label) continue; if (caller_instruction.products.empty()) continue; if (caller_instruction.operation < MAX_PRIMITIVE_RECIPES) continue; const recipe& callee = get(Recipe, caller_instruction.operation); for (long long int i = 0; i < SIZE(callee.steps); ++i) { const instruction& reply_inst = callee.steps.at(i); if (reply_inst.operation != REPLY) continue; // check types with the caller if (SIZE(caller_instruction.products) > SIZE(reply_inst.ingredients)) { raise_error << maybe(caller.name) << "too few values replied from " << callee.name << '\n' << end(); break; } for (long long int i = 0; i < SIZE(caller_instruction.products); ++i) { if (!types_coercible(caller_instruction.products.at(i), reply_inst.ingredients.at(i))) { raise_error << maybe(callee.name) << "reply ingredient " << reply_inst.ingredients.at(i).original_string << " can't be saved in " << caller_instruction.products.at(i).original_string << '\n' << end(); reagent lhs = reply_inst.ingredients.at(i); canonize_type(lhs); reagent rhs = caller_instruction.products.at(i); canonize_type(rhs); raise_error << to_string(lhs.type) << " vs " << to_string(rhs.type) << '\n' << end(); goto finish_reply_check; } } // check that any reply ingredients with /same-as-ingredient connect up // the corresponding ingredient and product in the caller. for (long long int i = 0; i < SIZE(caller_instruction.products); ++i) { if (has_property(reply_inst.ingredients.at(i), "same-as-ingredient")) { string_tree* tmp = property(reply_inst.ingredients.at(i), "same-as-ingredient"); if (!tmp || tmp->right) { raise_error << maybe(caller.name) << "'same-as-ingredient' metadata should take exactly one value in " << to_string(reply_inst) << '\n' << end(); goto finish_reply_check; } long long int ingredient_index = to_integer(tmp->value); if (ingredient_index >= SIZE(caller_instruction.ingredients)) { raise_error << maybe(caller.name) << "too few ingredients in '" << to_string(caller_instruction) << "'\n" << end(); goto finish_reply_check; } if (!is_dummy(caller_instruction.products.at(i)) && !is_literal(caller_instruction.ingredients.at(ingredient_index)) && caller_instruction.products.at(i).name != caller_instruction.ingredients.at(ingredient_index).name) { raise_error << maybe(caller.name) << "'" << to_string(caller_instruction) << "' should write to " << caller_instruction.ingredients.at(ingredient_index).original_string << " rather than " << caller_instruction.products.at(i).original_string << '\n' << end(); } } } finish_reply_check:; } } } void test_reply_type_mismatch() { Trace_file = "reply_type_mismatch"; Hide_errors = true; run("recipe main [\n 3:number <- f 2\n]\nrecipe f [\n 12:number <- next-ingredient\n 13:number <- copy 35\n 14:point <- copy 12:point/raw\n reply 14:point\n]\n"); CHECK_TRACE_CONTENTS("error: f: reply ingredient 14:point can't be saved in 3:number"); } void test_reply_same_as_ingredient() { Trace_file = "reply_same_as_ingredient"; Hide_errors = true; run("recipe main [\n 1:number <- copy 0\n 2:number <- test1 1:number # call with different ingredient and product\n]\nrecipe test1 [\n 10:number <- next-ingredient\n reply 10:number/same-as-ingredient:0\n]\n"); CHECK_TRACE_CONTENTS("error: main: '2:number <- test1 1:number' should write to 1:number rather than 2:number"); } void test_reply_same_as_ingredient_dummy() { Trace_file = "reply_same_as_ingredient_dummy"; run("# % Hide_errors = true;\nrecipe main [\n 1:number <- copy 0\n _ <- test1 1:number # call with different ingredient and product\n]\nrecipe test1 [\n 10:number <- next-ingredient\n reply 10:number/same-as-ingredient:0\n]\n"); CHECK_TRACE_COUNT("error", 0); } string to_string(const vector& in) { if (in.empty()) return "[]"; ostringstream out; if (SIZE(in) == 1) { out << no_scientific(in.at(0)); return out.str(); } out << "["; for (long long int i = 0; i < SIZE(in); ++i) { if (i > 0) out << ", "; out << no_scientific(in.at(i)); } out << "]"; return out.str(); } void test_reply_if() { Trace_file = "reply_if"; run("recipe main [\n 1:number <- test1\n]\nrecipe test1 [\n reply-if 0, 34\n reply 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 35 in location 1"); } void test_reply_if_2() { Trace_file = "reply_if_2"; run("recipe main [\n 1:number <- test1\n]\nrecipe test1 [\n reply-if 1, 34\n reply 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1"); } void test_new() { Trace_file = "new"; run("# call new two times with identical arguments; you should get back different results\nrecipe main [\n 1:address:shared:number/raw <- new number:type\n 2:address:shared:number/raw <- new number:type\n 3:boolean/raw <- equal 1:address:shared:number/raw, 2:address:shared:number/raw\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3"); } bool product_of_new_is_valid(const instruction& inst) { reagent product = inst.products.at(0); canonize_type(product); if (!product.type || product.type->value != get(Type_ordinal, "address")) return false; drop_from_type(product, "address"); if (!product.type || product.type->value != get(Type_ordinal, "shared")) return false; drop_from_type(product, "shared"); if (SIZE(inst.ingredients) > 1) { // array allocation if (!product.type || product.type->value != get(Type_ordinal, "array")) return false; drop_from_type(product, "array"); } reagent expected_product("x:"+inst.ingredients.at(0).name); { string_tree* tmp_type_names = parse_string_tree(expected_product.type->name); delete expected_product.type; expected_product.type = new_type_tree(tmp_type_names); delete tmp_type_names; } // End Post-processing(expected_product) When Checking 'new' return types_strictly_match(product, expected_product); } void transform_new_to_allocate(const recipe_ordinal r) { trace(9991, "transform") << "--- convert 'new' to 'allocate' for recipe " << get(Recipe, r).name << end(); for (long long int i = 0; i < SIZE(get(Recipe, r).steps); ++i) { instruction& inst = get(Recipe, r).steps.at(i); if (inst.name == "new" && is_literal_string(inst.ingredients.at(0))) continue; // Convert 'new' To 'allocate' if (inst.name == "new") { inst.operation = ALLOCATE; string_tree* type_name = new string_tree(inst.ingredients.at(0).name); type_name = parse_string_tree(type_name); // End Post-processing(type_name) When Converting 'new' type_tree* type = new_type_tree(type_name); inst.ingredients.at(0).set_value(size_of(type)); trace(9992, "new") << "size of " << to_string(type_name) << " is " << inst.ingredients.at(0).value << end(); delete type; delete type_name; } } } void ensure_space(long long int size) { if (size > Initial_memory_per_routine) { tb_shutdown(); cerr << "can't allocate " << size << " locations, that's too much compared to " << Initial_memory_per_routine << ".\n"; exit(0); } if (Current_routine->alloc + size > Current_routine->alloc_max) { // waste the remaining space and create a new chunk Current_routine->alloc = Memory_allocated_until; Memory_allocated_until += Initial_memory_per_routine; Current_routine->alloc_max = Memory_allocated_until; trace(9999, "new") << "routine allocated memory from " << Current_routine->alloc << " to " << Current_routine->alloc_max << end(); } } void test_new_initializes() { Trace_file = "new_initializes"; Memory_allocated_until = 10; put(Memory, Memory_allocated_until, 1); run("recipe main [\n 1:address:shared:number <- new number:type\n 2:number <- copy *1:address:shared:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 2"); } void test_new_error() { Trace_file = "new_error"; Hide_errors = true; run("recipe main [\n 1:address:number/raw <- new number:type\n]\n"); CHECK_TRACE_CONTENTS("error: main: product of 'new' has incorrect type: 1:address:number/raw <- new number:type"); } void test_new_array() { Trace_file = "new_array"; run("recipe main [\n 1:address:shared:array:number/raw <- new number:type, 5\n 2:address:shared:number/raw <- new number:type\n 3:number/raw <- subtract 2:address:shared:number/raw, 1:address:shared:array:number/raw\n]\n"); CHECK_TRACE_CONTENTS("run: 1:address:shared:array:number/raw <- new number:type, 5mem: array size is 5mem: storing 7 in location 3"); } void test_new_empty_array() { Trace_file = "new_empty_array"; run("recipe main [\n 1:address:shared:array:number/raw <- new number:type, 0\n 2:address:shared:number/raw <- new number:type\n 3:number/raw <- subtract 2:address:shared:number/raw, 1:address:shared:array:number/raw\n]\n"); CHECK_TRACE_CONTENTS("run: 1:address:shared:array:number/raw <- new number:type, 0mem: array size is 0mem: storing 2 in location 3"); } void test_new_overflow() { Trace_file = "new_overflow"; Initial_memory_per_routine = 3; // barely enough room for point allocation below run("recipe main [\n 1:address:shared:number/raw <- new number:type\n 2:address:shared:point/raw <- new point:type # not enough room in initial page\n]\n"); CHECK_TRACE_CONTENTS("new: routine allocated memory from 1000 to 1003new: routine allocated memory from 1003 to 1006"); } void test_new_reclaim() { Trace_file = "new_reclaim"; run("recipe main [\n 1:address:shared:number <- new number:type\n 2:address:shared:number <- copy 1:address:shared:number # because 1 will get reset during abandon below\n abandon 1:address:shared:number # unsafe\n 3:address:shared:number <- new number:type # must be same size as abandoned memory to reuse\n 4:boolean <- equal 2:address:shared:number, 3:address:shared:number\n]\n# both allocations should have returned the same address\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 4"); } void abandon(long long int address, long long int size) { trace(9999, "abandon") << "saving in free-list of size " << size << end(); //? Total_free += size; //? Num_free++; //? cerr << "abandon: " << size << '\n'; // clear memory for (long long int curr = address; curr < address+size; ++curr) put(Memory, curr, 0); // append existing free list to address put(Memory, address, get_or_insert(Free_list, size)); put(Free_list, size, address); } void test_new_differing_size_no_reclaim() { Trace_file = "new_differing_size_no_reclaim"; run("recipe main [\n 1:address:shared:number <- new number:type\n 2:address:shared:number <- copy 1:address:shared:number\n abandon 1:address:shared:number\n 3:address:shared:array:number <- new number:type, 2 # different size\n 4:boolean <- equal 2:address:shared:number, 3:address:shared:array:number\n]\n# no reuse\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 4"); } void test_new_reclaim_array() { Trace_file = "new_reclaim_array"; run("recipe main [\n 1:address:shared:array:number <- new number:type, 2\n 2:address:shared:array:number <- copy 1:address:shared:array:number\n abandon 1:address:shared:array:number # unsafe\n 3:address:shared:array:number <- new number:type, 2\n 4:boolean <- equal 2:address:shared:array:number, 3:address:shared:array:number\n]\n# reuse\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 4"); } void test_reset_on_abandon() { Trace_file = "reset_on_abandon"; run("recipe main [\n 1:address:shared:number <- new number:type\n abandon 1:address:shared:number\n]\n# reuse\n"); CHECK_TRACE_CONTENTS("run: abandon 1:address:shared:numbermem: resetting location 1"); } void test_refcounts() { Trace_file = "refcounts"; run("recipe main [\n 1:address:shared:number <- copy 1000/unsafe\n 2:address:shared:number <- copy 1:address:shared:number\n 1:address:shared:number <- copy 0\n 2:address:shared:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("run: 1:address:shared:number <- copy 1000/unsafemem: incrementing refcount of 1000: 0 -> 1run: 2:address:shared:number <- copy 1:address:shared:numbermem: incrementing refcount of 1000: 1 -> 2run: 1:address:shared:number <- copy 0mem: decrementing refcount of 1000: 2 -> 1run: 2:address:shared:number <- copy 0mem: decrementing refcount of 1000: 1 -> 0mem: automatically abandoning 1000"); } void test_refcounts_2() { Trace_file = "refcounts_2"; run("recipe main [\n 1:address:shared:number <- new number:type\n # over-writing one allocation with another\n 1:address:shared:number <- new number:type\n 1:address:shared:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("run: 1:address:shared:number <- new number:typemem: incrementing refcount of 1000: 0 -> 1run: 1:address:shared:number <- new number:typemem: automatically abandoning 1000"); } void test_refcounts_3() { Trace_file = "refcounts_3"; run("recipe main [\n 1:address:shared:number <- new number:type\n # passing in addresses to recipes increments refcount\n foo 1:address:shared:number\n 1:address:shared:number <- copy 0\n]\nrecipe foo [\n 2:address:shared:number <- next-ingredient\n # return does NOT yet decrement refcount; memory must be explicitly managed\n 2:address:shared:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("run: 1:address:shared:number <- new number:typemem: incrementing refcount of 1000: 0 -> 1run: 2:address:shared:number <- next-ingredientmem: incrementing refcount of 1000: 1 -> 2run: 2:address:shared:number <- copy 0mem: decrementing refcount of 1000: 2 -> 1run: 1:address:shared:number <- copy 0mem: decrementing refcount of 1000: 1 -> 0mem: automatically abandoning 1000"); } void test_refcounts_4() { Trace_file = "refcounts_4"; run("recipe main [\n 1:address:shared:number <- new number:type\n # idempotent copies leave refcount unchanged\n 1:address:shared:number <- copy 1:address:shared:number\n]\n"); CHECK_TRACE_CONTENTS("run: 1:address:shared:number <- new number:typemem: incrementing refcount of 1000: 0 -> 1run: 1:address:shared:number <- copy 1:address:shared:numbermem: decrementing refcount of 1000: 1 -> 0mem: incrementing refcount of 1000: 0 -> 1"); } void test_refcounts_5() { Trace_file = "refcounts_5"; run("recipe main [\n 1:address:shared:number <- new number:type\n # passing in addresses to recipes increments refcount\n foo 1:address:shared:number\n # return does NOT yet decrement refcount; memory must be explicitly managed\n 1:address:shared:number <- new number:type\n]\nrecipe foo [\n 2:address:shared:number <- next-ingredient\n]\n"); CHECK_TRACE_CONTENTS("run: 1:address:shared:number <- new number:typemem: incrementing refcount of 1000: 0 -> 1run: 2:address:shared:number <- next-ingredientmem: incrementing refcount of 1000: 1 -> 2run: 1:address:shared:number <- new number:typemem: decrementing refcount of 1000: 2 -> 1"); } void test_new_string() { Trace_file = "new_string"; run("recipe main [\n 1:address:shared:array:character <- new [abc def]\n 2:character <- index *1:address:shared:array:character, 5\n]\n# number code for 'e'\n"); CHECK_TRACE_CONTENTS("mem: storing 101 in location 2"); } void test_new_string_handles_unicode() { Trace_file = "new_string_handles_unicode"; run("recipe main [\n 1:address:shared:array:character <- new [a«c]\n 2:number <- length *1:address:shared:array:character\n 3:character <- index *1:address:shared:array:character, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 3 in location 2mem: storing 171 in location 3"); } long long int new_mu_string(const string& contents) { // allocate an array just large enough for it long long int string_length = unicode_length(contents); //? Total_alloc += string_length+1; //? Num_alloc++; ensure_space(string_length+1); // don't forget the extra location for array size // initialize string long long int result = Current_routine->alloc; // initialize refcount put(Memory, Current_routine->alloc++, 0); // store length put(Memory, Current_routine->alloc++, string_length); long long int curr = 0; const char* raw_contents = contents.c_str(); for (long long int i = 0; i < string_length; ++i) { uint32_t curr_character; assert(curr < SIZE(contents)); tb_utf8_char_to_unicode(&curr_character, &raw_contents[curr]); put(Memory, Current_routine->alloc, curr_character); curr += tb_utf8_char_length(raw_contents[curr]); ++Current_routine->alloc; } // mu strings are not null-terminated in memory return result; } void test_stash_string() { Trace_file = "stash_string"; run("recipe main [\n 1:address:shared:array:character <- new [abc]\n stash [foo:], 1:address:shared:array:character\n]\n"); CHECK_TRACE_CONTENTS("app: foo: abc"); } void test_unicode_string() { Trace_file = "unicode_string"; run("recipe main [\n 1:address:shared:array:character <- new [♠]\n stash [foo:], 1:address:shared:array:character\n]\n"); CHECK_TRACE_CONTENTS("app: foo: ♠"); } void test_stash_space_after_string() { Trace_file = "stash_space_after_string"; run("recipe main [\n 1:address:shared:array:character <- new [abc]\n stash 1:address:shared:array:character, [foo]\n]\n"); CHECK_TRACE_CONTENTS("app: abc foo"); } void test_new_string_overflow() { Trace_file = "new_string_overflow"; Initial_memory_per_routine = 2; run("recipe main [\n 1:address:shared:number/raw <- new number:type\n 2:address:shared:array:character/raw <- new [a] # not enough room in initial page, if you take the array size into account\n]\n"); CHECK_TRACE_CONTENTS("new: routine allocated memory from 1000 to 1002new: routine allocated memory from 1002 to 1004"); } long long int unicode_length(const string& s) { const char* in = s.c_str(); long long int result = 0; long long int curr = 0; while (curr < SIZE(s)) { // carefully bounds-check on the string // before accessing its raw pointer ++result; curr += tb_utf8_char_length(in[curr]); } return result; } string read_mu_string(long long int address) { if (address == 0) return ""; address++; // skip refcount long long int size = get_or_insert(Memory, address); if (size == 0) return ""; ostringstream tmp; for (long long int curr = address+1; curr <= address+size; ++curr) { tmp << to_unicode(static_cast(get_or_insert(Memory, curr))); } return tmp.str(); } bool is_mu_type_literal(reagent r) { return is_literal(r) && r.type && r.type->name == "type"; } bool is_shared_address_of_array_of_numbers(reagent product) { canonize_type(product); if (!product.type || product.type->value != get(Type_ordinal, "address")) return false; drop_from_type(product, "address"); if (!product.type || product.type->value != get(Type_ordinal, "shared")) return false; drop_from_type(product, "shared"); if (!product.type || product.type->value != get(Type_ordinal, "array")) return false; drop_from_type(product, "array"); if (!product.type || product.type->value != get(Type_ordinal, "number")) return false; return true; } void test_brace_conversion() { Trace_file = "brace_conversion"; transform("recipe main [\n {\n break\n 1:number <- copy 0\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: jump 1:offsettransform: copy ..."); } void transform_braces(const recipe_ordinal r) { const int OPEN = 0, CLOSE = 1; // use signed integer for step index because we'll be doing arithmetic on it list > braces; trace(9991, "transform") << "--- transform braces for recipe " << get(Recipe, r).name << end(); //? cerr << "--- transform braces for recipe " << get(Recipe, r).name << '\n'; for (long long int index = 0; index < SIZE(get(Recipe, r).steps); ++index) { const instruction& inst = get(Recipe, r).steps.at(index); if (inst.label == "{") { trace(9993, "transform") << maybe(get(Recipe, r).name) << "push (open, " << index << ")" << end(); braces.push_back(pair(OPEN, index)); } if (inst.label == "}") { trace(9993, "transform") << "push (close, " << index << ")" << end(); braces.push_back(pair(CLOSE, index)); } } stack open_braces; for (long long int index = 0; index < SIZE(get(Recipe, r).steps); ++index) { instruction& inst = get(Recipe, r).steps.at(index); if (inst.label == "{") { open_braces.push(index); continue; } if (inst.label == "}") { if (open_braces.empty()) { raise << "missing '{' in " << get(Recipe, r).name << '\n'; return; } open_braces.pop(); continue; } if (inst.is_label) continue; if (inst.old_name != "loop" && inst.old_name != "loop-if" && inst.old_name != "loop-unless" && inst.old_name != "break" && inst.old_name != "break-if" && inst.old_name != "break-unless") { trace(9992, "transform") << inst.old_name << " ..." << end(); continue; } // check for errors if (inst.old_name.find("-if") != string::npos || inst.old_name.find("-unless") != string::npos) { if (inst.ingredients.empty()) { raise_error << inst.old_name << " expects 1 or 2 ingredients, but got none\n" << end(); continue; } } // update instruction operation if (inst.old_name.find("-if") != string::npos) { inst.name = "jump-if"; inst.operation = JUMP_IF; } else if (inst.old_name.find("-unless") != string::npos) { inst.name = "jump-unless"; inst.operation = JUMP_UNLESS; } else { inst.name = "jump"; inst.operation = JUMP; } // check for explicitly provided targets if (inst.old_name.find("-if") != string::npos || inst.old_name.find("-unless") != string::npos) { // conditional branches check arg 1 if (SIZE(inst.ingredients) > 1 && is_literal(inst.ingredients.at(1))) { trace(9992, "transform") << inst.name << ' ' << inst.ingredients.at(1).name << ":offset" << end(); continue; } } else { // unconditional branches check arg 0 if (!inst.ingredients.empty() && is_literal(inst.ingredients.at(0))) { trace(9992, "transform") << "jump " << inst.ingredients.at(0).name << ":offset" << end(); continue; } } // if implicit, compute target reagent target; target.type = new type_tree("offset", get(Type_ordinal, "offset")); target.set_value(0); if (open_braces.empty()) raise_error << inst.old_name << " needs a '{' before\n" << end(); else if (inst.old_name.find("loop") != string::npos) target.set_value(open_braces.top()-index); else // break instruction target.set_value(matching_brace(open_braces.top(), braces, r) - index - 1); inst.ingredients.push_back(target); // log computed target if (inst.name == "jump") trace(9992, "transform") << "jump " << no_scientific(target.value) << ":offset" << end(); else trace(9992, "transform") << inst.name << ' ' << inst.ingredients.at(0).name << ", " << no_scientific(target.value) << ":offset" << end(); } } // returns a signed integer not just so that we can return -1 but also to // enable future signed arithmetic long long int matching_brace(long long int index, const list >& braces, recipe_ordinal r) { int stacksize = 0; for (list >::const_iterator p = braces.begin(); p != braces.end(); ++p) { if (p->second < index) continue; stacksize += (p->first ? 1 : -1); if (stacksize == 0) return p->second; } raise_error << maybe(get(Recipe, r).name) << "unbalanced '{'\n" << end(); return SIZE(get(Recipe, r).steps); // exit current routine } void test_loop() { Trace_file = "loop"; transform("recipe main [\n 1:number <- copy 0\n 2:number <- copy 0\n {\n 3:number <- copy 0\n loop\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: copy ...transform: copy ...transform: copy ...transform: jump -2:offset"); } void test_break_empty_block() { Trace_file = "break_empty_block"; transform("recipe main [\n 1:number <- copy 0\n {\n break\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: copy ...transform: jump 0:offset"); } void test_break_cascading() { Trace_file = "break_cascading"; transform("recipe main [\n 1:number <- copy 0\n {\n break\n }\n {\n break\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: copy ...transform: jump 0:offsettransform: jump 0:offset"); } void test_break_cascading_2() { Trace_file = "break_cascading_2"; transform("recipe main [\n 1:number <- copy 0\n 2:number <- copy 0\n {\n break\n 3:number <- copy 0\n }\n {\n break\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: copy ...transform: copy ...transform: jump 1:offsettransform: copy ...transform: jump 0:offset"); } void test_break_if() { Trace_file = "break_if"; transform("recipe main [\n 1:number <- copy 0\n 2:number <- copy 0\n {\n break-if 2:number\n 3:number <- copy 0\n }\n {\n break\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: copy ...transform: copy ...transform: jump-if 2, 1:offsettransform: copy ...transform: jump 0:offset"); } void test_break_nested() { Trace_file = "break_nested"; transform("recipe main [\n 1:number <- copy 0\n {\n 2:number <- copy 0\n break\n {\n 3:number <- copy 0\n }\n 4:number <- copy 0\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: jump 4:offset"); } void test_break_nested_degenerate() { Trace_file = "break_nested_degenerate"; transform("recipe main [\n 1:number <- copy 0\n {\n 2:number <- copy 0\n break\n {\n }\n 4:number <- copy 0\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: jump 3:offset"); } void test_break_nested_degenerate_2() { Trace_file = "break_nested_degenerate_2"; transform("recipe main [\n 1:number <- copy 0\n {\n 2:number <- copy 0\n break\n {\n }\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: jump 2:offset"); } void test_break_label() { Trace_file = "break_label"; Hide_errors = true; transform("recipe main [\n 1:number <- copy 0\n {\n break +foo:offset\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: jump +foo:offset"); } void test_break_unless() { Trace_file = "break_unless"; transform("recipe main [\n 1:number <- copy 0\n 2:number <- copy 0\n {\n break-unless 2:number\n 3:number <- copy 0\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: copy ...transform: copy ...transform: jump-unless 2, 1:offsettransform: copy ..."); } void test_loop_unless() { Trace_file = "loop_unless"; transform("recipe main [\n 1:number <- copy 0\n 2:number <- copy 0\n {\n loop-unless 2:number\n 3:number <- copy 0\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: copy ...transform: copy ...transform: jump-unless 2, -1:offsettransform: copy ..."); } void test_loop_nested() { Trace_file = "loop_nested"; transform("recipe main [\n 1:number <- copy 0\n {\n 2:number <- copy 0\n {\n 3:number <- copy 0\n }\n loop-if 4:boolean\n 5:number <- copy 0\n }\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: jump-if 4, -5:offset"); } void test_loop_label() { Trace_file = "loop_label"; transform("recipe main [\n 1:number <- copy 0\n +foo\n 2:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("transform: --- transform braces for recipe maintransform: copy ...transform: copy ..."); } void test_brace_conversion_and_run() { Trace_file = "brace_conversion_and_run"; run("recipe test-factorial [\n 1:number <- copy 5\n 2:number <- copy 1\n {\n 3:boolean <- equal 1:number, 1\n break-if 3:boolean\n# $print 1:number\n 2:number <- multiply 2:number, 1:number\n 1:number <- subtract 1:number, 1\n loop\n }\n 4:number <- copy 2:number # trigger a read\n]\n"); CHECK_TRACE_CONTENTS("mem: location 2 is 120"); } void test_break_outside_braces_fails() { Trace_file = "break_outside_braces_fails"; Hide_errors = true; run("recipe main [\n break\n]\n"); CHECK_TRACE_CONTENTS("error: break needs a '{' before"); } void test_break_conditional_without_ingredient_fails() { Trace_file = "break_conditional_without_ingredient_fails"; Hide_errors = true; run("recipe main [\n {\n break-if\n }\n]\n"); CHECK_TRACE_CONTENTS("error: break-if expects 1 or 2 ingredients, but got none"); } void test_jump_to_label() { Trace_file = "jump_to_label"; run("recipe main [\n jump +target:label\n 1:number <- copy 0\n +target\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1"); } void transform_labels(const recipe_ordinal r) { map offset; for (long long int i = 0; i < SIZE(get(Recipe, r).steps); ++i) { const instruction& inst = get(Recipe, r).steps.at(i); if (!inst.label.empty() && inst.label.at(0) == '+') { if (!contains_key(offset, inst.label)) { put(offset, inst.label, i); } else { raise_error << maybe(get(Recipe, r).name) << "duplicate label '" << inst.label << "'" << end(); // have all jumps skip some random but noticeable and deterministic amount of code put(offset, inst.label, 9999); } } } for (long long int i = 0; i < SIZE(get(Recipe, r).steps); ++i) { instruction& inst = get(Recipe, r).steps.at(i); if (inst.name == "jump") { replace_offset(inst.ingredients.at(0), offset, i, r); } if (inst.name == "jump-if" || inst.name == "jump-unless") { replace_offset(inst.ingredients.at(1), offset, i, r); } if ((inst.name == "loop" || inst.name == "break") && SIZE(inst.ingredients) == 1) { replace_offset(inst.ingredients.at(0), offset, i, r); } if ((inst.name == "loop-if" || inst.name == "loop-unless" || inst.name == "break-if" || inst.name == "break-unless") && SIZE(inst.ingredients) == 2) { replace_offset(inst.ingredients.at(1), offset, i, r); } } } void replace_offset(reagent& x, /*const*/ map& offset, const long long int current_offset, const recipe_ordinal r) { if (!is_literal(x)) { raise_error << maybe(get(Recipe, r).name) << "jump target must be offset or label but is " << x.original_string << '\n' << end(); x.set_value(0); // no jump by default return; } if (x.initialized) return; if (is_integer(x.name)) return; // non-labels will be handled like other number operands if (!is_jump_target(x.name)) { raise_error << maybe(get(Recipe, r).name) << "can't jump to label " << x.name << '\n' << end(); x.set_value(0); // no jump by default return; } if (!contains_key(offset, x.name)) { raise_error << maybe(get(Recipe, r).name) << "can't find label " << x.name << '\n' << end(); x.set_value(0); // no jump by default return; } x.set_value(get(offset, x.name) - current_offset); } bool is_jump_target(string label) { return label.at(0) == '+'; } void test_break_to_label() { Trace_file = "break_to_label"; run("recipe main [\n {\n {\n break +target:label\n 1:number <- copy 0\n }\n }\n +target\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1"); } void test_jump_if_to_label() { Trace_file = "jump_if_to_label"; run("recipe main [\n {\n {\n jump-if 1, +target:label\n 1:number <- copy 0\n }\n }\n +target\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1"); } void test_loop_unless_to_label() { Trace_file = "loop_unless_to_label"; run("recipe main [\n {\n {\n loop-unless 0, +target:label # loop/break with a label don't care about braces\n 1:number <- copy 0\n }\n }\n +target\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1"); } void test_jump_runs_code_after_label() { Trace_file = "jump_runs_code_after_label"; run("recipe main [\n # first a few lines of padding to exercise the offset computation\n 1:number <- copy 0\n 2:number <- copy 0\n 3:number <- copy 0\n jump +target:label\n 4:number <- copy 0\n +target\n 5:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 5"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 4"); } void test_recipe_fails_on_duplicate_jump_target() { Trace_file = "recipe_fails_on_duplicate_jump_target"; Hide_errors = true; run("recipe main [\n +label\n 1:number <- copy 0\n +label\n 2:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("error: main: duplicate label '+label'"); } void test_jump_ignores_nontarget_label() { Trace_file = "jump_ignores_nontarget_label"; Hide_errors = true; run("recipe main [\n # first a few lines of padding to exercise the offset computation\n 1:number <- copy 0\n 2:number <- copy 0\n 3:number <- copy 0\n jump $target:label\n 4:number <- copy 0\n $target\n 5:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("error: main: can't jump to label $target"); } void test_transform_names() { Trace_file = "transform_names"; run("recipe main [\n x:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("name: assign x 1mem: storing 0 in location 1"); } void test_transform_names_fails_on_use_before_define() { Trace_file = "transform_names_fails_on_use_before_define"; Hide_errors = true; transform("recipe main [\n x:number <- copy y:number\n]\n"); CHECK_TRACE_CONTENTS("error: main: use before set: y"); } void transform_names(const recipe_ordinal r) { recipe& caller = get(Recipe, r); trace(9991, "transform") << "--- transform names for recipe " << caller.name << end(); //? cerr << "--- transform names for recipe " << caller.name << '\n'; bool names_used = false; bool numeric_locations_used = false; map& names = Name[r]; // store the indices 'used' so far in the map long long int& curr_idx = names[""]; ++curr_idx; // avoid using index 0, benign skip in some other cases for (long long int i = 0; i < SIZE(caller.steps); ++i) { instruction& inst = caller.steps.at(i); // replace element names of containers with offsets if (inst.name == "get" || inst.name == "get-address") { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "exactly 2 ingredients expected in '" << to_string(inst) << "'\n" << end(); break; } if (!is_literal(inst.ingredients.at(1))) raise_error << maybe(get(Recipe, r).name) << "expected ingredient 1 of " << (inst.name == "get" ? "'get'" : "'get-address'") << " to have type 'offset'; got " << inst.ingredients.at(1).original_string << '\n' << end(); if (inst.ingredients.at(1).name.find_first_not_of("0123456789") != string::npos) { // since first non-address in base type must be a container, we don't have to canonize type_ordinal base_type = skip_addresses(inst.ingredients.at(0).type, get(Recipe, r).name); if (contains_key(Type, base_type)) { // otherwise we'll raise an error elsewhere inst.ingredients.at(1).set_value(find_element_name(base_type, inst.ingredients.at(1).name, get(Recipe, r).name)); trace(9993, "name") << "element " << inst.ingredients.at(1).name << " of type " << get(Type, base_type).name << " is at offset " << no_scientific(inst.ingredients.at(1).value) << end(); } } } // convert variant names of exclusive containers if (inst.name == "maybe-convert") { if (SIZE(inst.ingredients) != 2) { raise_error << maybe(get(Recipe, r).name) << "exactly 2 ingredients expected in '" << to_string(inst) << "'\n" << end(); break; } assert(is_literal(inst.ingredients.at(1))); if (inst.ingredients.at(1).name.find_first_not_of("0123456789") != string::npos) { // since first non-address in base type must be an exclusive container, we don't have to canonize type_ordinal base_type = skip_addresses(inst.ingredients.at(0).type, get(Recipe, r).name); if (contains_key(Type, base_type)) { // otherwise we'll raise an error elsewhere inst.ingredients.at(1).set_value(find_element_name(base_type, inst.ingredients.at(1).name, get(Recipe, r).name)); trace(9993, "name") << "variant " << inst.ingredients.at(1).name << " of type " << get(Type, base_type).name << " has tag " << no_scientific(inst.ingredients.at(1).value) << end(); } } } // End transform_names(inst) Special-cases // map names to addresses for (long long int in = 0; in < SIZE(inst.ingredients); ++in) { if (is_disqualified(inst.ingredients.at(in), inst, caller.name)) continue; if (is_numeric_location(inst.ingredients.at(in))) numeric_locations_used = true; if (is_named_location(inst.ingredients.at(in))) names_used = true; if (is_integer(inst.ingredients.at(in).name)) continue; if (!already_transformed(inst.ingredients.at(in), names)) { raise_error << maybe(caller.name) << "use before set: " << inst.ingredients.at(in).name << '\n' << end(); } inst.ingredients.at(in).set_value(lookup_name(inst.ingredients.at(in), r)); } for (long long int out = 0; out < SIZE(inst.products); ++out) { if (is_disqualified(inst.products.at(out), inst, caller.name)) continue; if (is_numeric_location(inst.products.at(out))) numeric_locations_used = true; if (is_named_location(inst.products.at(out))) names_used = true; if (is_integer(inst.products.at(out).name)) continue; if (names.find(inst.products.at(out).name) == names.end()) { trace(9993, "name") << "assign " << inst.products.at(out).name << " " << curr_idx << end(); names[inst.products.at(out).name] = curr_idx; curr_idx += size_of(inst.products.at(out)); } inst.products.at(out).set_value(lookup_name(inst.products.at(out), r)); } } if (names_used && numeric_locations_used) raise_error << maybe(caller.name) << "mixing variable names and numeric addresses\n" << end(); } bool is_disqualified(/*mutable*/ reagent& x, const instruction& inst, const string& recipe_name) { if (!x.type) { if (!x.type && contains_key(Recipe_ordinal, x.name)) { x.type = new type_tree("recipe-literal", get(Type_ordinal, "recipe-literal")); x.set_value(get(Recipe_ordinal, x.name)); return true; } // End Null-type is_disqualified Exceptions raise_error << maybe(recipe_name) << "missing type for " << x.original_string << " in '" << to_string(inst) << "'\n" << end(); return true; } if (is_raw(x)) return true; if (is_literal(x)) return true; if (x.name == "default-space") x.initialized = true; if (x.name == "number-of-locals") x.initialized = true; if (x.name == "global-space") x.initialized = true; // End is_disqualified Cases if (x.initialized) return true; return false; } bool already_transformed(const reagent& r, const map& names) { if (has_property(r, "space")) { string_tree* p = property(r, "space"); if (!p || p->right) { raise_error << "/space property should have exactly one (non-negative integer) value in " << r.original_string << '\n' << end(); return false; } if (p->value != "0") return true; } return contains_key(names, r.name); } long long int lookup_name(const reagent& x, const recipe_ordinal default_recipe) { if (!has_property(x, "space")) { if (Name[default_recipe].empty()) raise_error << "name not found: " << x.name << '\n' << end(); return Name[default_recipe][x.name]; } string_tree* p = property(x, "space"); if (!p || p->right) raise_error << "/space property should have exactly one (non-negative integer) value\n" << end(); long long int n = to_integer(p->value); assert(n >= 0); recipe_ordinal surrounding_recipe = lookup_surrounding_recipe(default_recipe, n); set done; vector path; return lookup_name(x, surrounding_recipe, done, path); } // If the recipe we need to lookup this name in doesn't have names done yet, // recursively call transform_names on it. long long int lookup_name(const reagent& x, const recipe_ordinal r, set& done, vector& path) { if (!Name[r].empty()) return Name[r][x.name]; if (contains_key(done, r)) { raise_error << "can't compute address of " << to_string(x) << " because " << end(); for (long long int i = 1; i < SIZE(path); ++i) { raise_error << path.at(i-1) << " requires computing names of " << path.at(i) << '\n' << end(); } raise_error << path.at(SIZE(path)-1) << " requires computing names of " << r << "..ad infinitum\n" << end(); return 0; } done.insert(r); path.push_back(r); transform_names(r); // Not passing 'done' through. Might this somehow cause an infinite loop? assert(!Name[r].empty()); return Name[r][x.name]; } recipe_ordinal lookup_surrounding_recipe(const recipe_ordinal r, long long int n) { if (n == 0) return r; if (!contains_key(Surrounding_space, r)) { raise_error << "don't know surrounding recipe of " << get(Recipe, r).name << '\n' << end(); return 0; } assert(contains_key(Surrounding_space, r)); return lookup_surrounding_recipe(get(Surrounding_space, r), n-1); } type_ordinal skip_addresses(type_tree* type, const string& recipe_name) { type_ordinal address = get(Type_ordinal, "address"); type_ordinal shared = get(Type_ordinal, "shared"); for (; type; type = type->right) { if (type->value != address && type->value != shared) return type->value; } raise_error << maybe(recipe_name) << "expected a container" << '\n' << end(); return -1; } int find_element_name(const type_ordinal t, const string& name, const string& recipe_name) { const type_info& container = get(Type, t); for (long long int i = 0; i < SIZE(container.elements); ++i) if (container.elements.at(i).name == name) return i; raise_error << maybe(recipe_name) << "unknown element " << name << " in container " << get(Type, t).name << '\n' << end(); return -1; } bool is_numeric_location(const reagent& x) { if (is_global(x)) return false; if (is_literal(x)) return false; if (is_raw(x)) return false; if (x.name == "0") return false; // used for chaining lexical scopes return is_integer(x.name); } bool is_named_location(const reagent& x) { if (is_literal(x)) return false; if (is_raw(x)) return false; if (is_special_name(x.name)) return false; return !is_integer(x.name); } bool is_special_name(const string& s) { if (s == "_") return true; if (s == "0") return true; if (s == "default-space") return true; if (s == "number-of-locals") return true; if (s == "global-space") return true; if (s == "screen") return true; if (s == "console") return true; // End is_special_name Cases return false; } void test_transform_names_passes_dummy() { Trace_file = "transform_names_passes_dummy"; transform("# _ is just a dummy result that never gets consumed\nrecipe main [\n _, x:number <- copy 0, 1\n]\n"); CHECK_TRACE_CONTENTS("name: assign x 1"); CHECK_TRACE_DOESNT_CONTAIN("name: assign _ 1"); } void test_transform_names_passes_raw() { Trace_file = "transform_names_passes_raw"; Hide_errors = true; run("recipe main [\n x:number/raw <- copy 0\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("name: assign x 1"); run(""); CHECK_TRACE_CONTENTS("error: can't write to location 0 in 'x:number/raw <- copy 0'"); } void test_transform_names_fails_when_mixing_names_and_numeric_locations() { Trace_file = "transform_names_fails_when_mixing_names_and_numeric_locations"; Hide_errors = true; transform("recipe main [\n x:number <- copy 1:number\n]\n"); CHECK_TRACE_CONTENTS("error: main: mixing variable names and numeric addresses"); } void test_transform_names_fails_when_mixing_names_and_numeric_locations_2() { Trace_file = "transform_names_fails_when_mixing_names_and_numeric_locations_2"; Hide_errors = true; transform("recipe main [\n x:number <- copy 1\n 1:number <- copy x:number\n]\n"); CHECK_TRACE_CONTENTS("error: main: mixing variable names and numeric addresses"); } void test_transform_names_does_not_fail_when_mixing_names_and_raw_locations() { Trace_file = "transform_names_does_not_fail_when_mixing_names_and_raw_locations"; Hide_errors = true; transform("recipe main [\n x:number <- copy 1:number/raw\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("error: main: mixing variable names and numeric addresses"); CHECK_TRACE_COUNT("error", 0); } void test_transform_names_does_not_fail_when_mixing_names_and_literals() { Trace_file = "transform_names_does_not_fail_when_mixing_names_and_literals"; Hide_errors = true; transform("recipe main [\n x:number <- copy 1\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("error: main: mixing variable names and numeric addresses"); CHECK_TRACE_COUNT("error", 0); } void test_transform_names_transforms_container_elements() { Trace_file = "transform_names_transforms_container_elements"; transform("recipe main [\n p:address:point <- copy 0\n a:number <- get *p:address:point, y:offset\n b:number <- get *p:address:point, x:offset\n]\n"); CHECK_TRACE_CONTENTS("name: element y of type point is at offset 1name: element x of type point is at offset 0"); } void test_transform_names_handles_containers() { Trace_file = "transform_names_handles_containers"; transform("recipe main [\n a:point <- copy 0/unsafe\n b:number <- copy 0/unsafe\n]\n"); CHECK_TRACE_CONTENTS("name: assign a 1name: assign b 3"); } void test_transform_names_handles_exclusive_containers() { Trace_file = "transform_names_handles_exclusive_containers"; run("recipe main [\n 12:number <- copy 1\n 13:number <- copy 35\n 14:number <- copy 36\n 20:address:point <- maybe-convert 12:number-or-point/unsafe, p:variant\n]\n"); CHECK_TRACE_CONTENTS("name: variant p of type number-or-point has tag 1mem: storing 13 in location 20"); } void test_set_default_space() { Trace_file = "set_default_space"; run("# if default-space is 10, and if an array of 5 locals lies from location 12 to 16 (inclusive),\n# then local 0 is really location 12, local 1 is really location 13, and so on.\nrecipe main [\n # pretend shared:array:location; in practice we'll use new\n 10:number <- copy 0 # refcount\n 11:number <- copy 5 # length\n default-space:address:shared:array:location <- copy 10/unsafe\n 1:number <- copy 23\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 23 in location 13"); } void test_lookup_sidesteps_default_space() { Trace_file = "lookup_sidesteps_default_space"; run("recipe main [\n # pretend pointer from outside\n 3:number <- copy 34\n # pretend shared:array:location; in practice we'll use new\n 1000:number <- copy 0 # refcount\n 1001:number <- copy 5 # length\n # actual start of this recipe\n default-space:address:shared:array:location <- copy 1000/unsafe\n 1:address:number <- copy 3/unsafe\n 8:number/raw <- copy *1:address:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 8"); } void test_convert_names_passes_default_space() { Trace_file = "convert_names_passes_default_space"; Hide_errors = true; run("recipe main [\n default-space:number, x:number <- copy 0, 1\n]\n"); CHECK_TRACE_CONTENTS("name: assign x 1"); CHECK_TRACE_DOESNT_CONTAIN("name: assign default-space 1"); } void absolutize(reagent& x) { if (is_raw(x) || is_dummy(x)) return; if (x.name == "default-space") return; if (!x.initialized) { raise_error << to_string(current_instruction()) << ": reagent not initialized: " << x.original_string << '\n' << end(); } x.set_value(address(x.value, space_base(x))); x.properties.push_back(pair("raw", NULL)); assert(is_raw(x)); } long long int space_base(const reagent& x) { if (is_global(x)) { if (!Current_routine->global_space) raise_error << "routine has no global space\n" << end(); return Current_routine->global_space + /*skip refcount*/1; } long long int base = current_call().default_space ? (current_call().default_space+/*skip refcount*/1) : 0; return space_base(x, space_index(x), base); } long long int space_base(const reagent& x, long long int space_index, long long int base) { //? trace(9999, "space") << "space-base: " << space_index << " " << base << end(); if (space_index == 0) { return base; } long long int result = space_base(x, space_index-1, get_or_insert(Memory, base+/*skip length*/1))+/*skip refcount*/1; //? trace(9999, "space") << "space-base: " << space_index << " " << base << " => " << result << end(); return result; } long long int space_index(const reagent& x) { for (long long int i = 0; i < SIZE(x.properties); ++i) { if (x.properties.at(i).first == "space") { if (!x.properties.at(i).second || x.properties.at(i).second->right) raise_error << maybe(current_recipe_name()) << "/space metadata should take exactly one value in " << x.original_string << '\n' << end(); return to_integer(x.properties.at(i).second->value); } } return 0; } long long int address(long long int offset, long long int base) { if (base == 0) return offset; // raw long long int size = get_or_insert(Memory, base); if (offset >= size) { // todo: test raise_error << "location " << offset << " is out of bounds " << size << " at " << base << '\n' << end(); return 0; } return base + /*skip length*/1 + offset; } void test_get_default_space() { Trace_file = "get_default_space"; run("recipe main [\n default-space:address:shared:array:location <- copy 10/unsafe\n 1:address:shared:array:location/raw <- copy default-space:address:shared:array:location\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 10 in location 1"); } void test_lookup_sidesteps_default_space_in_get() { Trace_file = "lookup_sidesteps_default_space_in_get"; run("recipe main [\n # pretend pointer to container from outside\n 12:number <- copy 34\n 13:number <- copy 35\n # pretend shared:array:location; in practice we'll use new\n 1000:number <- copy 0 # refcount\n 1001:number <- copy 5 # length\n # actual start of this recipe\n default-space:address:shared:array:location <- copy 1000/unsafe\n 1:address:point <- copy 12/unsafe\n 9:number/raw <- get *1:address:point, 1:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 35 in location 9"); } void test_lookup_sidesteps_default_space_in_index() { Trace_file = "lookup_sidesteps_default_space_in_index"; run("recipe main [\n # pretend pointer to array from outside\n 12:number <- copy 2\n 13:number <- copy 34\n 14:number <- copy 35\n # pretend shared:array:location; in practice we'll use new\n 1000:number <- copy 0 # refcount\n 1001:number <- copy 5 # length\n # actual start of this recipe\n default-space:address:shared:array:location <- copy 1000/unsafe\n 1:address:array:number <- copy 12/unsafe\n 9:number/raw <- index *1:address:array:number, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 35 in location 9"); } void test_new_default_space() { Trace_file = "new_default_space"; run("recipe main [\n new-default-space\n x:number <- copy 0\n y:number <- copy 3\n]\n# allocate space for x and y, as well as the chaining slot at 0\n"); CHECK_TRACE_CONTENTS("mem: array size is 3"); } void test_local_scope() { Trace_file = "local_scope"; run("recipe main [\n 1:address <- foo\n 2:address <- foo\n 3:boolean <- equal 1:address, 2:address\n]\nrecipe foo [\n local-scope\n x:number <- copy 34\n reply default-space:address:shared:array:location\n]\n# both calls to foo should have received the same default-space\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void try_reclaim_locals() { // only reclaim routines starting with 'local-scope' const recipe_ordinal r = get(Recipe_ordinal, current_recipe_name()); if (get(Recipe, r).steps.empty()) return; const instruction& inst = get(Recipe, r).steps.at(0); if (inst.old_name != "local-scope") return; abandon(current_call().default_space, /*refcount*/1 + /*array length*/1 + /*number-of-locals*/Name[r][""]); } void rewrite_default_space_instruction(instruction& curr) { if (!curr.ingredients.empty()) raise_error << to_string(curr) << " can't take any ingredients\n" << end(); curr.name = "new"; curr.ingredients.push_back(reagent("location:type")); curr.ingredients.push_back(reagent("number-of-locals:literal")); if (!curr.products.empty()) raise_error << "new-default-space can't take any results\n" << end(); curr.products.push_back(reagent("default-space:address:shared:array:location")); } void check_default_space(const recipe_ordinal r) { if (!Warn_on_missing_default_space) return; // skip previous core tests; this is only for mu code const recipe& caller = get(Recipe, r); // skip scenarios (later layer) // user code should never create recipes with underscores in their names if (caller.name.find("scenario_") == 0) return; // skip mu scenarios which will use raw memory locations if (caller.name.find("run_") == 0) return; // skip calls to 'run', which should be in scenarios and will also use raw memory locations // assume recipes with only numeric addresses know what they're doing (usually tests) if (!contains_non_special_name(r)) return; trace(9991, "transform") << "--- check that recipe " << caller.name << " sets default-space" << end(); if (caller.steps.empty()) return; if (caller.steps.at(0).products.empty() || caller.steps.at(0).products.at(0).name != "default-space") { raise << maybe(caller.name) << " does not seem to start with default-space or local-scope\n" << end(); //? cerr << maybe(caller.name) << " does not seem to start with default-space or local-scope\n" << '\n'; } } bool contains_non_special_name(const recipe_ordinal r) { for (map::iterator p = Name[r].begin(); p != Name[r].end(); ++p) { if (p->first.empty()) continue; if (p->first.find("stash_") == 0) continue; // generated by rewrite_stashes_to_text (cross-layer) if (!is_special_name(p->first)) { //? cerr << " " << get(Recipe, r).name << ": " << p->first << '\n'; return true; } } return false; } void test_surrounding_space() { Trace_file = "surrounding_space"; run("# location 1 in space 1 refers to the space surrounding the default space, here 20.\nrecipe main [\n # pretend shared:array:location; in practice we'll use new\n 10:number <- copy 0 # refcount\n 11:number <- copy 5 # length\n # pretend shared:array:location; in practice we'll use new\n 20:number <- copy 0 # refcount\n 21:number <- copy 5 # length\n # actual start of this recipe\n default-space:address:shared:array:location <- copy 10/unsafe\n 0:address:shared:array:location/names:dummy <- copy 20/unsafe # later layers will explain the /names: property\n 1:number <- copy 32\n 1:number/space:1 <- copy 33\n]\nrecipe dummy [ # just for the /names: property above\n]\n# chain space: 10 + /*skip refcount*/1 + /*skip length*/1\n"); CHECK_TRACE_CONTENTS("mem: storing 20 in location 12mem: storing 32 in location 13mem: storing 33 in location 23"); } void test_permit_space_as_variable_name() { Trace_file = "permit_space_as_variable_name"; run("recipe main [\n space:number <- copy 0\n]\n"); } void test_closure() { Trace_file = "closure"; run("recipe main [\n default-space:address:shared:array:location <- new location:type, 30\n 1:address:shared:array:location/names:new-counter <- new-counter\n 2:number/raw <- increment-counter 1:address:shared:array:location/names:new-counter\n 3:number/raw <- increment-counter 1:address:shared:array:location/names:new-counter\n]\nrecipe new-counter [\n default-space:address:shared:array:location <- new location:type, 30\n x:number <- copy 23\n y:number <- copy 3 # variable that will be incremented\n reply default-space:address:shared:array:location\n]\nrecipe increment-counter [\n default-space:address:shared:array:location <- new location:type, 30\n 0:address:shared:array:location/names:new-counter <- next-ingredient # outer space must be created by 'new-counter' above\n y:number/space:1 <- add y:number/space:1, 1 # increment\n y:number <- copy 234 # dummy\n reply y:number/space:1\n]\n"); CHECK_TRACE_CONTENTS("name: lexically surrounding space for recipe increment-counter comes from new-countermem: storing 5 in location 3"); } void collect_surrounding_spaces(const recipe_ordinal r) { trace(9991, "transform") << "--- collect surrounding spaces for recipe " << get(Recipe, r).name << end(); //? cerr << "--- collect surrounding spaces for recipe " << get(Recipe, r).name << '\n'; for (long long int i = 0; i < SIZE(get(Recipe, r).steps); ++i) { const instruction& inst = get(Recipe, r).steps.at(i); if (inst.is_label) continue; for (long long int j = 0; j < SIZE(inst.products); ++j) { if (is_literal(inst.products.at(j))) continue; if (inst.products.at(j).name != "0") continue; type_tree* type = inst.products.at(j).type; if (!type || type->value != get(Type_ordinal, "address") || !type->right || type->right->value != get(Type_ordinal, "shared") || !type->right->right || type->right->right->value != get(Type_ordinal, "array") || !type->right->right->right || type->right->right->right->value != get(Type_ordinal, "location") || type->right->right->right->right) { raise_error << "slot 0 should always have type address:shared:array:location, but is " << to_string(inst.products.at(j)) << '\n' << end(); continue; } string_tree* s = property(inst.products.at(j), "names"); if (!s) { raise_error << "slot 0 requires a /names property in recipe " << get(Recipe, r).name << end(); continue; } if (s->right) raise_error << "slot 0 should have a single value in /names, but got " << to_string(inst.products.at(j)) << '\n' << end(); const string& surrounding_recipe_name = s->value; if (surrounding_recipe_name.empty()) { raise_error << "slot 0 doesn't initialize its /names property in recipe " << get(Recipe, r).name << end(); continue; } if (contains_key(Surrounding_space, r) && get(Surrounding_space, r) != get(Recipe_ordinal, surrounding_recipe_name)) { raise_error << "recipe " << get(Recipe, r).name << " can have only one 'surrounding' recipe but has " << get(Recipe, get(Surrounding_space, r)).name << " and " << surrounding_recipe_name << '\n' << end(); continue; } trace(9993, "name") << "lexically surrounding space for recipe " << get(Recipe, r).name << " comes from " << surrounding_recipe_name << end(); //? cerr << "lexically surrounding space for recipe " << get(Recipe, r).name << " comes from " << surrounding_recipe_name << '\n'; if (!contains_key(Recipe_ordinal, surrounding_recipe_name)) { raise << "can't find recipe providing surrounding space for " << get(Recipe, r).name << ": " << surrounding_recipe_name << '\n' << end(); continue; } put(Surrounding_space, r, get(Recipe_ordinal, surrounding_recipe_name)); } } } // So far we have local variables, and we can nest local variables of short // lifetimes inside longer ones. Now let's support 'global' variables that // last for the life of a routine. If we create multiple routines they won't // have access to each other's globals. void test_global_space() { Trace_file = "global_space"; run("recipe main [\n # pretend shared:array:location; in practice we'll use new\n 10:number <- copy 0 # refcount\n 11:number <- copy 5 # length\n # pretend shared:array:location; in practice we'll use new\n 20:number <- copy 0 # refcount\n 21:number <- copy 5 # length\n # actual start of this recipe\n global-space:address:shared:array:location <- copy 20/unsafe\n default-space:address:shared:array:location <- copy 10/unsafe\n 1:number <- copy 23\n 1:number/space:global <- copy 24\n]\n# store to default space: 10 + /*skip refcount*/1 + /*skip length*/1 + /*index*/1\n"); CHECK_TRACE_CONTENTS("mem: storing 23 in location 13mem: storing 24 in location 23"); } void test_global_space_with_names() { Trace_file = "global_space_with_names"; Hide_errors = true; run("recipe main [\n global-space:address:shared:array:location <- new location:type, 10\n x:number <- copy 23\n 1:number/space:global <- copy 24\n]\n# don't complain about mixing numeric addresses and names\n"); CHECK_TRACE_COUNT("error", 0); } bool is_global(const reagent& x) { for (long long int i = 0; i < SIZE(x.properties); ++i) { if (x.properties.at(i).first == "space") return x.properties.at(i).second && x.properties.at(i).second->value == "global"; } return false; } void test_transform_fails_on_reusing_name_with_different_type() { Trace_file = "transform_fails_on_reusing_name_with_different_type"; Hide_errors = true; run("recipe main [\n x:number <- copy 1\n x:boolean <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("error: main: x used with multiple types"); } void check_or_set_types_by_name(const recipe_ordinal r) { trace(9991, "transform") << "--- deduce types for recipe " << get(Recipe, r).name << end(); map type; for (long long int i = 0; i < SIZE(get(Recipe, r).steps); ++i) { instruction& inst = get(Recipe, r).steps.at(i); for (long long int in = 0; in < SIZE(inst.ingredients); ++in) { deduce_missing_type(type, inst.ingredients.at(in)); check_type(type, inst.ingredients.at(in), r); } for (long long int out = 0; out < SIZE(inst.products); ++out) { deduce_missing_type(type, inst.products.at(out)); check_type(type, inst.products.at(out), r); } } } void deduce_missing_type(map& type, reagent& x) { if (x.type) return; if (!contains_key(type, x.name)) return; x.type = new type_tree(*get(type, x.name)); trace(9992, "transform") << x.name << " <= " << names_to_string(x.type) << end(); } void check_type(map& type, const reagent& x, const recipe_ordinal r) { if (is_literal(x)) return; if (is_integer(x.name)) return; // if you use raw locations you're probably doing something unsafe if (!x.type) return; // might get filled in by other logic later if (!contains_key(type, x.name)) { trace(9992, "transform") << x.name << " => " << names_to_string(x.type) << end(); put(type, x.name, x.type); } if (!types_strictly_match(get(type, x.name), x.type)) { raise_error << maybe(get(Recipe, r).name) << x.name << " used with multiple types\n" << end(); return; } if (get(type, x.name)->name == "array") { if (!get(type, x.name)->right) { raise_error << maybe(get(Recipe, r).name) << x.name << " can't be just an array. What is it an array of?\n" << end(); return; } if (!get(type, x.name)->right->right) { raise_error << get(Recipe, r).name << " can't determine the size of array variable " << x.name << ". Either allocate it separately and make the type of " << x.name << " address:shared:..., or specify the length of the array in the type of " << x.name << ".\n" << end(); return; } } } void test_transform_fills_in_missing_types() { Trace_file = "transform_fills_in_missing_types"; run("recipe main [\n x:number <- copy 1\n y:number <- add x, 1\n]\n"); } void test_transform_fills_in_missing_types_in_product() { Trace_file = "transform_fills_in_missing_types_in_product"; run("recipe main [\n x:number <- copy 1\n x <- copy 2\n]\n"); } void test_transform_fills_in_missing_types_in_product_and_ingredient() { Trace_file = "transform_fills_in_missing_types_in_product_and_ingredient"; run("recipe main [\n x:number <- copy 1\n x <- add x, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_transform_fails_on_missing_types_in_first_mention() { Trace_file = "transform_fails_on_missing_types_in_first_mention"; Hide_errors = true; run("recipe main [\n x <- copy 1\n x:number <- copy 2\n]\n"); CHECK_TRACE_CONTENTS("error: main: missing type for x in 'x <- copy 1'"); } void test_typo_in_address_type_fails() { Trace_file = "typo_in_address_type_fails"; Hide_errors = true; run("recipe main [\n y:address:shared:charcter <- new character:type\n *y <- copy 67\n]\n"); CHECK_TRACE_CONTENTS("error: main: unknown type charcter in 'y:address:shared:charcter <- new character:type'"); } void test_array_type_without_size_fails() { Trace_file = "array_type_without_size_fails"; Hide_errors = true; run("recipe main [\n x:array:number <- merge 2, 12, 13\n]\n"); CHECK_TRACE_CONTENTS("error: main can't determine the size of array variable x. Either allocate it separately and make the type of x address:shared:..., or specify the length of the array in the type of x."); } scenario parse_scenario(istream& in) { scenario result; result.name = next_word(in); if (contains_key(Scenario_names, result.name)) raise_error << "duplicate scenario name: " << result.name << '\n' << end(); Scenario_names.insert(result.name); skip_whitespace_and_comments(in); assert(in.peek() == '['); // scenarios are take special 'code' strings so we need to ignore brackets // inside comments result.to_run = slurp_quoted(in); // delete [] delimiters assert(result.to_run.at(0) == '['); result.to_run.erase(0, 1); assert(result.to_run.at(SIZE(result.to_run)-1) == ']'); result.to_run.erase(SIZE(result.to_run)-1); return result; } void test_warn_on_redefine_scenario() { Trace_file = "warn_on_redefine_scenario"; Hide_warnings = true; Disable_redefine_warnings = true; run("recipe scenario-foo [\n 1:number <- copy 34\n]\nrecipe scenario-foo [\n 1:number <- copy 35\n]\n"); CHECK_TRACE_CONTENTS("warn: redefining recipe scenario-foo"); } void test_run() { Trace_file = "run"; run("recipe main [\n run [\n 1:number <- copy 13\n ]\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 13 in location 1"); } void bind_special_scenario_names(recipe_ordinal r) { // Special Scenario Variable Names(r) Name[r]["screen"] = SCREEN; Name[r]["console"] = CONSOLE; // End Special Scenario Variable Names(r) } void test_run_multiple() { Trace_file = "run_multiple"; run("recipe main [\n run [\n 1:number <- copy 13\n ]\n run [\n 2:number <- copy 13\n ]\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 13 in location 1mem: storing 13 in location 2"); } void test_memory_check() { Trace_file = "memory_check"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n memory-should-contain [\n 1 <- 13\n ]\n]\n"); CHECK_TRACE_CONTENTS("run: checking location 1error: expected location 1 to contain 13 but saw 0"); } void check_memory(const string& s) { istringstream in(s); in >> std::noskipws; set locations_checked; while (true) { skip_whitespace_and_comments(in); if (!has_data(in)) break; string lhs = next_word(in); if (!is_integer(lhs)) { check_type(lhs, in); continue; } long long int address = to_integer(lhs); skip_whitespace_and_comments(in); string _assign; in >> _assign; assert(_assign == "<-"); skip_whitespace_and_comments(in); double value = 0; in >> value; if (contains_key(locations_checked, address)) raise_error << "duplicate expectation for location " << address << '\n' << end(); trace(9999, "run") << "checking location " << address << end(); if (get_or_insert(Memory, address) != value) { if (Current_scenario && !Scenario_testing_scenario) { // genuine test in a mu file raise_error << "\nF - " << Current_scenario->name << ": expected location " << address << " to contain " << no_scientific(value) << " but saw " << no_scientific(get_or_insert(Memory, address)) << '\n' << end(); } else { // just testing scenario support raise_error << "expected location " << address << " to contain " << no_scientific(value) << " but saw " << no_scientific(get_or_insert(Memory, address)) << '\n' << end(); } if (!Scenario_testing_scenario) { Passed = false; ++Num_failures; } return; } locations_checked.insert(address); } } void check_type(const string& lhs, istream& in) { reagent x(lhs); if (x.type->name == "array" && x.type->right && x.type->right->name == "character" && !x.type->right->right) { x.set_value(to_integer(x.name)); skip_whitespace_and_comments(in); string _assign = next_word(in); assert(_assign == "<-"); skip_whitespace_and_comments(in); string literal = next_word(in); long long int address = x.value; // exclude quoting brackets assert(*literal.begin() == '['); literal.erase(literal.begin()); assert(*--literal.end() == ']'); literal.erase(--literal.end()); check_string(address, literal); return; } raise_error << "don't know how to check memory for " << lhs << '\n' << end(); } void check_string(long long int address, const string& literal) { trace(9999, "run") << "checking string length at " << address << end(); if (get_or_insert(Memory, address) != SIZE(literal)) { if (Current_scenario && !Scenario_testing_scenario) raise_error << "\nF - " << Current_scenario->name << ": expected location " << address << " to contain length " << SIZE(literal) << " of string [" << literal << "] but saw " << no_scientific(get_or_insert(Memory, address)) << " (" << read_mu_string(address) << ")\n" << end(); else raise_error << "expected location " << address << " to contain length " << SIZE(literal) << " of string [" << literal << "] but saw " << no_scientific(get_or_insert(Memory, address)) << '\n' << end(); if (!Scenario_testing_scenario) { Passed = false; ++Num_failures; } return; } ++address; // now skip length for (long long int i = 0; i < SIZE(literal); ++i) { trace(9999, "run") << "checking location " << address+i << end(); if (get_or_insert(Memory, address+i) != literal.at(i)) { if (Current_scenario && !Scenario_testing_scenario) { // genuine test in a mu file raise_error << "\nF - " << Current_scenario->name << ": expected location " << (address+i) << " to contain " << literal.at(i) << " but saw " << no_scientific(get_or_insert(Memory, address+i)) << " ('" << static_cast(get_or_insert(Memory, address+i)) << "')\n" << end(); } else { // just testing scenario support raise_error << "expected location " << (address+i) << " to contain " << literal.at(i) << " but saw " << no_scientific(get_or_insert(Memory, address+i)) << '\n' << end(); } if (!Scenario_testing_scenario) { Passed = false; ++Num_failures; } return; } } } void test_memory_check_multiple() { Trace_file = "memory_check_multiple"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n memory-should-contain [\n 1 <- 0\n 1 <- 0\n ]\n]\n"); CHECK_TRACE_CONTENTS("error: duplicate expectation for location 1"); } void test_memory_check_string_length() { Trace_file = "memory_check_string_length"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n 1:number <- copy 3\n 2:number <- copy 97 # 'a'\n 3:number <- copy 98 # 'b'\n 4:number <- copy 99 # 'c'\n memory-should-contain [\n 1:array:character <- [ab]\n ]\n]\n"); CHECK_TRACE_CONTENTS("error: expected location 1 to contain length 2 of string [ab] but saw 3"); } void test_memory_check_string() { Trace_file = "memory_check_string"; run("recipe main [\n 1:number <- copy 3\n 2:number <- copy 97 # 'a'\n 3:number <- copy 98 # 'b'\n 4:number <- copy 99 # 'c'\n memory-should-contain [\n 1:array:character <- [abc]\n ]\n]\n"); CHECK_TRACE_CONTENTS("run: checking string length at 1run: checking location 2run: checking location 3run: checking location 4"); } // Like runs of contiguous '+' lines, order is important. The trace checks // that the lines are present *and* in the specified sequence. (There can be // other lines in between.) void test_trace_check_fails() { Trace_file = "trace_check_fails"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n trace-should-contain [\n a: b\n a: d\n ]\n]\n"); CHECK_TRACE_CONTENTS("error: missing [b] in trace with label a"); } // simplified version of check_trace_contents() that emits errors rather // than just printing to stderr void check_trace(const string& expected) { Trace_stream->newline(); vector expected_lines = parse_trace(expected); if (expected_lines.empty()) return; long long int curr_expected_line = 0; for (vector::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) { if (expected_lines.at(curr_expected_line).label != p->label) continue; if (expected_lines.at(curr_expected_line).contents != trim(p->contents)) continue; // match ++curr_expected_line; if (curr_expected_line == SIZE(expected_lines)) return; } raise_error << "missing [" << expected_lines.at(curr_expected_line).contents << "] " << "in trace with label " << expected_lines.at(curr_expected_line).label << '\n' << end(); Passed = false; } vector parse_trace(const string& expected) { vector buf = split(expected, "\n"); vector result; for (long long int i = 0; i < SIZE(buf); ++i) { buf.at(i) = trim(buf.at(i)); if (buf.at(i).empty()) continue; long long int delim = buf.at(i).find(": "); result.push_back(trace_line(trim(buf.at(i).substr(0, delim)), trim(buf.at(i).substr(delim+2)))); } return result; } void test_trace_check_fails_in_nonfirst_line() { Trace_file = "trace_check_fails_in_nonfirst_line"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n run [\n trace 1, [a], [b]\n ]\n trace-should-contain [\n a: b\n a: d\n ]\n]\n"); CHECK_TRACE_CONTENTS("error: missing [d] in trace with label a"); } void test_trace_check_passes_silently() { Trace_file = "trace_check_passes_silently"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n run [\n trace 1, [a], [b]\n ]\n trace-should-contain [\n a: b\n ]\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("error: missing [b] in trace with label a"); CHECK_TRACE_COUNT("error", 0); } void test_trace_negative_check_fails() { Trace_file = "trace_negative_check_fails"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n run [\n trace 1, [a], [b]\n ]\n trace-should-not-contain [\n a: b\n ]\n]\n"); CHECK_TRACE_CONTENTS("error: unexpected [b] in trace with label a"); } // simplified version of check_trace_contents() that emits errors rather // than just printing to stderr bool check_trace_missing(const string& in) { Trace_stream->newline(); vector lines = parse_trace(in); for (long long int i = 0; i < SIZE(lines); ++i) { if (trace_count(lines.at(i).label, lines.at(i).contents) != 0) { raise_error << "unexpected [" << lines.at(i).contents << "] in trace with label " << lines.at(i).label << '\n' << end(); Passed = false; return false; } } return true; } void test_trace_negative_check_passes_silently() { Trace_file = "trace_negative_check_passes_silently"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n trace-should-not-contain [\n a: b\n ]\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("error: unexpected [b] in trace with label a"); CHECK_TRACE_COUNT("error", 0); } void test_trace_negative_check_fails_on_any_unexpected_line() { Trace_file = "trace_negative_check_fails_on_any_unexpected_line"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n run [\n trace 1, [a], [d]\n ]\n trace-should-not-contain [\n a: b\n a: d\n ]\n]\n"); CHECK_TRACE_CONTENTS("error: unexpected [d] in trace with label a"); } void test_trace_count_check() { Trace_file = "trace_count_check"; run("recipe main [\n run [\n trace 1, [a], [foo]\n ]\n check-trace-count-for-label 1, [a]\n]\n"); } void test_trace_count_check_2() { Trace_file = "trace_count_check_2"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n run [\n trace 1, [a], [foo]\n ]\n check-trace-count-for-label 2, [a]\n]\n"); CHECK_TRACE_CONTENTS("error: main: expected 2 lines in trace with label a in trace"); } void test_tangle_before() { Trace_file = "tangle_before"; run("recipe main [\n 1:number <- copy 0\n \n 3:number <- copy 0\n]\nbefore [\n 2:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1mem: storing 0 in location 2mem: storing 0 in location 3"); CHECK_TRACE_COUNT("mem", 3); } void insert_fragments(const recipe_ordinal r) { bool made_progress = true; long long int pass = 0; while (made_progress) { made_progress = false; // create a new vector because insertions invalidate iterators vector result; for (long long int i = 0; i < SIZE(get(Recipe, r).steps); ++i) { const instruction& inst = get(Recipe, r).steps.at(i); if (!inst.is_label || !is_waypoint(inst.label) || inst.tangle_done) { result.push_back(inst); continue; } inst.tangle_done = true; made_progress = true; Fragments_used.insert(inst.label); ostringstream prefix; prefix << '+' << get(Recipe, r).name << '_' << pass << '_' << i; // ok to use contains_key even though Before_fragments uses [], // because appending an empty recipe is a noop if (contains_key(Before_fragments, inst.label)) append_fragment(result, Before_fragments[inst.label].steps, prefix.str()); result.push_back(inst); if (contains_key(After_fragments, inst.label)) append_fragment(result, After_fragments[inst.label].steps, prefix.str()); } get(Recipe, r).steps.swap(result); ++pass; } } void append_fragment(vector& base, const vector& patch, const string prefix) { // append 'patch' to 'base' while keeping 'base' oblivious to any new jump // targets in 'patch' oblivious to 'base' by prepending 'prefix' to them. // we might tangle the same fragment at multiple points in a single recipe, // and we need to avoid duplicate jump targets. // so we'll keep jump targets local to the specific before/after fragment // that introduces them. set jump_targets; for (long long int i = 0; i < SIZE(patch); ++i) { const instruction& inst = patch.at(i); if (inst.is_label && is_jump_target(inst.label)) jump_targets.insert(inst.label); } for (long long int i = 0; i < SIZE(patch); ++i) { instruction inst = patch.at(i); if (inst.is_label) { if (contains_key(jump_targets, inst.label)) inst.label = prefix+inst.label; base.push_back(inst); continue; } for (long long int j = 0; j < SIZE(inst.ingredients); ++j) { reagent& x = inst.ingredients.at(j); if (!is_literal(x)) continue; if (x.type->name == "label" && contains_key(jump_targets, x.name)) x.name = prefix+x.name; } base.push_back(inst); } } bool is_waypoint(string label) { return *label.begin() == '<' && *label.rbegin() == '>'; } void check_insert_fragments(unused recipe_ordinal) { if (Transform_check_insert_fragments_Ran) return; Transform_check_insert_fragments_Ran = true; for (map::iterator p = Before_fragments.begin(); p != Before_fragments.end(); ++p) { if (!contains_key(Fragments_used, p->first)) raise_error << "could not locate insert before " << p->first << '\n' << end(); } for (map::iterator p = After_fragments.begin(); p != After_fragments.end(); ++p) { if (!contains_key(Fragments_used, p->first)) raise_error << "could not locate insert after " << p->first << '\n' << end(); } } void test_tangle_before_and_after() { Trace_file = "tangle_before_and_after"; run("recipe main [\n 1:number <- copy 0\n \n 4:number <- copy 0\n]\nbefore [\n 2:number <- copy 0\n]\nafter [\n 3:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1mem: storing 0 in location 2mem: storing 0 in location 3mem: storing 0 in location 4"); CHECK_TRACE_COUNT("mem", 4); } void test_tangle_ignores_jump_target() { Trace_file = "tangle_ignores_jump_target"; Hide_errors = true; run("recipe main [\n 1:number <- copy 0\n +label1\n 4:number <- copy 0\n]\nbefore +label1 [\n 2:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("error: can't tangle before label +label1"); } void test_tangle_keeps_labels_separate() { Trace_file = "tangle_keeps_labels_separate"; run("recipe main [\n 1:number <- copy 0\n \n \n 6:number <- copy 0\n]\nbefore [\n 2:number <- copy 0\n]\nafter [\n 3:number <- copy 0\n]\nbefore [\n 4:number <- copy 0\n]\nafter [\n 5:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1mem: storing 0 in location 2mem: storing 0 in location 3mem: storing 0 in location 4mem: storing 0 in location 5mem: storing 0 in location 6"); CHECK_TRACE_COUNT("mem", 6); } void test_tangle_stacks_multiple_fragments() { Trace_file = "tangle_stacks_multiple_fragments"; run("recipe main [\n 1:number <- copy 0\n \n 6:number <- copy 0\n]\nbefore [\n 2:number <- copy 0\n]\nafter [\n 3:number <- copy 0\n]\nbefore [\n 4:number <- copy 0\n]\nafter [\n 5:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1mem: storing 0 in location 2mem: storing 0 in location 4mem: storing 0 in location 5mem: storing 0 in location 3mem: storing 0 in location 6"); CHECK_TRACE_COUNT("mem", 6); } void test_tangle_supports_fragments_with_multiple_instructions() { Trace_file = "tangle_supports_fragments_with_multiple_instructions"; run("recipe main [\n 1:number <- copy 0\n \n 6:number <- copy 0\n]\nbefore [\n 2:number <- copy 0\n 3:number <- copy 0\n]\nafter [\n 4:number <- copy 0\n 5:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1mem: storing 0 in location 2mem: storing 0 in location 3mem: storing 0 in location 4mem: storing 0 in location 5mem: storing 0 in location 6"); CHECK_TRACE_COUNT("mem", 6); } void test_tangle_tangles_into_all_labels_with_same_name() { Trace_file = "tangle_tangles_into_all_labels_with_same_name"; run("recipe main [\n 1:number <- copy 10\n \n 4:number <- copy 10\n recipe2\n]\nrecipe recipe2 [\n 1:number <- copy 11\n \n 4:number <- copy 11\n]\nbefore [\n 2:number <- copy 12\n]\nafter [\n 3:number <- copy 12\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 10 in location 1mem: storing 12 in location 2mem: storing 12 in location 3mem: storing 10 in location 4mem: storing 11 in location 1mem: storing 12 in location 2mem: storing 12 in location 3mem: storing 11 in location 4"); CHECK_TRACE_COUNT("mem", 8); } void test_tangle_tangles_into_all_labels_with_same_name_2() { Trace_file = "tangle_tangles_into_all_labels_with_same_name_2"; run("recipe main [\n 1:number <- copy 10\n \n \n 4:number <- copy 10\n]\nbefore [\n 2:number <- copy 12\n]\nafter [\n 3:number <- copy 12\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 10 in location 1mem: storing 12 in location 2mem: storing 12 in location 3mem: storing 12 in location 2mem: storing 12 in location 3mem: storing 10 in location 4"); CHECK_TRACE_COUNT("mem", 6); } void test_tangle_tangles_into_all_labels_with_same_name_3() { Trace_file = "tangle_tangles_into_all_labels_with_same_name_3"; run("recipe main [\n 1:number <- copy 10\n \n \n 4:number <- copy 10\n]\nbefore [\n 2:number <- copy 12\n]\nafter [\n 3:number <- copy 12\n]\nafter [\n \n]\n"); CHECK_TRACE_CONTENTS("mem: storing 10 in location 1mem: storing 12 in location 2mem: storing 12 in location 3mem: storing 12 in location 2mem: storing 12 in location 3mem: storing 10 in location 4"); CHECK_TRACE_COUNT("mem", 6); } void test_tangle_handles_jump_target_inside_fragment() { Trace_file = "tangle_handles_jump_target_inside_fragment"; run("recipe main [\n 1:number <- copy 10\n \n 4:number <- copy 10\n]\nbefore [\n jump +label2:label\n 2:number <- copy 12\n +label2\n 3:number <- copy 12\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 10 in location 1mem: storing 12 in location 3mem: storing 10 in location 4"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 12 in label 2"); CHECK_TRACE_COUNT("mem", 3); } void test_tangle_renames_jump_target() { Trace_file = "tangle_renames_jump_target"; run("recipe main [\n 1:number <- copy 10\n \n +label2\n 4:number <- copy 10\n]\nbefore [\n jump +label2:label\n 2:number <- copy 12\n +label2 # renamed\n 3:number <- copy 12\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 10 in location 1mem: storing 12 in location 3mem: storing 10 in location 4"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 12 in label 2"); CHECK_TRACE_COUNT("mem", 3); } void test_tangle_jump_to_base_recipe() { Trace_file = "tangle_jump_to_base_recipe"; run("recipe main [\n 1:number <- copy 10\n \n +label2\n 4:number <- copy 10\n]\nbefore [\n jump +label2:label\n 2:number <- copy 12\n 3:number <- copy 12\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 10 in location 1mem: storing 10 in location 4"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 12 in label 2"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 12 in location 3"); CHECK_TRACE_COUNT("mem", 2); } void rewrite_stashes_to_text(recipe_ordinal r) { recipe& caller = get(Recipe, r); trace(9991, "transform") << "--- rewrite 'stash' instructions in recipe " << caller.name << end(); if (contains_named_locations(caller)) rewrite_stashes_to_text_named(caller); // in recipes without named locations, 'stash' is still not extensible } bool contains_named_locations(const recipe& caller) { for (long long int i = 0; i < SIZE(caller.steps); ++i) { const instruction& inst = caller.steps.at(i); for (long long int in = 0; in < SIZE(inst.ingredients); ++in) if (is_named_location(inst.ingredients.at(in))) return true; for (long long int out = 0; out < SIZE(inst.products); ++out) if (is_named_location(inst.products.at(out))) return true; } return false; } void rewrite_stashes_to_text_named(recipe& caller) { static long long int stash_instruction_idx = 0; vector new_instructions; for (long long int i = 0; i < SIZE(caller.steps); ++i) { instruction& inst = caller.steps.at(i); if (inst.name == "stash") { for (long long int j = 0; j < SIZE(inst.ingredients); ++j) { if (is_literal(inst.ingredients.at(j))) continue; if (is_mu_string(inst.ingredients.at(j))) continue; instruction def; def.name = "to-text-line"; def.ingredients.push_back(inst.ingredients.at(j)); ostringstream ingredient_name; ingredient_name << "stash_" << stash_instruction_idx << '_' << j << ":address:shared:array:character"; def.products.push_back(reagent(ingredient_name.str())); new_instructions.push_back(def); inst.ingredients.at(j).clear(); // reclaim old memory inst.ingredients.at(j) = reagent(ingredient_name.str()); } } new_instructions.push_back(inst); } new_instructions.swap(caller.steps); } void test_dilated_reagent() { Trace_file = "dilated_reagent"; load("recipe main [\n {1: number, foo: bar} <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("parse: product: 1: \"number\", {\"foo\": \"bar\"}"); } void test_load_trailing_space_after_curly_bracket() { Trace_file = "load_trailing_space_after_curly_bracket"; load("recipe main [\n # line below has a space at the end\n { \n]\n# successfully parsed\n"); } void test_dilated_reagent_with_comment() { Trace_file = "dilated_reagent_with_comment"; Hide_errors = true; run("recipe main [\n {1: number, foo: bar} <- copy 34 # test comment\n]\n"); CHECK_TRACE_CONTENTS("parse: product: 1: \"number\", {\"foo\": \"bar\"}"); CHECK_TRACE_COUNT("error", 0); } void test_dilated_reagent_with_comment_immediately_following() { Trace_file = "dilated_reagent_with_comment_immediately_following"; Hide_errors = true; run("recipe main [\n 1:number <- copy {34: literal} # test comment\n]\n"); CHECK_TRACE_COUNT("error", 0); } // A curly is considered a label if it's the last thing on a line. Dilated // reagents should remain all on one line. bool start_of_dilated_reagent(istream& in) { if (in.peek() != '{') return false; long long int pos = in.tellg(); in.get(); // slurp '{' skip_whitespace_but_not_newline(in); char next = in.peek(); in.seekg(pos); return next != '\n'; } // Assume the first letter is an open bracket, and read everything until the // matching close bracket. // We balance {} () and []. And we skip one character after '\'. string slurp_balanced_bracket(istream& in) { ostringstream result; char c; list open_brackets; while (in >> c) { if (c == '\\') { // always silently skip the next character result << c; if (!(in >> c)) break; result << c; continue; } if (c == '(') open_brackets.push_back(c); if (c == ')') { assert(open_brackets.back() == '('); open_brackets.pop_back(); } if (c == '[') open_brackets.push_back(c); if (c == ']') { assert(open_brackets.back() == '['); open_brackets.pop_back(); } if (c == '{') open_brackets.push_back(c); if (c == '}') { assert(open_brackets.back() == '{'); open_brackets.pop_back(); } result << c; if (open_brackets.empty()) break; } skip_whitespace_and_comments_but_not_newline(in); return result.str(); } string slurp_key(istream& in) { string result = next_word(in); while (!result.empty() && *result.rbegin() == ':') strip_last(result); while (isspace(in.peek()) || in.peek() == ':') in.get(); return result; } // So far instructions can only contain linear lists of properties. Now we add // support for more complex trees of properties in dilated reagents. This will // come in handy later for expressing complex types, like "a dictionary from // (address to array of charaters) to (list of numbers)". void test_dilated_reagent_with_nested_brackets() { Trace_file = "dilated_reagent_with_nested_brackets"; run("recipe main [\n {1: number, foo: (bar (baz quux))} <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("parse: product: 1: \"number\", {\"foo\": (\"bar\" (\"baz\" \"quux\"))}"); } string_tree* parse_string_tree(string_tree* s) { assert(!s->left && !s->right); if (s->value.at(0) != '(') return s; string_tree* result = parse_string_tree(s->value); delete s; return result; } string_tree* parse_string_tree(const string& s) { istringstream in(s); in >> std::noskipws; return parse_string_tree(in); } string_tree* parse_string_tree(istream& in) { skip_whitespace_but_not_newline(in); if (!has_data(in)) return NULL; if (in.peek() == ')') { in.get(); return NULL; } if (in.peek() != '(') { string_tree* result = new string_tree(next_word(in)); return result; } in.get(); // skip '(' string_tree* result = NULL; string_tree** curr = &result; while (in.peek() != ')') { assert(has_data(in)); *curr = new string_tree(""); skip_whitespace_but_not_newline(in); if (in.peek() == '(') (*curr)->left = parse_string_tree(in); else (*curr)->value = next_word(in); curr = &(*curr)->right; } in.get(); // skip ')' return result; } void test_dilated_reagent_with_type_tree() { Trace_file = "dilated_reagent_with_type_tree"; Hide_errors = true; // 'map' isn't defined yet run("recipe main [\n {1: (foo (address array character) (bar number))} <- copy 34\n]\n# just to avoid errors\ncontainer foo [\n]\ncontainer bar [\n]\n"); CHECK_TRACE_CONTENTS("parse: product: 1: (\"foo\" (\"address\" \"array\" \"character\") (\"bar\" \"number\"))"); } void test_dilated_reagent_with_new() { Trace_file = "dilated_reagent_with_new"; run("recipe main [\n x:address:shared:address:number <- new {(address number): type}\n]\n"); CHECK_TRACE_CONTENTS("new: size of (\"address\" \"number\") is 1"); } void test_recipe_with_header() { Trace_file = "recipe_with_header"; run("recipe main [\n 1:number/raw <- add2 3, 5\n]\nrecipe add2 x:number, y:number -> z:number [\n local-scope\n load-ingredients\n z:number <- add x, y\n reply z\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 8 in location 1"); } void load_recipe_header(istream& in, recipe& result) { result.has_header = true; while (has_data(in) && in.peek() != '[' && in.peek() != '\n') { string s = next_word(in); if (s == "->") break; result.ingredients.push_back(reagent(s)); trace(9999, "parse") << "header ingredient: " << result.ingredients.back().original_string << end(); skip_whitespace_but_not_newline(in); } while (has_data(in) && in.peek() != '[' && in.peek() != '\n') { string s = next_word(in); result.products.push_back(reagent(s)); trace(9999, "parse") << "header product: " << result.products.back().original_string << end(); skip_whitespace_but_not_newline(in); } // there can only ever be one variant for main if (result.name != "main" && contains_key(Recipe_ordinal, result.name)) { const recipe_ordinal r = get(Recipe_ordinal, result.name); //? cerr << result.name << ": " << contains_key(Recipe, r) << (contains_key(Recipe, r) ? get(Recipe, r).has_header : 0) << matching_variant_name(result) << '\n'; if (!contains_key(Recipe, r) || get(Recipe, r).has_header) { string new_name = matching_variant_name(result); if (new_name.empty()) { // variant doesn't already exist new_name = next_unused_recipe_name(result.name); put(Recipe_ordinal, new_name, Next_recipe_ordinal++); get_or_insert(Recipe_variants, result.name).push_back(get(Recipe_ordinal, new_name)); } trace(9999, "load") << "switching " << result.name << " to " << new_name << end(); result.name = new_name; //? cerr << "=> " << new_name << '\n'; } } else { // save first variant put(Recipe_ordinal, result.name, Next_recipe_ordinal++); get_or_insert(Recipe_variants, result.name).push_back(get(Recipe_ordinal, result.name)); } // End Load Recipe Header(result) } void test_recipe_handles_stray_comma() { Trace_file = "recipe_handles_stray_comma"; run("recipe main [\n 1:number/raw <- add2 3, 5\n]\nrecipe add2 x:number, y:number -> z:number, [\n local-scope\n load-ingredients\n z:number <- add x, y\n reply z\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 8 in location 1"); } void test_recipe_handles_stray_comma_2() { Trace_file = "recipe_handles_stray_comma_2"; run("recipe main [\n foo\n]\nrecipe foo, [\n 1:number/raw <- add 2, 2\n]\nrecipe bar [\n 1:number/raw <- add 2, 3\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 1"); } void test_recipe_handles_missing_bracket() { Trace_file = "recipe_handles_missing_bracket"; Hide_errors = true; run("recipe main\n]\n"); CHECK_TRACE_CONTENTS("error: recipe body must begin with '['"); } void test_recipe_handles_missing_bracket_2() { Trace_file = "recipe_handles_missing_bracket_2"; Hide_errors = true; run("recipe main\n local-scope\n {\n }\n]\n# doesn't overflow line when reading header\n"); CHECK_TRACE_DOESNT_CONTAIN("parse: header ingredient: local-scope"); run(""); CHECK_TRACE_CONTENTS("error: recipe body must begin with '['"); } void test_recipe_handles_missing_bracket_3() { Trace_file = "recipe_handles_missing_bracket_3"; Hide_errors = true; run("recipe main # comment\n local-scope\n {\n }\n]\n# doesn't overflow line when reading header\n"); CHECK_TRACE_DOESNT_CONTAIN("parse: header ingredient: local-scope"); run(""); CHECK_TRACE_CONTENTS("error: recipe body must begin with '['"); } void test_recipe_without_ingredients_or_products_has_header() { Trace_file = "recipe_without_ingredients_or_products_has_header"; run("recipe test [\n 1:number <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("parse: recipe test has a header"); } void test_show_clear_error_on_bad_call() { Trace_file = "show_clear_error_on_bad_call"; Hide_errors = true; run("recipe main [\n 1:number <- foo 34\n]\nrecipe foo x:boolean -> y:number [\n local-scope\n load-ingredients\n reply 35\n]\n"); CHECK_TRACE_CONTENTS("error: main: ingredient 0 has the wrong type at '1:number <- foo 34'"); } void test_show_clear_error_on_bad_call_2() { Trace_file = "show_clear_error_on_bad_call_2"; Hide_errors = true; run("recipe main [\n 1:boolean <- foo 34\n]\nrecipe foo x:number -> y:number [\n local-scope\n load-ingredients\n reply x\n]\n"); CHECK_TRACE_CONTENTS("error: main: product 0 has the wrong type at '1:boolean <- foo 34'"); } void check_calls_against_header(const recipe_ordinal r) { trace(9991, "transform") << "--- type-check calls inside recipe " << get(Recipe, r).name << end(); const recipe& caller = get(Recipe, r); for (long long int i = 0; i < SIZE(caller.steps); ++i) { const instruction& inst = caller.steps.at(i); if (inst.operation < MAX_PRIMITIVE_RECIPES) continue; const recipe& callee = get(Recipe, inst.operation); if (!callee.has_header) continue; for (long int i = 0; i < min(SIZE(inst.ingredients), SIZE(callee.ingredients)); ++i) { // ingredients coerced from call to callee if (!types_coercible(callee.ingredients.at(i), inst.ingredients.at(i))) raise_error << maybe(caller.name) << "ingredient " << i << " has the wrong type at '" << to_string(inst) << "'\n" << end(); if (is_unique_address(inst.ingredients.at(i))) raise << maybe(caller.name) << "try to avoid passing non-shared addresses into calls, like ingredient " << i << " at '" << to_string(inst) << "'\n" << end(); } for (long int i = 0; i < min(SIZE(inst.products), SIZE(callee.products)); ++i) { if (is_dummy(inst.products.at(i))) continue; // products coerced from callee to call if (!types_coercible(inst.products.at(i), callee.products.at(i))) raise_error << maybe(caller.name) << "product " << i << " has the wrong type at '" << to_string(inst) << "'\n" << end(); if (is_unique_address(inst.products.at(i))) raise << maybe(caller.name) << "try to avoid getting non-shared addresses out of calls, like product " << i << " at '" << to_string(inst) << "'\n" << end(); } } } bool is_unique_address(reagent x) { if (!canonize_type(x)) return false; if (!x.type) return false; if (x.type->value != get(Type_ordinal, "address")) return false; if (!x.type->right) return true; return x.type->right->value != get(Type_ordinal, "shared"); } void test_warn_on_calls_with_addresses() { Trace_file = "warn_on_calls_with_addresses"; Hide_warnings= true; run("recipe main [\n 1:address:number <- copy 3/unsafe\n foo 1:address:number\n]\nrecipe foo x:address:number [\n local-scope\n load-ingredients\n]\n"); CHECK_TRACE_CONTENTS("warn: main: try to avoid passing non-shared addresses into calls, like ingredient 0 at 'foo 1:address:number'"); } void test_warn_on_calls_with_addresses_2() { Trace_file = "warn_on_calls_with_addresses_2"; Hide_warnings= true; run("recipe main [\n 1:address:number <- foo\n]\nrecipe foo -> x:address:number [\n local-scope\n load-ingredients\n x <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("warn: main: try to avoid getting non-shared addresses out of calls, like product 0 at '1:address:number <- foo '"); } void test_recipe_headers_are_checked() { Trace_file = "recipe_headers_are_checked"; Hide_errors = true; transform("recipe add2 x:number, y:number -> z:number [\n local-scope\n load-ingredients\n z:address:number <- copy 0/unsafe\n reply z\n]\n"); CHECK_TRACE_CONTENTS("error: add2: replied with the wrong type at 'reply z'"); } void check_reply_instructions_against_header(const recipe_ordinal r) { const recipe& caller_recipe = get(Recipe, r); if (!caller_recipe.has_header) return; trace(9991, "transform") << "--- checking reply instructions against header for " << caller_recipe.name << end(); for (long long int i = 0; i < SIZE(caller_recipe.steps); ++i) { const instruction& inst = caller_recipe.steps.at(i); if (inst.name != "reply") continue; if (SIZE(caller_recipe.products) != SIZE(inst.ingredients)) { raise_error << maybe(caller_recipe.name) << "replied with the wrong number of products at '" << to_string(inst) << "'\n" << end(); continue; } for (long long int i = 0; i < SIZE(caller_recipe.products); ++i) { if (!types_match(caller_recipe.products.at(i), inst.ingredients.at(i))) raise_error << maybe(caller_recipe.name) << "replied with the wrong type at '" << to_string(inst) << "'\n" << end(); } } } void test_recipe_headers_are_checked_2() { Trace_file = "recipe_headers_are_checked_2"; Hide_errors = true; transform("recipe add2 x:number, y:number [\n local-scope\n load-ingredients\n z:address:number <- copy 0/unsafe\n reply z\n]\n"); CHECK_TRACE_CONTENTS("error: add2: replied with the wrong number of products at 'reply z'"); } void test_recipe_headers_check_for_duplicate_names() { Trace_file = "recipe_headers_check_for_duplicate_names"; Hide_errors = true; transform("recipe add2 x:number, x:number -> z:number [\n local-scope\n load-ingredients\n reply z\n]\n"); CHECK_TRACE_CONTENTS("error: add2: x can't repeat in the ingredients"); } void check_header_ingredients(const recipe_ordinal r) { recipe& caller_recipe = get(Recipe, r); if (caller_recipe.products.empty()) return; caller_recipe.ingredient_index.clear(); trace(9991, "transform") << "--- checking reply instructions against header for " << caller_recipe.name << end(); for (long long int i = 0; i < SIZE(caller_recipe.ingredients); ++i) { if (contains_key(caller_recipe.ingredient_index, caller_recipe.ingredients.at(i).name)) raise_error << maybe(caller_recipe.name) << caller_recipe.ingredients.at(i).name << " can't repeat in the ingredients\n" << end(); put(caller_recipe.ingredient_index, caller_recipe.ingredients.at(i).name, i); } } void test_deduce_instruction_types_from_recipe_header() { Trace_file = "deduce_instruction_types_from_recipe_header"; run("recipe main [\n 1:number/raw <- add2 3, 5\n]\nrecipe add2 x:number, y:number -> z:number [\n local-scope\n load-ingredients\n z <- add x, y # no type for z\n reply z\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 8 in location 1"); } void deduce_types_from_header(const recipe_ordinal r) { recipe& caller_recipe = get(Recipe, r); if (caller_recipe.products.empty()) return; trace(9991, "transform") << "--- deduce types from header for " << caller_recipe.name << end(); map header_type; for (long long int i = 0; i < SIZE(caller_recipe.ingredients); ++i) { put(header_type, caller_recipe.ingredients.at(i).name, caller_recipe.ingredients.at(i).type); //TODO: replace to_string with names_to_string trace(9993, "transform") << "type of " << caller_recipe.ingredients.at(i).name << " is " << to_string(caller_recipe.ingredients.at(i).type) << end(); } for (long long int i = 0; i < SIZE(caller_recipe.products); ++i) { put(header_type, caller_recipe.products.at(i).name, caller_recipe.products.at(i).type); trace(9993, "transform") << "type of " << caller_recipe.products.at(i).name << " is " << to_string(caller_recipe.products.at(i).type) << end(); } for (long long int i = 0; i < SIZE(caller_recipe.steps); ++i) { instruction& inst = caller_recipe.steps.at(i); trace(9992, "transform") << "instruction: " << to_string(inst) << end(); for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (inst.ingredients.at(i).type) continue; if (header_type.find(inst.ingredients.at(i).name) == header_type.end()) continue; if (!inst.ingredients.at(i).type) inst.ingredients.at(i).type = new type_tree(*get(header_type, inst.ingredients.at(i).name)); trace(9993, "transform") << "type of " << inst.ingredients.at(i).name << " is " << to_string(inst.ingredients.at(i).type) << end(); } for (long long int i = 0; i < SIZE(inst.products); ++i) { trace(9993, "transform") << " product: " << to_string(inst.products.at(i)) << end(); if (inst.products.at(i).type) continue; if (header_type.find(inst.products.at(i).name) == header_type.end()) continue; if (!inst.products.at(i).type) inst.products.at(i).type = new type_tree(*get(header_type, inst.products.at(i).name)); trace(9993, "transform") << "type of " << inst.products.at(i).name << " is " << to_string(inst.products.at(i).type) << end(); } } } void test_reply_based_on_header() { Trace_file = "reply_based_on_header"; run("recipe main [\n 1:number/raw <- add2 3, 5\n]\nrecipe add2 x:number, y:number -> z:number [\n local-scope\n load-ingredients\n z <- add x, y\n reply\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 8 in location 1"); } void fill_in_reply_ingredients(recipe_ordinal r) { recipe& caller_recipe = get(Recipe, r); if (!caller_recipe.has_header) return; trace(9991, "transform") << "--- fill in reply ingredients from header for recipe " << caller_recipe.name << end(); for (long long int i = 0; i < SIZE(caller_recipe.steps); ++i) { instruction& inst = caller_recipe.steps.at(i); if (inst.name == "reply") add_header_products(inst, caller_recipe); } // fall through reply if (caller_recipe.steps.at(SIZE(caller_recipe.steps)-1).name != "reply") { instruction inst; inst.name = "reply"; add_header_products(inst, caller_recipe); caller_recipe.steps.push_back(inst); } } void add_header_products(instruction& inst, const recipe& caller_recipe) { assert(inst.name == "reply"); // collect any products with the same names as ingredients for (long long int i = 0; i < SIZE(caller_recipe.products); ++i) { // if the ingredient is missing, add it from the header if (SIZE(inst.ingredients) == i) inst.ingredients.push_back(caller_recipe.products.at(i)); // if it's missing /same_as_ingredient, try to fill it in if (contains_key(caller_recipe.ingredient_index, caller_recipe.products.at(i).name) && !has_property(inst.ingredients.at(i), "same_as_ingredient")) { ostringstream same_as_ingredient; same_as_ingredient << get(caller_recipe.ingredient_index, caller_recipe.products.at(i).name); inst.ingredients.at(i).properties.push_back(pair("same-as-ingredient", new string_tree(same_as_ingredient.str()))); } } } void test_explicit_reply_ignores_header() { Trace_file = "explicit_reply_ignores_header"; run("recipe main [\n 1:number/raw, 2:number/raw <- add2 3, 5\n]\nrecipe add2 a:number, b:number -> y:number, z:number [\n local-scope\n load-ingredients\n y <- add a, b\n z <- subtract a, b\n reply a, z\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 3 in location 1mem: storing -2 in location 2"); } void test_reply_on_fallthrough_based_on_header() { Trace_file = "reply_on_fallthrough_based_on_header"; run("recipe main [\n 1:number/raw <- add2 3, 5\n]\nrecipe add2 x:number, y:number -> z:number [\n local-scope\n load-ingredients\n z <- add x, y\n]\n"); CHECK_TRACE_CONTENTS("transform: instruction: reply z:numbermem: storing 8 in location 1"); } void test_reply_on_fallthrough_already_exists() { Trace_file = "reply_on_fallthrough_already_exists"; run("recipe main [\n 1:number/raw <- add2 3, 5\n]\nrecipe add2 x:number, y:number -> z:number [\n local-scope\n load-ingredients\n z <- add x, y # no type for z\n reply z\n]\n"); CHECK_TRACE_CONTENTS("transform: instruction: reply z"); CHECK_TRACE_DOESNT_CONTAIN("transform: instruction: reply z:number"); run(""); CHECK_TRACE_CONTENTS("mem: storing 8 in location 1"); } void test_recipe_headers_perform_same_ingredient_check() { Trace_file = "recipe_headers_perform_same_ingredient_check"; Hide_errors = true; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 34\n 3:number <- add2 1:number, 2:number\n]\nrecipe add2 x:number, y:number -> x:number [\n local-scope\n load-ingredients\n]\n"); CHECK_TRACE_CONTENTS("error: main: '3:number <- add2 1:number, 2:number' should write to 1:number rather than 3:number"); } void test_static_dispatch() { Trace_file = "static_dispatch"; run("recipe main [\n 7:number/raw <- test 3\n]\nrecipe test a:number -> z:number [\n z <- copy 1\n]\nrecipe test a:number, b:number -> z:number [\n z <- copy 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 7"); } string matching_variant_name(const recipe& rr) { const vector& variants = get_or_insert(Recipe_variants, rr.name); for (long long int i = 0; i < SIZE(variants); ++i) { if (!contains_key(Recipe, variants.at(i))) continue; const recipe& candidate = get(Recipe, variants.at(i)); if (!all_reagents_match(rr, candidate)) continue; return candidate.name; } return ""; } bool all_reagents_match(const recipe& r1, const recipe& r2) { if (SIZE(r1.ingredients) != SIZE(r2.ingredients)) return false; if (SIZE(r1.products) != SIZE(r2.products)) return false; for (long long int i = 0; i < SIZE(r1.ingredients); ++i) { if (!deeply_equal_type_names(r1.ingredients.at(i), r2.ingredients.at(i))) { return false; } } for (long long int i = 0; i < SIZE(r1.products); ++i) { if (!deeply_equal_type_names(r1.products.at(i), r2.products.at(i))) { return false; } } return true; } bool deeply_equal_type_names(const reagent& a, const reagent& b) { return deeply_equal_type_names(a.type, b.type); } bool deeply_equal_type_names(const type_tree* a, const type_tree* b) { if (!a) return !b; if (!b) return !a; if (a->name == "literal" && b->name == "literal") return true; if (a->name == "literal") return Literal_type_names.find(b->name) != Literal_type_names.end(); if (b->name == "literal") return Literal_type_names.find(a->name) != Literal_type_names.end(); return a->name == b->name && deeply_equal_type_names(a->left, b->left) && deeply_equal_type_names(a->right, b->right); } string next_unused_recipe_name(const string& recipe_name) { for (long long int i = 2; ; ++i) { ostringstream out; out << recipe_name << '_' << i; if (!contains_key(Recipe_ordinal, out.str())) return out.str(); } } void test_static_dispatch_picks_most_similar_variant() { Trace_file = "static_dispatch_picks_most_similar_variant"; run("recipe main [\n 7:number/raw <- test 3, 4, 5\n]\nrecipe test a:number -> z:number [\n z <- copy 1\n]\nrecipe test a:number, b:number -> z:number [\n z <- copy 2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 7"); } void resolve_ambiguous_calls(recipe_ordinal r) { cerr << "DDD " << get(Recipe, r).name << ": " << contains_key(Type_ordinal, "_elem") << '\n'; recipe& caller_recipe = get(Recipe, r); trace(9991, "transform") << "--- resolve ambiguous calls for recipe " << caller_recipe.name << end(); for (long long int index = 0; index < SIZE(caller_recipe.steps); ++index) { instruction& inst = caller_recipe.steps.at(index); if (inst.is_label) continue; if (non_ghost_size(get_or_insert(Recipe_variants, inst.name)) == 0) continue; trace(9992, "transform") << "instruction " << inst.original_string << end(); resolve_stack.push_front(call(r)); resolve_stack.front().running_step_index = index; cerr << "EEE " << contains_key(Type_ordinal, "_elem") << '\n'; string new_name = best_variant(inst, caller_recipe); if (!new_name.empty()) inst.name = new_name; assert(resolve_stack.front().running_recipe == r); assert(resolve_stack.front().running_step_index == index); resolve_stack.pop_front(); } if (contains_key(Type_ordinal, "_elem")) { cerr << "ZZZ " << get(Recipe, r).name << '\n'; exit(0); } } string best_variant(instruction& inst, const recipe& caller_recipe) { cerr << "FFF " << contains_key(Type_ordinal, "_elem") << '\n'; vector& variants = get(Recipe_variants, inst.name); vector candidates; // Static Dispatch Phase 1 candidates = strictly_matching_variants(inst, variants); if (!candidates.empty()) return best_variant(inst, candidates).name; // Static Dispatch Phase 2 (shape-shifting recipes in a later layer) candidates = strictly_matching_shape_shifting_variants(inst, variants); if (!candidates.empty()) { recipe_ordinal exemplar = best_shape_shifting_variant(inst, candidates); trace(9992, "transform") << "found variant to specialize: " << exemplar << ' ' << get(Recipe, exemplar).name << end(); cerr << "GGG " << contains_key(Type_ordinal, "_elem") << '\n'; recipe_ordinal new_recipe_ordinal = new_variant(exemplar, inst, caller_recipe); cerr << "XXX " << contains_key(Type_ordinal, "_elem") << '\n'; if (new_recipe_ordinal == 0) goto skip_shape_shifting_variants; variants.push_back(new_recipe_ordinal); // side-effect recipe& variant = get(Recipe, new_recipe_ordinal); // perform all transforms on the new specialization cerr << "YYY " << contains_key(Type_ordinal, "_elem") << '\n'; if (!variant.steps.empty()) { trace(9992, "transform") << "transforming new specialization: " << variant.name << end(); for (long long int t = 0; t < SIZE(Transform); ++t) { (*Transform.at(t))(new_recipe_ordinal); } } variant.transformed_until = SIZE(Transform)-1; trace(9992, "transform") << "new specialization: " << variant.name << end(); return variant.name; } skip_shape_shifting_variants:; // End Static Dispatch Phase 2 // Static Dispatch Phase 3 candidates = strictly_matching_variants_except_literal_against_boolean(inst, variants); if (!candidates.empty()) return best_variant(inst, candidates).name; // Static Dispatch Phase 4 candidates = matching_variants(inst, variants); if (!candidates.empty()) return best_variant(inst, candidates).name; // error messages if (get(Recipe_ordinal, inst.name) >= MAX_PRIMITIVE_RECIPES) { // we currently don't check types for primitive variants raise_error << maybe(caller_recipe.name) << "failed to find a matching call for '" << to_string(inst) << "'\n" << end(); for (list::iterator p = /*skip*/++resolve_stack.begin(); p != resolve_stack.end(); ++p) { const recipe& specializer_recipe = get(Recipe, p->running_recipe); const instruction& specializer_inst = specializer_recipe.steps.at(p->running_step_index); if (specializer_recipe.name != "interactive") raise_error << " (from '" << to_string(specializer_inst) << "' in " << specializer_recipe.name << ")\n" << end(); else raise_error << " (from '" << to_string(specializer_inst) << "')\n" << end(); // One special-case to help with the rewrite_stash transform. (cross-layer) if (specializer_inst.products.at(0).name.find("stash_") == 0) { instruction stash_inst; if (next_stash(*p, &stash_inst)) { if (specializer_recipe.name != "interactive") raise_error << " (part of '" << stash_inst.original_string << "' in " << specializer_recipe.name << ")\n" << end(); else raise_error << " (part of '" << stash_inst.original_string << "')\n" << end(); } } } } return ""; } // phase 1 vector strictly_matching_variants(const instruction& inst, vector& variants) { vector result; for (long long int i = 0; i < SIZE(variants); ++i) { if (variants.at(i) == -1) continue; trace(9992, "transform") << "checking variant (strict) " << i << ": " << header_label(variants.at(i)) << end(); if (all_header_reagents_strictly_match(inst, get(Recipe, variants.at(i)))) result.push_back(variants.at(i)); } return result; } bool all_header_reagents_strictly_match(const instruction& inst, const recipe& variant) { for (long long int i = 0; i < min(SIZE(inst.ingredients), SIZE(variant.ingredients)); ++i) { if (!types_strictly_match(variant.ingredients.at(i), inst.ingredients.at(i))) { trace(9993, "transform") << "strict match failed: ingredient " << i << end(); return false; } } for (long long int i = 0; i < min(SIZE(inst.products), SIZE(variant.products)); ++i) { if (is_dummy(inst.products.at(i))) continue; if (!types_strictly_match(variant.products.at(i), inst.products.at(i))) { trace(9993, "transform") << "strict match failed: product " << i << end(); return false; } } return true; } // phase 3 vector strictly_matching_variants_except_literal_against_boolean(const instruction& inst, vector& variants) { vector result; for (long long int i = 0; i < SIZE(variants); ++i) { if (variants.at(i) == -1) continue; trace(9992, "transform") << "checking variant (strict except literals-against-booleans) " << i << ": " << header_label(variants.at(i)) << end(); if (all_header_reagents_strictly_match_except_literal_against_boolean(inst, get(Recipe, variants.at(i)))) result.push_back(variants.at(i)); } return result; } bool all_header_reagents_strictly_match_except_literal_against_boolean(const instruction& inst, const recipe& variant) { for (long long int i = 0; i < min(SIZE(inst.ingredients), SIZE(variant.ingredients)); ++i) { if (!types_strictly_match_except_literal_against_boolean(variant.ingredients.at(i), inst.ingredients.at(i))) { trace(9993, "transform") << "strict match failed: ingredient " << i << end(); return false; } } for (long long int i = 0; i < min(SIZE(variant.products), SIZE(inst.products)); ++i) { if (is_dummy(inst.products.at(i))) continue; if (!types_strictly_match_except_literal_against_boolean(variant.products.at(i), inst.products.at(i))) { trace(9993, "transform") << "strict match failed: product " << i << end(); return false; } } return true; } // phase 4 vector matching_variants(const instruction& inst, vector& variants) { vector result; for (long long int i = 0; i < SIZE(variants); ++i) { if (variants.at(i) == -1) continue; trace(9992, "transform") << "checking variant " << i << ": " << header_label(variants.at(i)) << end(); if (all_header_reagents_match(inst, get(Recipe, variants.at(i)))) result.push_back(variants.at(i)); } return result; } bool all_header_reagents_match(const instruction& inst, const recipe& variant) { for (long long int i = 0; i < min(SIZE(inst.ingredients), SIZE(variant.ingredients)); ++i) { if (!types_match(variant.ingredients.at(i), inst.ingredients.at(i))) { trace(9993, "transform") << "strict match failed: ingredient " << i << end(); return false; } } for (long long int i = 0; i < min(SIZE(variant.products), SIZE(inst.products)); ++i) { if (is_dummy(inst.products.at(i))) continue; if (!types_match(variant.products.at(i), inst.products.at(i))) { trace(9993, "transform") << "strict match failed: product " << i << end(); return false; } } return true; } // tie-breaker for each phase const recipe& best_variant(const instruction& inst, vector& candidates) { assert(!candidates.empty()); long long int min_score = 999; long long int min_index = 0; for (long long int i = 0; i < SIZE(candidates); ++i) { const recipe& candidate = get(Recipe, candidates.at(i)); long long int score = abs(SIZE(candidate.products)-SIZE(inst.products)) + abs(SIZE(candidate.ingredients)-SIZE(inst.ingredients)); assert(score < 999); if (score < min_score) { min_score = score; min_index = i; } } return get(Recipe, candidates.at(min_index)); } long long int non_ghost_size(vector& variants) { long long int result = 0; for (long long int i = 0; i < SIZE(variants); ++i) if (variants.at(i) != -1) ++result; return result; } bool next_stash(const call& c, instruction* stash_inst) { const recipe& specializer_recipe = get(Recipe, c.running_recipe); long long int index = c.running_step_index; for (++index; index < SIZE(specializer_recipe.steps); ++index) { const instruction& inst = specializer_recipe.steps.at(index); if (inst.name == "stash") { *stash_inst = inst; return true; } } return false; } void test_static_dispatch_disabled_in_recipe_without_variants() { Trace_file = "static_dispatch_disabled_in_recipe_without_variants"; run("recipe main [\n 1:number <- test 3\n]\nrecipe test [\n 2:number <- next-ingredient # ensure no header\n reply 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1"); } void test_static_dispatch_disabled_on_headerless_definition() { Trace_file = "static_dispatch_disabled_on_headerless_definition"; Hide_warnings = true; run("recipe test a:number -> z:number [\n z <- copy 1\n]\nrecipe test [\n reply 34\n]\n"); CHECK_TRACE_CONTENTS("warn: redefining recipe test"); } void test_static_dispatch_disabled_on_headerless_definition_2() { Trace_file = "static_dispatch_disabled_on_headerless_definition_2"; Hide_warnings = true; run("recipe test [\n reply 34\n]\nrecipe test a:number -> z:number [\n z <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("warn: redefining recipe test"); } void test_static_dispatch_on_primitive_names() { Trace_file = "static_dispatch_on_primitive_names"; run("recipe main [\n 1:number <- copy 34\n 2:number <- copy 34\n 3:boolean <- equal 1:number, 2:number\n 4:boolean <- copy 0/false\n 5:boolean <- copy 0/false\n 6:boolean <- equal 4:boolean, 5:boolean\n]\n# temporarily hardcode number equality to always fail\nrecipe equal x:number, y:number -> z:boolean [\n local-scope\n load-ingredients\n z <- copy 0/false\n]\n# comparing numbers used overload\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3mem: storing 1 in location 6"); } void test_static_dispatch_works_with_dummy_results_for_containers() { Trace_file = "static_dispatch_works_with_dummy_results_for_containers"; Hide_errors = true; run("recipe main [\n _ <- test 3, 4\n]\nrecipe test a:number -> z:point [\n local-scope\n load-ingredients\n z <- merge a, 0\n]\nrecipe test a:number, b:number -> z:point [\n local-scope\n load-ingredients\n z <- merge a, b\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_static_dispatch_works_with_compound_type_containing_container_defined_after_first_use() { Trace_file = "static_dispatch_works_with_compound_type_containing_container_defined_after_first_use"; Hide_errors = true; run("recipe main [\n x:address:shared:foo <- new foo:type\n test x\n]\ncontainer foo [\n x:number\n]\nrecipe test a:address:shared:foo -> z:number [\n local-scope\n load-ingredients\n z:number <- get *a, x:offset\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_static_dispatch_works_with_compound_type_containing_container_defined_after_second_use() { Trace_file = "static_dispatch_works_with_compound_type_containing_container_defined_after_second_use"; Hide_errors = true; run("recipe main [\n x:address:shared:foo <- new foo:type\n test x\n]\nrecipe test a:address:shared:foo -> z:number [\n local-scope\n load-ingredients\n z:number <- get *a, x:offset\n]\ncontainer foo [\n x:number\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_static_dispatch_prefers_literals_to_be_numbers_rather_than_addresses() { Trace_file = "static_dispatch_prefers_literals_to_be_numbers_rather_than_addresses"; run("recipe main [\n 1:number <- foo 0\n]\nrecipe foo x:address:number -> y:number [\n reply 34\n]\nrecipe foo x:number -> y:number [\n reply 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 35 in location 1"); } void test_static_dispatch_on_non_literal_character_ignores_variant_with_numbers() { Trace_file = "static_dispatch_on_non_literal_character_ignores_variant_with_numbers"; Hide_errors = true; run("recipe main [\n local-scope\n x:character <- copy 10/newline\n 1:number/raw <- foo x\n]\nrecipe foo x:number -> y:number [\n load-ingredients\n reply 34\n]\n"); CHECK_TRACE_CONTENTS("error: main: ingredient 0 has the wrong type at '1:number/raw <- foo x'"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 34 in location 1"); } void test_static_dispatch_dispatches_literal_to_boolean_before_character() { Trace_file = "static_dispatch_dispatches_literal_to_boolean_before_character"; run("recipe main [\n 1:number/raw <- foo 0 # valid literal for boolean\n]\nrecipe foo x:character -> y:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo x:boolean -> y:number [\n local-scope\n load-ingredients\n reply 35\n]\n# boolean variant is preferred\n"); CHECK_TRACE_CONTENTS("mem: storing 35 in location 1"); } void test_static_dispatch_dispatches_literal_to_character_when_out_of_boolean_range() { Trace_file = "static_dispatch_dispatches_literal_to_character_when_out_of_boolean_range"; run("recipe main [\n 1:number/raw <- foo 97 # not a valid literal for boolean\n]\nrecipe foo x:character -> y:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo x:boolean -> y:number [\n local-scope\n load-ingredients\n reply 35\n]\n# character variant is preferred\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1"); } void test_static_dispatch_dispatches_literal_to_number_if_at_all_possible() { Trace_file = "static_dispatch_dispatches_literal_to_number_if_at_all_possible"; run("recipe main [\n 1:number/raw <- foo 97\n]\nrecipe foo x:character -> y:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo x:number -> y:number [\n local-scope\n load-ingredients\n reply 35\n]\n# number variant is preferred\n"); CHECK_TRACE_CONTENTS("mem: storing 35 in location 1"); } string header_label(recipe_ordinal r) { const recipe& caller = get(Recipe, r); ostringstream out; out << "recipe " << caller.name; for (long long int i = 0; i < SIZE(caller.ingredients); ++i) out << ' ' << to_string(caller.ingredients.at(i)); if (!caller.products.empty()) out << " ->"; for (long long int i = 0; i < SIZE(caller.products); ++i) out << ' ' << to_string(caller.products.at(i)); return out.str(); } void test_reload_variant_retains_other_variants() { Trace_file = "reload_variant_retains_other_variants"; run("recipe main [\n 1:number <- copy 34\n 2:number <- foo 1:number\n]\nrecipe foo x:number -> y:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo x:address:number -> y:number [\n local-scope\n load-ingredients\n reply 35\n]\nrecipe! foo x:address:number -> y:number [\n local-scope\n load-ingredients\n reply 36\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 2"); CHECK_TRACE_COUNT("error", 0); run(""); CHECK_TRACE_COUNT("warn", 0); } void test_dispatch_errors_come_after_unknown_name_errors() { Trace_file = "dispatch_errors_come_after_unknown_name_errors"; Hide_errors = true; run("recipe main [\n y:number <- foo x\n]\nrecipe foo a:number -> b:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo a:boolean -> b:number [\n local-scope\n load-ingredients\n reply 35\n]\n"); CHECK_TRACE_CONTENTS("error: main: missing type for x in 'y:number <- foo x'error: main: failed to find a matching call for 'y:number <- foo x'"); } void test_size_of_shape_shifting_container() { Trace_file = "size_of_shape_shifting_container"; run("container foo:_t [\n x:_t\n y:number\n]\nrecipe main [\n 1:foo:number <- merge 12, 13\n 3:foo:point <- merge 14, 15, 16\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 12 in location 1mem: storing 13 in location 2mem: storing 14 in location 3mem: storing 15 in location 4mem: storing 16 in location 5"); } void test_size_of_shape_shifting_container_2() { Trace_file = "size_of_shape_shifting_container_2"; Hide_errors = true; run("# multiple type ingredients\ncontainer foo:_a:_b [\n x:_a\n y:_b\n]\nrecipe main [\n 1:foo:number:boolean <- merge 34, 1/true\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_size_of_shape_shifting_container_3() { Trace_file = "size_of_shape_shifting_container_3"; Hide_errors = true; run("container foo:_a:_b [\n x:_a\n y:_b\n]\nrecipe main [\n 1:address:shared:array:character <- new [abc]\n # compound types for type ingredients\n {2: (foo number (address shared array character))} <- merge 34/x, 1:address:shared:array:character/y\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_size_of_shape_shifting_container_4() { Trace_file = "size_of_shape_shifting_container_4"; Hide_errors = true; run("container foo:_a:_b [\n x:_a\n y:_b\n]\ncontainer bar:_a:_b [\n # dilated element\n {data: (foo _a (address shared _b))}\n]\nrecipe main [\n 1:address:shared:array:character <- new [abc]\n 2:bar:number:array:character <- merge 34/x, 1:address:shared:array:character/y\n]\n"); CHECK_TRACE_COUNT("error", 0); } void read_type_ingredients(string& name) { string save_name = name; istringstream in(save_name); name = slurp_until(in, ':'); if (!contains_key(Type_ordinal, name) || get(Type_ordinal, name) == 0) put(Type_ordinal, name, Next_type_ordinal++); type_info& info = get_or_insert(Type, get(Type_ordinal, name)); long long int next_type_ordinal = START_TYPE_INGREDIENTS; while (has_data(in)) { string curr = slurp_until(in, ':'); if (info.type_ingredient_names.find(curr) != info.type_ingredient_names.end()) { raise_error << "can't repeat type ingredient names in a single container definition\n" << end(); return; } put(info.type_ingredient_names, curr, next_type_ordinal++); } } bool is_type_ingredient_name(const string& type) { return !type.empty() && type.at(0) == '_'; } void test_size_of_shape_shifting_exclusive_container() { Trace_file = "size_of_shape_shifting_exclusive_container"; run("exclusive-container foo:_t [\n x:_t\n y:number\n]\nrecipe main [\n 1:foo:number <- merge 0/x, 34\n 3:foo:point <- merge 0/x, 15, 16\n 6:foo:point <- merge 1/y, 23\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1mem: storing 34 in location 2mem: storing 0 in location 3mem: storing 15 in location 4mem: storing 16 in location 5mem: storing 1 in location 6mem: storing 23 in location 7"); CHECK_TRACE_COUNT("mem", 7); } // shape-shifting version of size_of long long int size_of_type_ingredient(const type_tree* element_template, const type_tree* rest_of_use) { type_tree* element_type = type_ingredient(element_template, rest_of_use); if (!element_type) return 0; long long int result = size_of(element_type); delete element_type; return result; } type_tree* type_ingredient(const type_tree* element_template, const type_tree* rest_of_use) { long long int type_ingredient_index = element_template->value - START_TYPE_INGREDIENTS; const type_tree* curr = rest_of_use; if (!curr) return NULL; while (type_ingredient_index > 0) { --type_ingredient_index; curr = curr->right; if (!curr) return NULL; } assert(curr); if (curr->left) curr = curr->left; assert(curr->value > 0); trace(9999, "type") << "type deduced to be " << get(Type, curr->value).name << "$" << end(); return new type_tree(*curr); } void test_get_on_shape_shifting_container() { Trace_file = "get_on_shape_shifting_container"; run("container foo:_t [\n x:_t\n y:number\n]\nrecipe main [\n 1:foo:point <- merge 14, 15, 16\n 2:number <- get 1:foo:point, y:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 16 in location 2"); } void test_get_on_shape_shifting_container_2() { Trace_file = "get_on_shape_shifting_container_2"; run("container foo:_t [\n x:_t\n y:number\n]\nrecipe main [\n 1:foo:point <- merge 14, 15, 16\n 2:point <- get 1:foo:point, x:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 14 in location 2mem: storing 15 in location 3"); } void test_get_on_shape_shifting_container_3() { Trace_file = "get_on_shape_shifting_container_3"; run("container foo:_t [\n x:_t\n y:number\n]\nrecipe main [\n 1:foo:address:point <- merge 34/unsafe, 48\n 2:address:point <- get 1:foo:address:point, x:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 2"); } void test_get_on_shape_shifting_container_inside_container() { Trace_file = "get_on_shape_shifting_container_inside_container"; run("container foo:_t [\n x:_t\n y:number\n]\ncontainer bar [\n x:foo:point\n y:number\n]\nrecipe main [\n 1:bar <- merge 14, 15, 16, 17\n 2:number <- get 1:bar, 1:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 17 in location 2"); } void test_get_on_complex_shape_shifting_container() { Trace_file = "get_on_complex_shape_shifting_container"; run("container foo:_a:_b [\n x:_a\n y:_b\n]\nrecipe main [\n 1:address:shared:array:character <- new [abc]\n {2: (foo number (address shared array character))} <- merge 34/x, 1:address:shared:array:character/y\n 3:address:shared:array:character <- get {2: (foo number (address shared array character))}, y:offset\n 4:boolean <- equal 1:address:shared:array:character, 3:address:shared:array:character\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 4"); } bool contains_type_ingredient(const reagent& x) { return contains_type_ingredient(x.type); } bool contains_type_ingredient(const type_tree* type) { if (!type) return false; if (type->value >= START_TYPE_INGREDIENTS) return true; return contains_type_ingredient(type->left) || contains_type_ingredient(type->right); } // todo: too complicated and likely incomplete; maybe avoid replacing in place? Maybe process element_type and element_type_name in separate functions? void replace_type_ingredients(type_tree* element_type, const type_tree* callsite_type, const type_info& container_info) { if (!callsite_type) return; // error but it's already been raised above if (!element_type) return; // A. recurse first to avoid nested replaces (which I can't reason about yet) replace_type_ingredients(element_type->left, callsite_type, container_info); replace_type_ingredients(element_type->right, callsite_type, container_info); if (element_type->value < START_TYPE_INGREDIENTS) return; const long long int type_ingredient_index = element_type->value-START_TYPE_INGREDIENTS; if (!has_nth_type(callsite_type, type_ingredient_index)) { raise_error << "illegal type " << names_to_string(callsite_type) << " seems to be missing a type ingredient or three\n" << end(); return; } // B. replace the current location const type_tree* replacement = NULL; bool splice_right = true ; { const type_tree* curr = callsite_type; for (long long int i = 0; i < type_ingredient_index; ++i) curr = curr->right; if (curr && curr->left) { replacement = curr->left; } else { // We want foo:_t to be used like foo:number, which expands to {foo: number} // rather than {foo: (number)} // We'd also like to use it with multiple types: foo:address:number. replacement = curr; if (!final_type_ingredient(type_ingredient_index, container_info)) { splice_right = false; } } } element_type->name = replacement->name; element_type->value = replacement->value; assert(!element_type->left); // since value is set element_type->left = replacement->left ? new type_tree(*replacement->left) : NULL; if (splice_right) { type_tree* old_right = element_type->right; element_type->right = replacement->right ? new type_tree(*replacement->right) : NULL; append(element_type->right, old_right); } } bool final_type_ingredient(long long int type_ingredient_index, const type_info& container_info) { for (map::const_iterator p = container_info.type_ingredient_names.begin(); p != container_info.type_ingredient_names.end(); ++p) { if (p->second > START_TYPE_INGREDIENTS+type_ingredient_index) return false; } return true; } void append(type_tree*& base, type_tree* extra) { if (!base) { base = extra; return; } type_tree* curr = base; while (curr->right) curr = curr->right; curr->right = extra; } void append(string_tree*& base, string_tree* extra) { if (!base) { base = extra; return; } string_tree* curr = base; while (curr->right) curr = curr->right; curr->right = extra; } void test_replace_type_ingredients_entire() { run("container foo:_elem [\n" " x:_elem\n" " y:number\n" "]\n"); reagent callsite("x:foo:point"); reagent element = element_type(callsite, 0); cerr << debug_string(element) << '\n'; //? DUMP(""); CHECK_EQ(element.name, "x"); cerr << get(Type_ordinal, "point") << '\n'; cerr << contains_key(Type, 23) << '\n'; for (map::iterator p = Type_ordinal.begin(); p != Type_ordinal.end(); ++p) if (p->second == 23) cerr << p->first << '\n'; CHECK_EQ(element.type->name, "point"); CHECK(!element.type->right); } void test_replace_type_ingredients_tail() { exit(0); run("container foo:_elem [\n" " x:_elem\n" "]\n" "container bar:_elem [\n" " x:foo:_elem\n" "]\n"); reagent callsite("x:bar:point"); reagent element = element_type(callsite, 0); CHECK_EQ(element.name, "x"); CHECK_EQ(element.type->name, "foo"); CHECK_EQ(element.type->right->name, "point"); CHECK(!element.type->right->right); } void test_replace_type_ingredients_head_tail_multiple() { run("container foo:_elem [\n" " x:_elem\n" "]\n" "container bar:_elem [\n" " x:foo:_elem\n" "]\n"); reagent callsite("x:bar:address:shared:array:character"); reagent element = element_type(callsite, 0); CHECK_EQ(element.name, "x"); CHECK_EQ(element.type->name, "foo"); CHECK_EQ(element.type->right->name, "address"); CHECK_EQ(element.type->right->right->name, "shared"); CHECK_EQ(element.type->right->right->right->name, "array"); CHECK_EQ(element.type->right->right->right->right->name, "character"); CHECK(!element.type->right->right->right->right->right); } void test_replace_type_ingredients_head_middle() { run("container foo:_elem [\n" " x:_elem\n" "]\n" "container bar:_elem [\n" " x:foo:_elem:number\n" "]\n"); reagent callsite("x:bar:address"); reagent element = element_type(callsite, 0); CHECK_EQ(element.name, "x"); CHECK(element.type) CHECK_EQ(element.type->name, "foo"); CHECK(element.type->right) CHECK_EQ(element.type->right->name, "address"); CHECK(element.type->right->right) CHECK_EQ(element.type->right->right->name, "number"); CHECK(!element.type->right->right->right); } void test_replace_last_type_ingredient_with_multiple() { run("container foo:_a:_b [\n" " x:_a\n" " y:_b\n" "]\n"); reagent callsite("{f: (foo number (address shared array character))}"); reagent element1 = element_type(callsite, 0); CHECK_EQ(element1.name, "x"); CHECK_EQ(element1.type->name, "number"); CHECK(!element1.type->right); reagent element2 = element_type(callsite, 1); CHECK_EQ(element2.name, "y"); CHECK_EQ(element2.type->name, "address"); CHECK_EQ(element2.type->right->name, "shared"); CHECK_EQ(element2.type->right->right->name, "array"); CHECK_EQ(element2.type->right->right->right->name, "character"); CHECK(!element2.type->right->right->right->right); } void test_replace_middle_type_ingredient_with_multiple() { run("container foo:_a:_b:_c [\n" " x:_a\n" " y:_b\n" " z:_c\n" "]\n"); reagent callsite("{f: (foo number (address shared array character) boolean)}"); reagent element1 = element_type(callsite, 0); CHECK_EQ(element1.name, "x"); CHECK_EQ(element1.type->name, "number"); CHECK(!element1.type->right); reagent element2 = element_type(callsite, 1); CHECK_EQ(element2.name, "y"); CHECK_EQ(element2.type->name, "address"); CHECK_EQ(element2.type->right->name, "shared"); CHECK_EQ(element2.type->right->right->name, "array"); CHECK_EQ(element2.type->right->right->right->name, "character"); CHECK(!element2.type->right->right->right->right); reagent element3 = element_type(callsite, 2); CHECK_EQ(element3.name, "z"); CHECK_EQ(element3.type->name, "boolean"); CHECK(!element3.type->right); } bool has_nth_type(const type_tree* base, long long int n) { assert(n >= 0); if (base == NULL) return false; if (n == 0) return true; return has_nth_type(base->right, n-1); } void test_get_on_shape_shifting_container_error() { Trace_file = "get_on_shape_shifting_container_error"; Hide_errors = true; run("container foo:_t [\n x:_t\n y:number\n]\nrecipe main [\n 10:foo:point <- merge 14, 15, 16\n 1:number <- get 10:foo, 1:offset\n]\n"); CHECK_TRACE_CONTENTS("error: illegal type \"foo\" seems to be missing a type ingredient or three"); } void test_get_address_on_shape_shifting_container() { Trace_file = "get_address_on_shape_shifting_container"; run("container foo:_t [\n x:_t\n y:number\n]\nrecipe main [\n 10:foo:point <- merge 14, 15, 16\n 1:address:number <- get-address 10:foo:point, 1:offset\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 12 in location 1"); } void test_merge_check_shape_shifting_container_containing_exclusive_container() { Trace_file = "merge_check_shape_shifting_container_containing_exclusive_container"; Hide_errors = true; run("container foo:_elem [\n x:number\n y:_elem\n]\nexclusive-container bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo:bar <- merge 23, 1/y, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 23 in location 1mem: storing 1 in location 2mem: storing 34 in location 3"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_shape_shifting_container_containing_exclusive_container_2() { Trace_file = "merge_check_shape_shifting_container_containing_exclusive_container_2"; Hide_errors = true; run("container foo:_elem [\n x:number\n y:_elem\n]\nexclusive-container bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo:bar <- merge 23, 1/y, 34, 35\n]\n"); CHECK_TRACE_CONTENTS("error: main: too many ingredients in '1:foo:bar <- merge 23, 1/y, 34, 35'"); } void test_merge_check_shape_shifting_exclusive_container_containing_container() { Trace_file = "merge_check_shape_shifting_exclusive_container_containing_container"; Hide_errors = true; run("exclusive-container foo:_elem [\n x:number\n y:_elem\n]\ncontainer bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo:bar <- merge 1/y, 23, 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 1mem: storing 23 in location 2mem: storing 34 in location 3"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_shape_shifting_exclusive_container_containing_container_2() { Trace_file = "merge_check_shape_shifting_exclusive_container_containing_container_2"; Hide_errors = true; run("exclusive-container foo:_elem [\n x:number\n y:_elem\n]\ncontainer bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo:bar <- merge 0/x, 23\n]\n"); CHECK_TRACE_COUNT("error", 0); } void test_merge_check_shape_shifting_exclusive_container_containing_container_3() { Trace_file = "merge_check_shape_shifting_exclusive_container_containing_container_3"; Hide_errors = true; run("exclusive-container foo:_elem [\n x:number\n y:_elem\n]\ncontainer bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo:bar <- merge 1/y, 23\n]\n"); CHECK_TRACE_CONTENTS("error: main: too few ingredients in '1:foo:bar <- merge 1/y, 23'"); } void test_shape_shifting_recipe() { Trace_file = "shape_shifting_recipe"; run("recipe main [\n 10:point <- merge 14, 15\n 11:point <- foo 10:point\n]\n# non-matching variant\nrecipe foo a:number -> result:number [\n local-scope\n load-ingredients\n result <- copy 34\n]\n# matching shape-shifting variant\nrecipe foo a:_t -> result:_t [\n local-scope\n load-ingredients\n result <- copy a\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 14 in location 11mem: storing 15 in location 12"); } // phase 2 of static dispatch vector strictly_matching_shape_shifting_variants(const instruction& inst, vector& variants) { vector result; for (long long int i = 0; i < SIZE(variants); ++i) { if (variants.at(i) == -1) continue; if (!any_type_ingredient_in_header(variants.at(i))) continue; if (all_concrete_header_reagents_strictly_match(inst, get(Recipe, variants.at(i)))) result.push_back(variants.at(i)); } return result; } bool all_concrete_header_reagents_strictly_match(const instruction& inst, const recipe& variant) { if (SIZE(inst.ingredients) < SIZE(variant.ingredients)) { trace(9993, "transform") << "too few ingredients" << end(); return false; } if (SIZE(variant.products) < SIZE(inst.products)) { trace(9993, "transform") << "too few products" << end(); return false; } for (long long int i = 0; i < SIZE(variant.ingredients); ++i) { if (!concrete_type_names_strictly_match(variant.ingredients.at(i), inst.ingredients.at(i))) { trace(9993, "transform") << "concrete-type match failed: ingredient " << i << end(); return false; } } for (long long int i = 0; i < SIZE(inst.products); ++i) { if (is_dummy(inst.products.at(i))) continue; if (!concrete_type_names_strictly_match(variant.products.at(i), inst.products.at(i))) { trace(9993, "transform") << "strict match failed: product " << i << end(); return false; } } return true; } // tie-breaker for phase 2 recipe_ordinal best_shape_shifting_variant(const instruction& inst, vector& candidates) { assert(!candidates.empty()); // primary score long long int max_score = -1; for (long long int i = 0; i < SIZE(candidates); ++i) { long long int score = number_of_concrete_type_names(candidates.at(i)); assert(score > -1); if (score > max_score) max_score = score; } // break any ties at max_score by a secondary score long long int min_score2 = 999; long long int best_index = 0; for (long long int i = 0; i < SIZE(candidates); ++i) { long long int score1 = number_of_concrete_type_names(candidates.at(i)); assert(score1 <= max_score); if (score1 != max_score) continue; const recipe& candidate = get(Recipe, candidates.at(i)); long long int score2 = (SIZE(candidate.products)-SIZE(inst.products)) + (SIZE(inst.ingredients)-SIZE(candidate.ingredients)); assert(score2 < 999); if (score2 < min_score2) { min_score2 = score2; best_index = i; } } return candidates.at(best_index); } bool any_type_ingredient_in_header(recipe_ordinal variant) { const recipe& caller = get(Recipe, variant); for (long long int i = 0; i < SIZE(caller.ingredients); ++i) { if (contains_type_ingredient_name(caller.ingredients.at(i))) return true; } for (long long int i = 0; i < SIZE(caller.products); ++i) { if (contains_type_ingredient_name(caller.products.at(i))) return true; } return false; } bool concrete_type_names_strictly_match(reagent to, reagent from) { canonize_type(to); canonize_type(from); return concrete_type_names_strictly_match(to.type, from.type, from); } long long int number_of_concrete_type_names(recipe_ordinal r) { const recipe& caller = get(Recipe, r); long long int result = 0; for (long long int i = 0; i < SIZE(caller.ingredients); ++i) result += number_of_concrete_type_names(caller.ingredients.at(i)); for (long long int i = 0; i < SIZE(caller.products); ++i) result += number_of_concrete_type_names(caller.products.at(i)); return result; } long long int number_of_concrete_type_names(const reagent& r) { return number_of_concrete_type_names(r.type); } long long int number_of_concrete_type_names(const type_tree* type) { if (!type) return 0; long long int result = 0; if (!type->name.empty() && !is_type_ingredient_name(type->name)) result++; result += number_of_concrete_type_names(type->left); result += number_of_concrete_type_names(type->right); return result; } bool concrete_type_names_strictly_match(const type_tree* to, const type_tree* from, const reagent& rhs_reagent) { if (!to) return !from; if (!from) return !to; if (is_type_ingredient_name(to->name)) return true; // type ingredient matches anything if (to->name == "literal" && from->name == "literal") return true; if (to->name == "literal" && Literal_type_names.find(from->name) != Literal_type_names.end()) return true; if (from->name == "literal" && Literal_type_names.find(to->name) != Literal_type_names.end()) return true; if (from->name == "literal" && to->name == "address") return rhs_reagent.name == "0"; return to->name == from->name && concrete_type_names_strictly_match(to->left, from->left, rhs_reagent) && concrete_type_names_strictly_match(to->right, from->right, rhs_reagent); } bool contains_type_ingredient_name(const reagent& x) { return contains_type_ingredient_name(x.type); } bool contains_type_ingredient_name(const type_tree* type) { if (!type) return false; if (is_type_ingredient_name(type->name)) return true; return contains_type_ingredient_name(type->left) || contains_type_ingredient_name(type->right); } recipe_ordinal new_variant(recipe_ordinal exemplar, const instruction& inst, const recipe& caller_recipe) { cerr << "HHH " << contains_key(Type_ordinal, "_elem") << '\n'; string new_name = next_unused_recipe_name(inst.name); assert(!contains_key(Recipe_ordinal, new_name)); recipe_ordinal new_recipe_ordinal = put(Recipe_ordinal, new_name, Next_recipe_ordinal++); // make a copy assert(contains_key(Recipe, exemplar)); assert(!contains_key(Recipe, new_recipe_ordinal)); Recently_added_recipes.push_back(new_recipe_ordinal); Recently_added_shape_shifting_recipes.push_back(new_recipe_ordinal); put(Recipe, new_recipe_ordinal, get(Recipe, exemplar)); recipe& new_recipe = get(Recipe, new_recipe_ordinal); new_recipe.name = new_name; trace(9993, "transform") << "switching " << inst.name << " to specialized " << header_label(new_recipe_ordinal) << end(); // Since the exemplar never ran any transforms, we have to redo some of the // work of the check_types_by_name transform while supporting type-ingredients. compute_type_names(new_recipe); // that gives enough information to replace type-ingredients with concrete types { map mappings; bool error = false; cerr << "III " << contains_key(Type_ordinal, "_elem") << '\n'; compute_type_ingredient_mappings(get(Recipe, exemplar), inst, mappings, caller_recipe, &error); cerr << "JJJ " << contains_key(Type_ordinal, "_elem") << '\n'; if (!error) replace_type_ingredients(new_recipe, mappings); cerr << "VVV " << contains_key(Type_ordinal, "_elem") << '\n'; //? if (!is_type_ingredient_name(x.type->name) && contains_key(Type_ordinal, x.type->name)) { //? x.type->value = get(Type_ordinal, x.type->name); //? return; //? } for (map::iterator p = mappings.begin(); p != mappings.end(); ++p) delete p->second; if (error) return 0; // todo: delete new_recipe_ordinal from Recipes and other global state } ensure_all_concrete_types(new_recipe, get(Recipe, exemplar)); cerr << "WWW " << contains_key(Type_ordinal, "_elem") << '\n'; return new_recipe_ordinal; } void compute_type_names(recipe& variant) { trace(9993, "transform") << "compute type names: " << variant.name << end(); map type_names; for (long long int i = 0; i < SIZE(variant.ingredients); ++i) save_or_deduce_type_name(variant.ingredients.at(i), type_names, variant); for (long long int i = 0; i < SIZE(variant.products); ++i) save_or_deduce_type_name(variant.products.at(i), type_names, variant); for (long long int i = 0; i < SIZE(variant.steps); ++i) { instruction& inst = variant.steps.at(i); trace(9993, "transform") << " instruction: " << to_string(inst) << end(); for (long long int in = 0; in < SIZE(inst.ingredients); ++in) save_or_deduce_type_name(inst.ingredients.at(in), type_names, variant); for (long long int out = 0; out < SIZE(inst.products); ++out) save_or_deduce_type_name(inst.products.at(out), type_names, variant); } } void save_or_deduce_type_name(reagent& x, map& type, const recipe& variant) { trace(9994, "transform") << " checking " << to_string(x) << ": " << names_to_string(x.type) << end(); if (!x.type && contains_key(type, x.name)) { x.type = new type_tree(*get(type, x.name)); trace(9994, "transform") << " deducing type to " << names_to_string(x.type) << end(); return; } if (!x.type) { raise_error << maybe(variant.original_name) << "unknown type for " << x.original_string << " (check the name for typos)\n" << end(); return; } if (contains_key(type, x.name)) return; if (x.type->name == "offset" || x.type->name == "variant") return; // special-case for container-access instructions put(type, x.name, x.type); trace(9993, "transform") << "type of " << x.name << " is " << names_to_string(x.type) << end(); } void compute_type_ingredient_mappings(const recipe& exemplar, const instruction& inst, map& mappings, const recipe& caller_recipe, bool* error) { long long int limit = min(SIZE(inst.ingredients), SIZE(exemplar.ingredients)); for (long long int i = 0; i < limit; ++i) { const reagent& exemplar_reagent = exemplar.ingredients.at(i); reagent ingredient = inst.ingredients.at(i); canonize_type(ingredient); if (is_mu_address(exemplar_reagent) && ingredient.name == "0") continue; // assume it matches accumulate_type_ingredients(exemplar_reagent, ingredient, mappings, exemplar, inst, caller_recipe, error); } limit = min(SIZE(inst.products), SIZE(exemplar.products)); for (long long int i = 0; i < limit; ++i) { const reagent& exemplar_reagent = exemplar.products.at(i); reagent product = inst.products.at(i); canonize_type(product); accumulate_type_ingredients(exemplar_reagent, product, mappings, exemplar, inst, caller_recipe, error); } } inline long long int min(long long int a, long long int b) { return (a < b) ? a : b; } void accumulate_type_ingredients(const reagent& exemplar_reagent, reagent& refinement, map& mappings, const recipe& exemplar, const instruction& call_instruction, const recipe& caller_recipe, bool* error) { assert(refinement.type); accumulate_type_ingredients(exemplar_reagent.type, refinement.type, mappings, exemplar, exemplar_reagent, call_instruction, caller_recipe, error); } void accumulate_type_ingredients(const type_tree* exemplar_type, const type_tree* refinement_type, map& mappings, const recipe& exemplar, const reagent& exemplar_reagent, const instruction& call_instruction, const recipe& caller_recipe, bool* error) { if (!exemplar_type) return; if (!refinement_type) { // todo: make this smarter; only warn if exemplar_type contains some *new* type ingredient raise_error << maybe(exemplar.name) << "missing type ingredient in " << exemplar_reagent.original_string << '\n' << end(); return; } if (is_type_ingredient_name(exemplar_type->name)) { assert(!refinement_type->name.empty()); if (exemplar_type->right) { raise_error << "type_ingredients in non-last position not currently supported\n" << end(); return; } if (!contains_key(mappings, exemplar_type->name)) { trace(9993, "transform") << "adding mapping from " << exemplar_type->name << " to " << to_string(refinement_type) << end(); put(mappings, exemplar_type->name, new type_tree(*refinement_type)); } else { if (!deeply_equal_type_names(get(mappings, exemplar_type->name), refinement_type)) { raise_error << maybe(caller_recipe.name) << "no call found for '" << to_string(call_instruction) << "'\n" << end(); *error = true; return; } if (get(mappings, exemplar_type->name)->name == "literal") { delete get(mappings, exemplar_type->name); put(mappings, exemplar_type->name, new type_tree(*refinement_type)); } } } else { accumulate_type_ingredients(exemplar_type->left, refinement_type->left, mappings, exemplar, exemplar_reagent, call_instruction, caller_recipe, error); } accumulate_type_ingredients(exemplar_type->right, refinement_type->right, mappings, exemplar, exemplar_reagent, call_instruction, caller_recipe, error); } void replace_type_ingredients(recipe& new_recipe, const map& mappings) { cerr << "KKK " << contains_key(Type_ordinal, "_elem") << '\n'; // update its header if (mappings.empty()) return; cerr << "LLL " << contains_key(Type_ordinal, "_elem") << '\n'; trace(9993, "transform") << "replacing in recipe header ingredients" << end(); for (long long int i = 0; i < SIZE(new_recipe.ingredients); ++i) replace_type_ingredients(new_recipe.ingredients.at(i), mappings, new_recipe); cerr << "MMM " << contains_key(Type_ordinal, "_elem") << '\n'; trace(9993, "transform") << "replacing in recipe header products" << end(); for (long long int i = 0; i < SIZE(new_recipe.products); ++i) replace_type_ingredients(new_recipe.products.at(i), mappings, new_recipe); // update its body cerr << "NNN " << contains_key(Type_ordinal, "_elem") << '\n'; for (long long int i = 0; i < SIZE(new_recipe.steps); ++i) { cerr << "OOO " << i << ' ' << contains_key(Type_ordinal, "_elem") << ' ' << to_string(new_recipe.steps.at(i)) << '\n'; instruction& inst = new_recipe.steps.at(i); trace(9993, "transform") << "replacing in instruction '" << to_string(inst) << "'" << end(); for (long long int j = 0; j < SIZE(inst.ingredients); ++j) replace_type_ingredients(inst.ingredients.at(j), mappings, new_recipe); for (long long int j = 0; j < SIZE(inst.products); ++j) replace_type_ingredients(inst.products.at(j), mappings, new_recipe); // special-case for new: replace type ingredient in first ingredient *value* if (inst.name == "new" && inst.ingredients.at(0).type->name != "literal-string") { cerr << "PPP " << contains_key(Type_ordinal, "_elem") << '\n'; type_tree* type = parse_type_tree(inst.ingredients.at(0).name); cerr << "QQQ " << contains_key(Type_ordinal, "_elem") << '\n'; replace_type_ingredients(type, mappings); cerr << "RRR " << contains_key(Type_ordinal, "_elem") << '\n'; inst.ingredients.at(0).name = inspect(type); delete type; cerr << "SSS " << contains_key(Type_ordinal, "_elem") << '\n'; } } cerr << "TTT " << contains_key(Type_ordinal, "_elem") << '\n'; } void replace_type_ingredients(reagent& x, const map& mappings, const recipe& caller) { string before = to_string(x); trace(9993, "transform") << "replacing in ingredient " << x.original_string << end(); if (!x.type) { raise_error << "specializing " << caller.original_name << ": missing type for " << x.original_string << '\n' << end(); return; } replace_type_ingredients(x.type, mappings); } void replace_type_ingredients(type_tree* type, const map& mappings) { if (!type) return; if (contains_key(Type_ordinal, type->name)) type->value = get(Type_ordinal, type->name); if (is_type_ingredient_name(type->name) && contains_key(mappings, type->name)) { const type_tree* replacement = get(mappings, type->name); //TODO names_to_string trace(9993, "transform") << type->name << " => " << to_string(replacement) << end(); if (replacement->name == "literal") { type->name = "number"; type->value = get(Type_ordinal, "number"); } else { type->name = replacement->name; type->value = replacement->value; } if (replacement->left) type->left = new type_tree(*replacement->left); if (replacement->right) type->right = new type_tree(*replacement->right); } replace_type_ingredients(type->left, mappings); replace_type_ingredients(type->right, mappings); } type_tree* parse_type_tree(const string& s) { istringstream in(s); in >> std::noskipws; return parse_type_tree(in); } type_tree* parse_type_tree(istream& in) { skip_whitespace_but_not_newline(in); if (!has_data(in)) return NULL; if (in.peek() == ')') { in.get(); return NULL; } if (in.peek() != '(') { string type_name = next_word(in); if (!contains_key(Type_ordinal, type_name)) put(Type_ordinal, type_name, Next_type_ordinal++); type_tree* result = new type_tree(type_name, get(Type_ordinal, type_name)); return result; } in.get(); // skip '(' type_tree* result = NULL; type_tree** curr = &result; while (in.peek() != ')') { assert(has_data(in)); *curr = new type_tree("", 0); skip_whitespace_but_not_newline(in); if (in.peek() == '(') (*curr)->left = parse_type_tree(in); else { (*curr)->name = next_word(in); if (!is_type_ingredient_name((*curr)->name)) { if (!contains_key(Type_ordinal, (*curr)->name)) { cerr << "RRR aaa " << contains_key(Type_ordinal, "_elem") << '\n'; put(Type_ordinal, (*curr)->name, Next_type_ordinal++); cerr << ">>> RRR zzz " << contains_key(Type_ordinal, "_elem") << '\n'; } (*curr)->value = get(Type_ordinal, (*curr)->name); } } curr = &(*curr)->right; } in.get(); // skip ')' return result; } string inspect(const type_tree* x) { ostringstream out; dump_inspect(x, out); return out.str(); } void dump_inspect(const type_tree* x, ostream& out) { if (!x->left && !x->right) { out << x->name; return; } out << '('; for (const type_tree* curr = x; curr; curr = curr->right) { if (curr != x) out << ' '; if (curr->left) dump_inspect(curr->left, out); else out << curr->name; } out << ')'; } void ensure_all_concrete_types(/*const*/ recipe& new_recipe, const recipe& exemplar) { for (long long int i = 0; i < SIZE(new_recipe.ingredients); ++i) ensure_all_concrete_types(new_recipe.ingredients.at(i), exemplar); for (long long int i = 0; i < SIZE(new_recipe.products); ++i) ensure_all_concrete_types(new_recipe.products.at(i), exemplar); for (long long int i = 0; i < SIZE(new_recipe.steps); ++i) { instruction& inst = new_recipe.steps.at(i); for (long long int j = 0; j < SIZE(inst.ingredients); ++j) ensure_all_concrete_types(inst.ingredients.at(j), exemplar); for (long long int j = 0; j < SIZE(inst.products); ++j) ensure_all_concrete_types(inst.products.at(j), exemplar); } } void ensure_all_concrete_types(/*const*/ reagent& x, const recipe& exemplar) { if (!x.type || contains_type_ingredient_name(x.type)) { raise_error << maybe(exemplar.name) << "failed to map a type to " << x.original_string << '\n' << end(); x.type = new type_tree("", 0); // just to prevent crashes later return; } if (x.type->value == -1) { raise_error << maybe(exemplar.name) << "failed to map a type to the unknown " << x.original_string << '\n' << end(); return; } } void test_shape_shifting_recipe_2() { Trace_file = "shape_shifting_recipe_2"; run("recipe main [\n 10:point <- merge 14, 15\n 11:point <- foo 10:point\n]\n# non-matching shape-shifting variant\nrecipe foo a:_t, b:_t -> result:number [\n local-scope\n load-ingredients\n result <- copy 34\n]\n# matching shape-shifting variant\nrecipe foo a:_t -> result:_t [\n local-scope\n load-ingredients\n result <- copy a\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 14 in location 11mem: storing 15 in location 12"); } void test_shape_shifting_recipe_nonroot() { Trace_file = "shape_shifting_recipe_nonroot"; run("recipe main [\n 10:foo:point <- merge 14, 15, 16\n 20:point/raw <- bar 10:foo:point\n]\n# shape-shifting recipe with type ingredient following some other type\nrecipe bar a:foo:_t -> result:_t [\n local-scope\n load-ingredients\n result <- get a, x:offset\n]\ncontainer foo:_t [\n x:_t\n y:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 14 in location 20mem: storing 15 in location 21"); } void test_shape_shifting_recipe_type_deduction_ignores_offsets() { Trace_file = "shape_shifting_recipe_type_deduction_ignores_offsets"; run("recipe main [\n 10:foo:point <- merge 14, 15, 16\n 20:point/raw <- bar 10:foo:point\n]\nrecipe bar a:foo:_t -> result:_t [\n local-scope\n load-ingredients\n x:number <- copy 1\n result <- get a, x:offset # shouldn't collide with other variable\n]\ncontainer foo:_t [\n x:_t\n y:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 14 in location 20mem: storing 15 in location 21"); } void test_shape_shifting_recipe_empty() { Trace_file = "shape_shifting_recipe_empty"; run("recipe main [\n foo 1\n]\n# shape-shifting recipe with no body\nrecipe foo a:_t [\n]\n# shouldn't crash\n"); } void test_shape_shifting_recipe_handles_shape_shifting_new_ingredient() { Trace_file = "shape_shifting_recipe_handles_shape_shifting_new_ingredient"; run("recipe main [\n 1:address:shared:foo:point <- bar 3\n 11:foo:point <- copy *1:address:shared:foo:point\n]\ncontainer foo:_t [\n x:_t\n y:number\n]\nrecipe bar x:number -> result:address:shared:foo:_t [\n local-scope\n load-ingredients\n # new refers to _t in its ingredient *value*\n result <- new {(foo _t) : type}\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 11mem: storing 0 in location 12mem: storing 0 in location 13"); } void test_shape_shifting_recipe_handles_shape_shifting_new_ingredient_2() { Trace_file = "shape_shifting_recipe_handles_shape_shifting_new_ingredient_2"; run("recipe main [\n 1:address:shared:foo:point <- bar 3\n 11:foo:point <- copy *1:address:shared:foo:point\n]\nrecipe bar x:number -> result:address:shared:foo:_t [\n local-scope\n load-ingredients\n # new refers to _t in its ingredient *value*\n result <- new {(foo _t) : type}\n]\n# container defined after use\ncontainer foo:_t [\n x:_t\n y:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 11mem: storing 0 in location 12mem: storing 0 in location 13"); } void test_shape_shifting_recipe_supports_compound_types() { Trace_file = "shape_shifting_recipe_supports_compound_types"; run("recipe main [\n 1:address:shared:point <- new point:type\n 2:address:number <- get-address *1:address:shared:point, y:offset\n *2:address:number <- copy 34\n 3:address:shared:point <- bar 1:address:shared:point # specialize _t to address:shared:point\n 4:point <- copy *3:address:shared:point\n]\nrecipe bar a:_t -> result:_t [\n local-scope\n load-ingredients\n result <- copy a\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 5"); } void test_shape_shifting_recipe_error() { Trace_file = "shape_shifting_recipe_error"; Hide_errors = true; run("recipe main [\n a:number <- copy 3\n b:address:shared:number <- foo a\n]\nrecipe foo a:_t -> b:_t [\n load-ingredients\n b <- copy a\n]\n"); CHECK_TRACE_CONTENTS("error: main: no call found for 'b:address:shared:number <- foo a'"); } void test_specialize_inside_recipe_without_header() { Trace_file = "specialize_inside_recipe_without_header"; run("recipe main [\n foo 3\n]\nrecipe foo [\n local-scope\n x:number <- next-ingredient # ensure no header\n 1:number/raw <- bar x # call a shape-shifting recipe\n]\nrecipe bar x:_elem -> y:_elem [\n local-scope\n load-ingredients\n y <- add x, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 1"); } void test_specialize_with_literal() { Trace_file = "specialize_with_literal"; run("recipe main [\n local-scope\n # permit literal to map to number\n 1:number/raw <- foo 3\n]\nrecipe foo x:_elem -> y:_elem [\n local-scope\n load-ingredients\n y <- add x, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 1"); } void test_specialize_with_literal_2() { Trace_file = "specialize_with_literal_2"; run("recipe main [\n local-scope\n # permit literal to map to character\n 1:character/raw <- foo 3\n]\nrecipe foo x:_elem -> y:_elem [\n local-scope\n load-ingredients\n y <- add x, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 1"); } void test_specialize_with_literal_3() { Trace_file = "specialize_with_literal_3"; Hide_errors = true; run("recipe main [\n local-scope\n # permit '0' to map to address to shape-shifting type-ingredient\n 1:address:shared:character/raw <- foo 0\n]\nrecipe foo x:address:_elem -> y:address:_elem [\n local-scope\n load-ingredients\n y <- copy x\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 1"); CHECK_TRACE_COUNT("error", 0); } void test_specialize_with_literal_4() { Trace_file = "specialize_with_literal_4"; Hide_errors = true; run("recipe main [\n local-scope\n # ambiguous call: what's the type of its ingredient?!\n foo 0\n]\nrecipe foo x:address:_elem -> y:address:_elem [\n local-scope\n load-ingredients\n y <- copy x\n]\n"); CHECK_TRACE_CONTENTS("error: foo: failed to map a type to xerror: foo: failed to map a type to y"); } void test_specialize_with_literal_5() { Trace_file = "specialize_with_literal_5"; run("recipe main [\n foo 3, 4 # recipe mapping two variables to literals\n]\nrecipe foo x:_elem, y:_elem [\n local-scope\n load-ingredients\n 1:number/raw <- add x, y\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 7 in location 1"); } void test_multiple_shape_shifting_variants() { Trace_file = "multiple_shape_shifting_variants"; run("# try to call two different shape-shifting recipes with the same name\nrecipe main [\n e1:d1:number <- merge 3\n e2:d2:number <- merge 4, 5\n 1:number/raw <- foo e1\n 2:number/raw <- foo e2\n]\n# the two shape-shifting definitions\nrecipe foo a:d1:_elem -> b:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo a:d2:_elem -> b:number [\n local-scope\n load-ingredients\n reply 35\n]\n# the shape-shifting containers they use\ncontainer d1:_elem [\n x:_elem\n]\ncontainer d2:_elem [\n x:number\n y:_elem\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1mem: storing 35 in location 2"); } void test_multiple_shape_shifting_variants_2() { Trace_file = "multiple_shape_shifting_variants_2"; run("# static dispatch between shape-shifting variants, _including pointer lookups_\nrecipe main [\n e1:d1:number <- merge 3\n e2:address:shared:d2:number <- new {(d2 number): type}\n 1:number/raw <- foo e1\n 2:number/raw <- foo *e2 # different from previous scenario\n]\nrecipe foo a:d1:_elem -> b:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo a:d2:_elem -> b:number [\n local-scope\n load-ingredients\n reply 35\n]\ncontainer d1:_elem [\n x:_elem\n]\ncontainer d2:_elem [\n x:number\n y:_elem\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1mem: storing 35 in location 2"); } void test_missing_type_in_shape_shifting_recipe() { Trace_file = "missing_type_in_shape_shifting_recipe"; Hide_errors = true; run("recipe main [\n a:d1:number <- merge 3\n foo a\n]\nrecipe foo a:d1:_elem -> b:number [\n local-scope\n load-ingredients\n copy e # no such variable\n reply 34\n]\ncontainer d1:_elem [\n x:_elem\n]\n"); CHECK_TRACE_CONTENTS("error: foo: unknown type for e (check the name for typos)error: specializing foo: missing type for e"); } void test_missing_type_in_shape_shifting_recipe_2() { Trace_file = "missing_type_in_shape_shifting_recipe_2"; Hide_errors = true; run("recipe main [\n a:d1:number <- merge 3\n foo a\n]\nrecipe foo a:d1:_elem -> b:number [\n local-scope\n load-ingredients\n get e, x:offset # unknown variable in a 'get', which does some extra checking\n reply 34\n]\ncontainer d1:_elem [\n x:_elem\n]\n"); CHECK_TRACE_CONTENTS("error: foo: unknown type for e (check the name for typos)error: specializing foo: missing type for e"); } void test_specialize_recursive_shape_shifting_recipe() { Trace_file = "specialize_recursive_shape_shifting_recipe"; transform("recipe main [\n 1:number <- copy 34\n 2:number <- foo 1:number\n]\nrecipe foo x:_elem -> y:number [\n local-scope\n load-ingredients\n {\n break\n y:number <- foo x\n }\n reply y\n]\n"); CHECK_TRACE_CONTENTS("transform: new specialization: foo_2"); } void test_specialize_most_similar_variant() { Trace_file = "specialize_most_similar_variant"; run("recipe main [\n 1:address:shared:number <- new number:type\n 2:number <- foo 1:address:shared:number\n]\nrecipe foo x:_elem -> y:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo x:address:shared:_elem -> y:number [\n local-scope\n load-ingredients\n reply 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 35 in location 2"); } void test_specialize_most_similar_variant_2() { Trace_file = "specialize_most_similar_variant_2"; run("# version with headers padded with lots of unrelated concrete types\nrecipe main [\n 1:number <- copy 23\n 2:address:shared:array:number <- copy 0\n 3:number <- foo 2:address:shared:array:number, 1:number\n]\n# variant with concrete type\nrecipe foo dummy:address:shared:array:number, x:number -> y:number, dummy:address:shared:array:number [\n local-scope\n load-ingredients\n reply 34\n]\n# shape-shifting variant\nrecipe foo dummy:address:shared:array:number, x:_elem -> y:number, dummy:address:shared:array:number [\n local-scope\n load-ingredients\n reply 35\n]\n# prefer the concrete variant\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 3"); } void test_specialize_most_similar_variant_3() { Trace_file = "specialize_most_similar_variant_3"; run("recipe main [\n 1:address:shared:array:character <- new [abc]\n foo 1:address:shared:array:character\n]\nrecipe foo x:address:shared:array:character [\n 2:number <- copy 34\n]\nrecipe foo x:address:_elem [\n 2:number <- copy 35\n]\n# make sure the more precise version was used\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 2"); } void test_specialize_literal_as_number() { Trace_file = "specialize_literal_as_number"; run("recipe main [\n 1:number <- foo 23\n]\nrecipe foo x:_elem -> y:number [\n local-scope\n load-ingredients\n reply 34\n]\nrecipe foo x:character -> y:number [\n local-scope\n load-ingredients\n reply 35\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1"); } void test_specialize_literal_as_number_2() { Trace_file = "specialize_literal_as_number_2"; run("# version calling with literal\nrecipe main [\n 1:number <- foo 0\n]\n# variant with concrete type\nrecipe foo x:number -> y:number [\n local-scope\n load-ingredients\n reply 34\n]\n# shape-shifting variant\nrecipe foo x:address:shared:_elem -> y:number [\n local-scope\n load-ingredients\n reply 35\n]\n# prefer the concrete variant, ignore concrete types in scoring the shape-shifting variant\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1"); } void test_can_modify_value_ingredients() { Trace_file = "can_modify_value_ingredients"; Hide_warnings = true; run("recipe main [\n local-scope\n p:address:shared:point <- new point:type\n foo *p\n]\nrecipe foo p:point [\n local-scope\n load-ingredients\n x:address:number <- get-address p, x:offset\n *x <- copy 34\n]\n"); CHECK_TRACE_COUNT("warn", 0); } void test_can_modify_ingredients_that_are_also_products() { Trace_file = "can_modify_ingredients_that_are_also_products"; Hide_warnings = true; run("recipe main [\n local-scope\n p:address:shared:point <- new point:type\n p <- foo p\n]\nrecipe foo p:address:shared:point -> p:address:shared:point [\n local-scope\n load-ingredients\n x:address:number <- get-address *p, x:offset\n *x <- copy 34\n]\n"); CHECK_TRACE_COUNT("warn", 0); } void test_ignore_literal_ingredients_for_immutability_checks() { Trace_file = "ignore_literal_ingredients_for_immutability_checks"; Hide_warnings = true; run("recipe main [\n local-scope\n p:address:shared:d1 <- new d1:type\n q:number <- foo p\n]\nrecipe foo p:address:shared:d1 -> q:number [\n local-scope\n load-ingredients\n x:address:shared:d1 <- new d1:type\n y:address:number <- get-address *x, p:offset # ignore this 'p'\n q <- copy 34\n]\ncontainer d1 [\n p:number\n q:number\n]\n"); CHECK_TRACE_COUNT("warn", 0); } void test_cannot_take_address_inside_immutable_ingredients() { Trace_file = "cannot_take_address_inside_immutable_ingredients"; Hide_warnings = true; run("recipe main [\n local-scope\n p:address:shared:point <- new point:type\n foo p\n]\nrecipe foo p:address:shared:point [\n local-scope\n load-ingredients\n x:address:number <- get-address *p, x:offset\n *x <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("warn: foo: cannot modify ingredient p after instruction 'x:address:number <- get-address *p, x:offset' because it's not also a product of foo"); } void test_cannot_call_mutating_recipes_on_immutable_ingredients() { Trace_file = "cannot_call_mutating_recipes_on_immutable_ingredients"; Hide_warnings = true; run("recipe main [\n local-scope\n p:address:shared:point <- new point:type\n foo p\n]\nrecipe foo p:address:shared:point [\n local-scope\n load-ingredients\n bar p\n]\nrecipe bar p:address:shared:point -> p:address:shared:point [\n local-scope\n load-ingredients\n x:address:number <- get-address *p, x:offset\n *x <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("warn: foo: cannot modify ingredient p at instruction 'bar p' because it's not also a product of foo"); } void test_cannot_modify_copies_of_immutable_ingredients() { Trace_file = "cannot_modify_copies_of_immutable_ingredients"; Hide_warnings = true; run("recipe main [\n local-scope\n p:address:shared:point <- new point:type\n foo p\n]\nrecipe foo p:address:shared:point [\n local-scope\n load-ingredients\n q:address:shared:point <- copy p\n x:address:number <- get-address *q, x:offset\n]\n"); CHECK_TRACE_CONTENTS("warn: foo: cannot modify q after instruction 'x:address:number <- get-address *q, x:offset' because that would modify ingredient p which is not also a product of foo"); } void test_can_traverse_immutable_ingredients() { Trace_file = "can_traverse_immutable_ingredients"; Hide_warnings = true; run("container test-list [\n next:address:shared:test-list\n]\nrecipe main [\n local-scope\n p:address:shared:test-list <- new test-list:type\n foo p\n]\nrecipe foo p:address:shared:test-list [\n local-scope\n load-ingredients\n p2:address:shared:test-list <- bar p\n]\nrecipe bar x:address:shared:test-list -> y:address:shared:test-list [\n local-scope\n load-ingredients\n y <- get *x, next:offset\n]\n"); CHECK_TRACE_COUNT("warn", 0); } void test_handle_optional_ingredients_in_immutability_checks() { Trace_file = "handle_optional_ingredients_in_immutability_checks"; Hide_warnings = true; run("recipe main [\n k:address:shared:number <- new number:type\n test k\n]\n# recipe taking an immutable address ingredient\nrecipe test k:address:shared:number [\n local-scope\n load-ingredients\n foo k\n]\n# ..calling a recipe with an optional address ingredient\nrecipe foo -> [\n local-scope\n load-ingredients\n k:address:shared:number, found?:boolean <- next-ingredient\n]\n"); CHECK_TRACE_COUNT("warn", 0); } void check_immutable_ingredients(recipe_ordinal r) { // to ensure a reagent isn't modified, it suffices to show that we never // call get-address or index-address with it, and that any non-primitive // recipe calls in the body aren't returning it as a product. const recipe& caller = get(Recipe, r); trace(9991, "transform") << "--- check mutability of ingredients in recipe " << caller.name << end(); if (!caller.has_header) return; // skip check for old-style recipes calling next-ingredient directly for (long long int i = 0; i < SIZE(caller.ingredients); ++i) { const reagent& current_ingredient = caller.ingredients.at(i); if (!is_mu_address(current_ingredient)) continue; // will be copied if (is_present_in_products(caller, current_ingredient.name)) continue; // not expected to be immutable if (has_property(current_ingredient, "contained-in")) { const string_tree* tmp = property(current_ingredient, "contained-in"); if (tmp->left || tmp->right || !is_present_in_ingredients(caller, tmp->value) || !is_present_in_products(caller, tmp->value)) raise_error << maybe(caller.name) << "contained-in can only point to another ingredient+product, but got " << to_string(property(current_ingredient, "contained-in")) << '\n' << end(); continue; } // End Immutable Ingredients Special-cases set immutable_vars; immutable_vars.insert(current_ingredient.name); for (long long int i = 0; i < SIZE(caller.steps); ++i) { const instruction& inst = caller.steps.at(i); check_immutable_ingredient_in_instruction(inst, immutable_vars, current_ingredient.name, caller); update_aliases(inst, immutable_vars); } } } void update_aliases(const instruction& inst, set& current_ingredient_and_aliases) { set current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases); if (!contains_key(Recipe, inst.operation)) { // primitive recipe if (inst.operation == COPY) { for (set::iterator p = current_ingredient_indices.begin(); p != current_ingredient_indices.end(); ++p) { current_ingredient_and_aliases.insert(inst.products.at(*p).name); } } } else { // defined recipe set contained_in_product_indices = scan_contained_in_product_indices(inst, current_ingredient_indices); for (set::iterator p = contained_in_product_indices.begin(); p != contained_in_product_indices.end(); ++p) { if (*p < SIZE(inst.products)) current_ingredient_and_aliases.insert(inst.products.at(*p).name); } } } set scan_contained_in_product_indices(const instruction& inst, set& ingredient_indices) { set selected_ingredient_names; const recipe& callee = get(Recipe, inst.operation); for (set::iterator p = ingredient_indices.begin(); p != ingredient_indices.end(); ++p) { if (*p >= SIZE(callee.ingredients)) continue; // optional immutable ingredient selected_ingredient_names.insert(callee.ingredients.at(*p).name); } set result; for (long long int i = 0; i < SIZE(callee.products); ++i) { const reagent& current_product = callee.products.at(i); const string_tree* contained_in_name = property(current_product, "contained-in"); if (contained_in_name && selected_ingredient_names.find(contained_in_name->value) != selected_ingredient_names.end()) result.insert(i); } return result; } void test_immutability_infects_contained_in_variables() { Trace_file = "immutability_infects_contained_in_variables"; Hide_warnings = true; transform("container test-list [\n next:address:shared:test-list\n]\nrecipe main [\n local-scope\n p:address:shared:test-list <- new test-list:type\n foo p\n]\nrecipe foo p:address:shared:test-list [ # p is immutable\n local-scope\n load-ingredients\n p2:address:shared:test-list <- test-next p # p2 is immutable\n p3:address:address:shared:test-list <- get-address *p2, next:offset # signal modification of p2\n]\nrecipe test-next x:address:shared:test-list -> y:address:shared:test-list/contained-in:x [\n local-scope\n load-ingredients\n y <- get *x, next:offset\n]\n"); CHECK_TRACE_CONTENTS("warn: foo: cannot modify p2 after instruction 'p3:address:address:shared:test-list <- get-address *p2, next:offset' because that would modify ingredient p which is not also a product of foo"); } void check_immutable_ingredient_in_instruction(const instruction& inst, const set& current_ingredient_and_aliases, const string& original_ingredient_name, const recipe& caller) { set current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases); if (current_ingredient_indices.empty()) return; // ingredient not found in call for (set::iterator p = current_ingredient_indices.begin(); p != current_ingredient_indices.end(); ++p) { const long long int current_ingredient_index = *p; reagent current_ingredient = inst.ingredients.at(current_ingredient_index); canonize_type(current_ingredient); const string& current_ingredient_name = current_ingredient.name; if (!contains_key(Recipe, inst.operation)) { // primitive recipe if (inst.operation == GET_ADDRESS || inst.operation == INDEX_ADDRESS) { if (current_ingredient_name == original_ingredient_name) raise << maybe(caller.name) << "cannot modify ingredient " << current_ingredient_name << " after instruction '" << to_string(inst) << "' because it's not also a product of " << caller.name << '\n' << end(); else raise << maybe(caller.name) << "cannot modify " << current_ingredient_name << " after instruction '" << to_string(inst) << "' because that would modify ingredient " << original_ingredient_name << " which is not also a product of " << caller.name << '\n' << end(); } } else { // defined recipe if (!is_mu_address(current_ingredient)) return; // making a copy is ok if (is_modified_in_recipe(inst.operation, current_ingredient_index, caller)) { if (current_ingredient_name == original_ingredient_name) raise << maybe(caller.name) << "cannot modify ingredient " << current_ingredient_name << " at instruction '" << to_string(inst) << "' because it's not also a product of " << caller.name << '\n' << end(); else raise << maybe(caller.name) << "cannot modify " << current_ingredient_name << " after instruction '" << to_string(inst) << "' because that would modify ingredient " << original_ingredient_name << " which is not also a product of " << caller.name << '\n' << end(); } } } } bool is_modified_in_recipe(recipe_ordinal r, long long int ingredient_index, const recipe& caller) { const recipe& callee = get(Recipe, r); if (!callee.has_header) { raise << maybe(caller.name) << "can't check mutability of ingredients in " << callee.name << " because it uses 'next-ingredient' directly, rather than a recipe header.\n" << end(); return true; } if (ingredient_index >= SIZE(callee.ingredients)) return false; // optional immutable ingredient return is_present_in_products(callee, callee.ingredients.at(ingredient_index).name); } bool is_present_in_products(const recipe& callee, const string& ingredient_name) { for (long long int i = 0; i < SIZE(callee.products); ++i) { if (callee.products.at(i).name == ingredient_name) return true; } return false; } bool is_present_in_ingredients(const recipe& callee, const string& ingredient_name) { for (long long int i = 0; i < SIZE(callee.ingredients); ++i) { if (callee.ingredients.at(i).name == ingredient_name) return true; } return false; } set ingredient_indices(const instruction& inst, const set& ingredient_names) { set result; for (long long int i = 0; i < SIZE(inst.ingredients); ++i) { if (is_literal(inst.ingredients.at(i))) continue; if (ingredient_names.find(inst.ingredients.at(i).name) != ingredient_names.end()) result.insert(i); } return result; } void test_can_modify_contained_in_addresses() { Trace_file = "can_modify_contained_in_addresses"; Hide_warnings = true; transform("container test-list [\n next:address:shared:test-list\n]\nrecipe main [\n local-scope\n p:address:shared:test-list <- new test-list:type\n foo p\n]\nrecipe foo p:address:shared:test-list -> p:address:shared:test-list [\n local-scope\n load-ingredients\n p2:address:shared:test-list <- test-next p\n p <- test-remove p2, p\n]\nrecipe test-next x:address:shared:test-list -> y:address:shared:test-list [\n local-scope\n load-ingredients\n y <- get *x, next:offset\n]\nrecipe test-remove x:address:shared:test-list/contained-in:from, from:address:shared:test-list -> from:address:shared:test-list [\n local-scope\n load-ingredients\n x2:address:address:shared:test-list <- get-address *x, next:offset # pretend modification\n]\n"); CHECK_TRACE_COUNT("warn", 0); } void test_call_literal_recipe() { Trace_file = "call_literal_recipe"; run("recipe main [\n 1:number <- call f, 34\n]\nrecipe f x:number -> y:number [\n local-scope\n load-ingredients\n y <- copy x\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1"); } void test_call_variable() { Trace_file = "call_variable"; run("recipe main [\n {1: (recipe number -> number)} <- copy f\n 2:number <- call {1: (recipe number -> number)}, 34\n]\nrecipe f x:number -> y:number [\n local-scope\n load-ingredients\n y <- copy x\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 2"); } void test_call_check_literal_recipe() { Trace_file = "call_check_literal_recipe"; Hide_errors = true; run("recipe main [\n 1:number <- call f, 34\n]\nrecipe f x:boolean -> y:boolean [\n local-scope\n load-ingredients\n y <- copy x\n]\n"); CHECK_TRACE_CONTENTS("error: main: ingredient 0 has the wrong type at '1:number <- call f, 34'error: main: product 0 has the wrong type at '1:number <- call f, 34'"); } void test_call_check_variable_recipe() { Trace_file = "call_check_variable_recipe"; Hide_errors = true; run("recipe main [\n {1: (recipe boolean -> boolean)} <- copy f\n 2:number <- call {1: (recipe boolean -> boolean)}, 34\n]\nrecipe f x:boolean -> y:boolean [\n local-scope\n load-ingredients\n y <- copy x\n]\n"); CHECK_TRACE_CONTENTS("error: main: ingredient 0 has the wrong type at '2:number <- call {1: (recipe boolean -> boolean)}, 34'error: main: product 0 has the wrong type at '2:number <- call {1: (recipe boolean -> boolean)}, 34'"); } void check_indirect_calls_against_header(const recipe_ordinal r) { trace(9991, "transform") << "--- type-check 'call' instructions inside recipe " << get(Recipe, r).name << end(); const recipe& caller = get(Recipe, r); for (long long int i = 0; i < SIZE(caller.steps); ++i) { const instruction& inst = caller.steps.at(i); if (inst.operation != CALL) continue; if (inst.ingredients.empty()) continue; // error raised above const reagent& callee = inst.ingredients.at(0); if (!is_mu_recipe(callee)) continue; // error raised above const recipe callee_header = is_literal(callee) ? get(Recipe, callee.value) : from_reagent(inst.ingredients.at(0)); if (!callee_header.has_header) continue; for (long int i = /*skip callee*/1; i < min(SIZE(inst.ingredients), SIZE(callee_header.ingredients)+/*skip callee*/1); ++i) { if (!types_coercible(callee_header.ingredients.at(i-/*skip callee*/1), inst.ingredients.at(i))) raise_error << maybe(caller.name) << "ingredient " << i-/*skip callee*/1 << " has the wrong type at '" << to_string(inst) << "'\n" << end(); } for (long int i = 0; i < min(SIZE(inst.products), SIZE(callee_header.products)); ++i) { if (is_dummy(inst.products.at(i))) continue; if (!types_coercible(callee_header.products.at(i), inst.products.at(i))) raise_error << maybe(caller.name) << "product " << i << " has the wrong type at '" << to_string(inst) << "'\n" << end(); } } } recipe from_reagent(const reagent& r) { assert(r.type->name == "recipe"); recipe result_header; // will contain only ingredients and products, nothing else result_header.has_header = true; const type_tree* curr = r.type->right; for (; curr; curr=curr->right) { if (curr->name == "->") { curr = curr->right; // skip delimiter break; } result_header.ingredients.push_back("recipe:"+curr->name); } for (; curr; curr=curr->right) result_header.products.push_back("recipe:"+curr->name); return result_header; } bool is_mu_recipe(reagent r) { if (!r.type) return false; if (r.type->name == "recipe") return true; if (r.type->name == "recipe-literal") return true; // End is_mu_recipe Cases return false; } void test_copy_typecheck_recipe_variable() { Trace_file = "copy_typecheck_recipe_variable"; Hide_errors = true; run("recipe main [\n 3:number <- copy 34 # abc def\n {1: (recipe number -> number)} <- copy f # store literal in a matching variable\n {2: (recipe boolean -> boolean)} <- copy {1: (recipe number -> number)} # mismatch between recipe variables\n]\nrecipe f x:number -> y:number [\n local-scope\n load-ingredients\n y <- copy x\n]\n"); CHECK_TRACE_CONTENTS("error: main: can't copy {1: (recipe number -> number)} to {2: (recipe boolean -> boolean)}; types don't match"); } void test_copy_typecheck_recipe_variable_2() { Trace_file = "copy_typecheck_recipe_variable_2"; Hide_errors = true; run("recipe main [\n {1: (recipe number -> number)} <- copy f # mismatch with a recipe literal\n]\nrecipe f x:boolean -> y:boolean [\n local-scope\n load-ingredients\n y <- copy x\n]\n"); CHECK_TRACE_CONTENTS("error: main: can't copy f to {1: (recipe number -> number)}; types don't match"); } void test_scheduler() { Trace_file = "scheduler"; run("recipe f1 [\n start-running f2\n # wait for f2 to run\n {\n jump-unless 1:number, -1\n }\n]\nrecipe f2 [\n 1:number <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("schedule: f1schedule: f2"); } void run(routine* rr) { Routines.push_back(rr); Current_routine_index = 0, Current_routine = Routines.at(0); while (!all_routines_done()) { skip_to_next_routine(); assert(Current_routine); assert(Current_routine->state == RUNNING); trace(9990, "schedule") << current_routine_label() << end(); run_current_routine(Scheduling_interval); // Scheduler State Transitions if (Current_routine->completed()) Current_routine->state = COMPLETED; if (Current_routine->limit >= 0) { if (Current_routine->limit <= Scheduling_interval) { trace(9999, "schedule") << "discontinuing routine " << Current_routine->id << end(); Current_routine->state = DISCONTINUED; Current_routine->limit = 0; } else { Current_routine->limit -= Scheduling_interval; } } for (long long int i = 0; i < SIZE(Routines); ++i) { if (Routines.at(i)->state != WAITING) continue; if (Routines.at(i)->waiting_on_location && get_or_insert(Memory, Routines.at(i)->waiting_on_location) != Routines.at(i)->old_value_of_waiting_location) { trace(9999, "schedule") << "waking up routine\n" << end(); Routines.at(i)->state = RUNNING; Routines.at(i)->waiting_on_location = Routines.at(i)->old_value_of_waiting_location = 0; } } // Wake up any routines waiting for other routines to go to sleep. // Important: this must come after the scheduler loop above giving routines // waiting for locations to change a chance to wake up. for (long long int i = 0; i < SIZE(Routines); ++i) { if (Routines.at(i)->state != WAITING) continue; if (!Routines.at(i)->waiting_on_routine) continue; long long int id = Routines.at(i)->waiting_on_routine; assert(id != Routines.at(i)->id); // routine can't wait on itself for (long long int j = 0; j < SIZE(Routines); ++j) { if (Routines.at(j)->id == id && Routines.at(j)->state != RUNNING) { trace(9999, "schedule") << "waking up routine " << Routines.at(i)->id << end(); Routines.at(i)->state = RUNNING; Routines.at(i)->waiting_on_routine = 0; } } } // End Scheduler State Transitions // Scheduler Cleanup for (long long int i = 0; i < SIZE(Routines); ++i) { if (Routines.at(i)->state == COMPLETED) continue; if (Routines.at(i)->parent_index < 0) continue; // root thread if (has_completed_parent(i)) { Routines.at(i)->state = COMPLETED; } } // End Scheduler Cleanup } } bool all_routines_done() { for (long long int i = 0; i < SIZE(Routines); ++i) { if (Routines.at(i)->state == RUNNING) { return false; } } return true; } // skip Current_routine_index past non-RUNNING routines void skip_to_next_routine() { assert(!Routines.empty()); assert(Current_routine_index < SIZE(Routines)); for (long long int i = (Current_routine_index+1)%SIZE(Routines); i != Current_routine_index; i = (i+1)%SIZE(Routines)) { if (Routines.at(i)->state == RUNNING) { Current_routine_index = i; Current_routine = Routines.at(i); return; } } } string current_routine_label() { ostringstream result; const call_stack& calls = Current_routine->calls; for (call_stack::const_iterator p = calls.begin(); p != calls.end(); ++p) { if (p != calls.begin()) result << '/'; result << get(Recipe, p->running_recipe).name; } return result.str(); } void test_scheduler_runs_single_routine() { Trace_file = "scheduler_runs_single_routine"; Scheduling_interval = 1; run("recipe f1 [\n 1:number <- copy 0\n 2:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("schedule: f1run: 1:number <- copy 0schedule: f1run: 2:number <- copy 0"); } void test_scheduler_interleaves_routines() { Trace_file = "scheduler_interleaves_routines"; Scheduling_interval = 1; run("recipe f1 [\n start-running f2\n 1:number <- copy 0\n 2:number <- copy 0\n]\nrecipe f2 [\n 3:number <- copy 0\n 4:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("schedule: f1run: start-running f2schedule: f2run: 3:number <- copy 0schedule: f1run: 1:number <- copy 0schedule: f2run: 4:number <- copy 0schedule: f1run: 2:number <- copy 0"); } void test_start_running_takes_ingredients() { Trace_file = "start_running_takes_ingredients"; run("recipe f1 [\n start-running f2, 3\n # wait for f2 to run\n {\n jump-unless 1:number, -1\n }\n]\nrecipe f2 [\n 1:number <- next-ingredient\n 2:number <- add 1:number, 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 4 in location 2"); } void test_start_running_returns_routine_id() { Trace_file = "start_running_returns_routine_id"; run("recipe f1 [\n 1:number <- start-running f2\n]\nrecipe f2 [\n 12:number <- copy 44\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 2 in location 1"); } void test_scheduler_skips_completed_routines() { Trace_file = "scheduler_skips_completed_routines"; recipe_ordinal f1 = load("recipe f1 [\n1:number <- copy 0\n]\n").front(); recipe_ordinal f2 = load("recipe f2 [\n2:number <- copy 0\n]\n").front(); Routines.push_back(new routine(f1)); // f1 meant to run Routines.push_back(new routine(f2)); Routines.back()->state = COMPLETED; // f2 not meant to run run("# must have at least one routine without escaping\nrecipe f3 [\n 3:number <- copy 0\n]\n# by interleaving '+' lines with '-' lines, we allow f1 and f3 to run in any order\n"); CHECK_TRACE_CONTENTS("schedule: f1mem: storing 0 in location 1"); CHECK_TRACE_DOESNT_CONTAIN("schedule: f2"); CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 2"); run(""); CHECK_TRACE_CONTENTS("schedule: f3mem: storing 0 in location 3"); } void test_scheduler_starts_at_middle_of_routines() { Trace_file = "scheduler_starts_at_middle_of_routines"; Routines.push_back(new routine(COPY)); Routines.back()->state = COMPLETED; run("recipe f1 [\n 1:number <- copy 0\n 2:number <- copy 0\n]\n"); CHECK_TRACE_CONTENTS("schedule: f1"); CHECK_TRACE_DOESNT_CONTAIN("run: idle"); } void test_scheduler_terminates_routines_after_errors() { Trace_file = "scheduler_terminates_routines_after_errors"; Hide_errors = true; Scheduling_interval = 2; run("recipe f1 [\n start-running f2\n 1:number <- copy 0\n 2:number <- copy 0\n]\nrecipe f2 [\n # divide by 0 twice\n 3:number <- divide-with-remainder 4, 0\n 4:number <- divide-with-remainder 4, 0\n]\n# f2 should stop after first divide by 0\n"); CHECK_TRACE_CONTENTS("error: f2: divide by zero in '3:number <- divide-with-remainder 4, 0'"); CHECK_TRACE_DOESNT_CONTAIN("error: f2: divide by zero in '4:number <- divide-with-remainder 4, 0'"); } void test_scheduler_kills_orphans() { Trace_file = "scheduler_kills_orphans"; run("recipe main [\n start-running f1\n # f1 never actually runs because its parent completes without waiting for it\n]\nrecipe f1 [\n 1:number <- copy 0\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("schedule: f1"); } bool has_completed_parent(long long int routine_index) { for (long long int j = routine_index; j >= 0; j = Routines.at(j)->parent_index) { if (Routines.at(j)->state == COMPLETED) return true; } return false; } void test_routine_state_test() { Trace_file = "routine_state_test"; Scheduling_interval = 2; run("recipe f1 [\n 1:number/child-id <- start-running f2\n 12:number <- copy 0 # race condition since we don't care about location 12\n # thanks to Scheduling_interval, f2's one instruction runs in between here and completes\n 2:number/state <- routine-state 1:number/child-id\n]\nrecipe f2 [\n 12:number <- copy 0\n # trying to run a second instruction marks routine as completed\n]\n# recipe f2 should be in state COMPLETED\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 2"); } void test_routine_discontinues_past_limit() { Trace_file = "routine_discontinues_past_limit"; Scheduling_interval = 2; run("recipe f1 [\n 1:number/child-id <- start-running f2\n limit-time 1:number/child-id, 10\n # padding loop just to make sure f2 has time to completed\n 2:number <- copy 20\n 2:number <- subtract 2:number, 1\n jump-if 2:number, -2:offset\n]\nrecipe f2 [\n jump -1:offset # run forever\n $print [should never get here], 10/newline\n]\n# f2 terminates\n"); CHECK_TRACE_CONTENTS("schedule: discontinuing routine 2"); } void test_new_concurrent() { Trace_file = "new_concurrent"; run("recipe f1 [\n start-running f2\n 1:address:shared:number/raw <- new number:type\n # wait for f2 to complete\n {\n loop-unless 4:number/raw\n }\n]\nrecipe f2 [\n 2:address:shared:number/raw <- new number:type\n # hack: assumes scheduler implementation\n 3:boolean/raw <- equal 1:address:shared:number/raw, 2:address:shared:number/raw\n # signal f2 complete\n 4:number/raw <- copy 1\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3"); } void test_wait_for_location() { Trace_file = "wait_for_location"; run("recipe f1 [\n 1:number <- copy 0\n start-running f2\n wait-for-location 1:number\n # now wait for f2 to run and modify location 1 before using its value\n 2:number <- copy 1:number\n]\nrecipe f2 [\n 1:number <- copy 34\n]\n# if we got the synchronization wrong we'd be storing 0 in location 2\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 2"); } void test_wait_for_routine() { Trace_file = "wait_for_routine"; run("recipe f1 [\n 1:number <- copy 0\n 12:number/routine <- start-running f2\n wait-for-routine 12:number/routine\n # now wait for f2 to run and modify location 1 before using its value\n 3:number <- copy 1:number\n]\nrecipe f2 [\n 1:number <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("schedule: f1run: waiting for routine 2schedule: f2schedule: waking up routine 1schedule: f1mem: storing 34 in location 3"); } long long int some_other_running_routine() { for (long long int i = 0; i < SIZE(Routines); ++i) { if (i == Current_routine_index) continue; assert(Routines.at(i) != Current_routine); assert(Routines.at(i)->id != Current_routine->id); if (Routines.at(i)->state == RUNNING) return Routines.at(i)->id; } return 0; } void test_round_to_nearest_integer() { Trace_file = "round_to_nearest_integer"; run("recipe main [\n 1:number <- round 12.2\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 12 in location 1"); } // A universal hash function that can handle objects of any type. // // The way it's currently implemented, two objects will have the same hash if // all their non-address fields (all the way down) expand to the same sequence // of scalar values. In particular, a container with all zero addresses hashes // to 0. Hopefully this won't be an issue because we are usually hashing // objects of a single type in any given hash table. // // Based on http://burtleburtle.net/bob/hash/hashfaq.html size_t hash(size_t h, reagent& r) { //? cerr << debug_string(r) << '\n'; canonize(r); if (is_mu_string(r)) // optimization return hash_mu_string(h, r); else if (is_mu_address(r)) return hash_mu_address(h, r); else if (is_mu_scalar(r)) return hash_mu_scalar(h, r); else if (is_mu_array(r)) return hash_mu_array(h, r); else if (is_mu_container(r)) return hash_mu_container(h, r); else if (is_mu_exclusive_container(r)) return hash_mu_exclusive_container(h, r); assert(false); } size_t hash_mu_scalar(size_t h, const reagent& r) { double input = is_literal(r) ? r.value : get_or_insert(Memory, r.value); return hash_iter(h, static_cast(input)); } size_t hash_mu_address(size_t h, reagent& r) { if (r.value == 0) return 0; r.value = get_or_insert(Memory, r.value); drop_from_type(r, "address"); if (r.type->name == "shared") { ++r.value; drop_from_type(r, "shared"); } return hash(h, r); } size_t hash_mu_string(size_t h, const reagent& r) { string input = read_mu_string(get_or_insert(Memory, r.value)); for (long long int i = 0; i < SIZE(input); ++i) { h = hash_iter(h, static_cast(input.at(i))); //? cerr << i << ": " << h << '\n'; } return h; } size_t hash_mu_array(size_t h, const reagent& r) { long long int size = get_or_insert(Memory, r.value); reagent elem = r; delete elem.type; elem.type = new type_tree(*array_element(r.type)); for (long long int i=0, address = r.value+1; i < size; ++i, address += size_of(elem)) { reagent tmp = elem; tmp.value = address; h = hash(h, tmp); //? cerr << i << " (" << address << "): " << h << '\n'; } return h; } bool is_mu_container(const reagent& r) { if (r.type->value == 0) return false; type_info& info = get(Type, r.type->value); return info.kind == CONTAINER; } size_t hash_mu_container(size_t h, const reagent& r) { assert(r.type->value); type_info& info = get(Type, r.type->value); long long int address = r.value; long long int offset = 0; for (long long int i = 0; i < SIZE(info.elements); ++i) { reagent element = element_type(r, i); if (has_property(element, "ignore-for-hash")) continue; element.set_value(address+offset); h = hash(h, element); //? cerr << i << ": " << h << '\n'; offset += size_of(info.elements.at(i).type); } return h; } bool is_mu_exclusive_container(const reagent& r) { if (r.type->value == 0) return false; type_info& info = get(Type, r.type->value); return info.kind == EXCLUSIVE_CONTAINER; } size_t hash_mu_exclusive_container(size_t h, const reagent& r) { assert(r.type->value); long long int tag = get(Memory, r.value); reagent variant = variant_type(r, tag); // todo: move this warning to container definition time if (has_property(variant, "ignore-for-hash")) raise << get(Type, r.type->value).name << ": /ignore-for-hash won't work in exclusive containers\n"; variant.set_value(r.value + /*skip tag*/1); h = hash(h, variant); return h; } size_t hash_iter(size_t h, size_t input) { h += input; h += (h<<10); h ^= (h>>6); h += (h<<3); h ^= (h>>11); h += (h<<15); return h; } void test_hash_container_checks_all_elements() { Trace_file = "hash_container_checks_all_elements"; run("container foo [\n x:number\n y:character\n]\nrecipe main [\n 1:foo <- merge 34, 97/a\n 3:number <- hash 1:foo\n reply-unless 3:number\n 4:foo <- merge 34, 98/a\n 6:number <- hash 4:foo\n reply-unless 6:number\n 7:boolean <- equal 3:number, 6:number\n]\n# hash on containers includes all elements\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 7"); } void test_hash_exclusive_container_checks_all_elements() { Trace_file = "hash_exclusive_container_checks_all_elements"; run("exclusive-container foo [\n x:bar\n y:number\n]\ncontainer bar [\n a:number\n b:number\n]\nrecipe main [\n 1:foo <- merge 0/x, 34, 35\n 4:number <- hash 1:foo\n reply-unless 4:number\n 5:foo <- merge 0/x, 34, 36\n 8:number <- hash 5:foo\n reply-unless 8:number\n 9:boolean <- equal 4:number, 8:number\n]\n# hash on containers includes all elements\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 9"); } void test_hash_can_ignore_container_elements() { Trace_file = "hash_can_ignore_container_elements"; run("container foo [\n x:number\n y:character/ignore-for-hash\n]\nrecipe main [\n 1:foo <- merge 34, 97/a\n 3:number <- hash 1:foo\n reply-unless 3:number\n 4:foo <- merge 34, 98/a\n 6:number <- hash 4:foo\n reply-unless 6:number\n 7:boolean <- equal 3:number, 6:number\n]\n# hashes match even though y is different\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 7"); } void test_hash_of_zero_address() { Trace_file = "hash_of_zero_address"; run("recipe main [\n 1:address:number <- copy 0\n 2:number <- hash 1:address:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 2"); } void test_hash_of_numbers_ignores_fractional_part() { Trace_file = "hash_of_numbers_ignores_fractional_part"; run("recipe main [\n 1:number <- hash 1.5\n 2:number <- hash 1\n 3:boolean <- equal 1:number, 2:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 3"); } void test_hash_of_array_same_as_string() { Trace_file = "hash_of_array_same_as_string"; run("recipe main [\n 10:number <- copy 3\n 11:number <- copy 97\n 12:number <- copy 98\n 13:number <- copy 99\n 2:number <- hash 10:array:number/unsafe\n reply-unless 2:number\n 3:address:shared:array:character <- new [abc]\n 4:number <- hash 3:address:shared:array:character\n reply-unless 4:number\n 5:boolean <- equal 2:number, 4:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 5"); } void test_hash_ignores_address_value() { Trace_file = "hash_ignores_address_value"; run("recipe main [\n 1:address:shared:number <- new number:type\n *1:address:shared:number <- copy 34\n 2:number <- hash 1:address:shared:number\n 3:address:shared:number <- new number:type\n *3:address:shared:number <- copy 34\n 4:number <- hash 3:address:shared:number\n 5:boolean <- equal 2:number, 4:number\n]\n# different addresses hash to the same result as long as the values the point to do so\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 5"); } void test_hash_ignores_address_refcount() { Trace_file = "hash_ignores_address_refcount"; run("recipe main [\n 1:address:shared:number <- new number:type\n *1:address:shared:number <- copy 34\n 2:number <- hash 1:address:shared:number\n reply-unless 2:number\n # increment refcount\n 3:address:shared:number <- copy 1:address:shared:number\n 4:number <- hash 3:address:shared:number\n reply-unless 4:number\n 5:boolean <- equal 2:number, 4:number\n]\n# hash doesn't change when refcount changes\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 5"); } void test_hash_container_depends_only_on_elements() { Trace_file = "hash_container_depends_only_on_elements"; run("container foo [\n x:number\n y:character\n]\ncontainer bar [\n x:number\n y:character\n]\nrecipe main [\n 1:foo <- merge 34, 97/a\n 3:number <- hash 1:foo\n reply-unless 3:number\n 4:bar <- merge 34, 97/a\n 6:number <- hash 4:bar\n reply-unless 6:number\n 7:boolean <- equal 3:number, 6:number\n]\n# containers with identical elements return identical hashes\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 7"); } void test_hash_container_depends_only_on_elements_2() { Trace_file = "hash_container_depends_only_on_elements_2"; run("container foo [\n x:number\n y:character\n z:address:shared:number\n]\nrecipe main [\n 1:address:shared:number <- new number:type\n *1:address:shared:number <- copy 34\n 2:foo <- merge 34, 97/a, 1:address:shared:number\n 5:number <- hash 2:foo\n reply-unless 5:number\n 6:address:shared:number <- new number:type\n *6:address:shared:number <- copy 34\n 7:foo <- merge 34, 97/a, 6:address:shared:number\n 10:number <- hash 7:foo\n reply-unless 10:number\n 11:boolean <- equal 5:number, 10:number\n]\n# containers with identical 'leaf' elements return identical hashes\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 11"); } void test_hash_container_depends_only_on_elements_3() { Trace_file = "hash_container_depends_only_on_elements_3"; run("container foo [\n x:number\n y:character\n z:bar\n]\ncontainer bar [\n x:number\n y:number\n]\nrecipe main [\n 1:foo <- merge 34, 97/a, 47, 48\n 6:number <- hash 1:foo\n reply-unless 6:number\n 7:foo <- merge 34, 97/a, 47, 48\n 12:number <- hash 7:foo\n reply-unless 12:number\n 13:boolean <- equal 6:number, 12:number\n]\n# containers with identical 'leaf' elements return identical hashes\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 13"); } void test_hash_exclusive_container_ignores_tag() { Trace_file = "hash_exclusive_container_ignores_tag"; run("exclusive-container foo [\n x:bar\n y:number\n]\ncontainer bar [\n a:number\n b:number\n]\nrecipe main [\n 1:foo <- merge 0/x, 34, 35\n 4:number <- hash 1:foo\n reply-unless 4:number\n 5:bar <- merge 34, 35\n 7:number <- hash 5:bar\n reply-unless 7:number\n 8:boolean <- equal 4:number, 7:number\n]\n# hash on containers includes all elements\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 8"); } void test_hash_matches_old_version() { Trace_file = "hash_matches_old_version"; run("recipe main [\n 1:address:shared:array:character <- new [abc]\n 2:number <- hash 1:address:shared:array:character\n 3:number <- hash_old 1:address:shared:array:character\n 4:number <- equal 2:number, 3:number\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 1 in location 4"); } void test_convert_names_does_not_fail_when_mixing_special_names_and_numeric_locations() { Trace_file = "convert_names_does_not_fail_when_mixing_special_names_and_numeric_locations"; Scenario_testing_scenario = true; Hide_errors = true; run("recipe main [\n screen:number <- copy 1:number\n]\n"); CHECK_TRACE_DOESNT_CONTAIN("error: mixing variable names and numeric addresses in main"); CHECK_TRACE_COUNT("error", 0); } void check_screen(const string& expected_contents, const int color) { assert(!current_call().default_space); // not supported long long int screen_location = get_or_insert(Memory, SCREEN)+/*skip refcount*/1; int data_offset = find_element_name(get(Type_ordinal, "screen"), "data", ""); assert(data_offset >= 0); long long int screen_data_location = screen_location+data_offset; // type: address:shared:array:character long long int screen_data_start = get_or_insert(Memory, screen_data_location) + /*skip refcount*/1; // type: array:character int width_offset = find_element_name(get(Type_ordinal, "screen"), "num-columns", ""); long long int screen_width = get_or_insert(Memory, screen_location+width_offset); int height_offset = find_element_name(get(Type_ordinal, "screen"), "num-rows", ""); long long int screen_height = get_or_insert(Memory, screen_location+height_offset); raw_string_stream cursor(expected_contents); // todo: too-long expected_contents should fail long long int addr = screen_data_start+/*skip length*/1; for (long long int row = 0; row < screen_height; ++row) { cursor.skip_whitespace_and_comments(); if (cursor.at_end()) break; assert(cursor.get() == '.'); for (long long int column = 0; column < screen_width; ++column, addr+= /*size of screen-cell*/2) { const int cell_color_offset = 1; uint32_t curr = cursor.get(); if (get_or_insert(Memory, addr) == 0 && isspace(curr)) continue; if (curr == ' ' && color != -1 && color != get_or_insert(Memory, addr+cell_color_offset)) { // filter out other colors continue; } if (get_or_insert(Memory, addr) != 0 && get_or_insert(Memory, addr) == curr) { if (color == -1 || color == get_or_insert(Memory, addr+cell_color_offset)) continue; // contents match but color is off if (Current_scenario && !Scenario_testing_scenario) { // genuine test in a mu file raise_error << "\nF - " << Current_scenario->name << ": expected screen location (" << row << ", " << column << ", address " << addr << ", value " << no_scientific(get_or_insert(Memory, addr)) << ") to be in color " << color << " instead of " << no_scientific(get_or_insert(Memory, addr+cell_color_offset)) << "\n" << end(); } else { // just testing check_screen raise_error << "expected screen location (" << row << ", " << column << ") to be in color " << color << " instead of " << no_scientific(get_or_insert(Memory, addr+cell_color_offset)) << '\n' << end(); } if (!Scenario_testing_scenario) { Passed = false; ++Num_failures; } return; } // really a mismatch // can't print multi-byte unicode characters in errors just yet. not very useful for debugging anyway. char expected_pretty[10] = {0}; if (curr < 256 && !iscntrl(curr)) { // " ('')" expected_pretty[0] = ' ', expected_pretty[1] = '(', expected_pretty[2] = '\'', expected_pretty[3] = static_cast(curr), expected_pretty[4] = '\'', expected_pretty[5] = ')', expected_pretty[6] = '\0'; } char actual_pretty[10] = {0}; if (get_or_insert(Memory, addr) < 256 && !iscntrl(get_or_insert(Memory, addr))) { // " ('')" actual_pretty[0] = ' ', actual_pretty[1] = '(', actual_pretty[2] = '\'', actual_pretty[3] = static_cast(get_or_insert(Memory, addr)), actual_pretty[4] = '\'', actual_pretty[5] = ')', actual_pretty[6] = '\0'; } ostringstream color_phrase; if (color != -1) color_phrase << " in color " << color; if (Current_scenario && !Scenario_testing_scenario) { // genuine test in a mu file raise_error << "\nF - " << Current_scenario->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(); dump_screen(); } else { // just testing check_screen raise_error << "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(); } if (!Scenario_testing_scenario) { Passed = false; ++Num_failures; } return; } assert(cursor.get() == '.'); } cursor.skip_whitespace_and_comments(); assert(cursor.at_end()); } raw_string_stream::raw_string_stream(const string& backing) :index(0), max(SIZE(backing)), buf(backing.c_str()) {} bool raw_string_stream::at_end() const { if (index >= max) return true; if (tb_utf8_char_length(buf[index]) > max-index) { raise_error << "unicode string seems corrupted at index "<< index << " character " << static_cast(buf[index]) << '\n' << end(); return true; } return false; } uint32_t raw_string_stream::get() { assert(index < max); // caller must check bounds before calling 'get' uint32_t result = 0; int length = tb_utf8_char_to_unicode(&result, &buf[index]); assert(length != TB_EOF); index += length; return result; } uint32_t raw_string_stream::peek() { assert(index < max); // caller must check bounds before calling 'get' uint32_t result = 0; int length = tb_utf8_char_to_unicode(&result, &buf[index]); assert(length != TB_EOF); return result; } void raw_string_stream::skip_whitespace_and_comments() { while (!at_end()) { if (isspace(peek())) get(); else if (peek() == '#') { // skip comment get(); while (peek() != '\n') get(); // implicitly also handles CRLF } else break; } } void dump_screen() { assert(!current_call().default_space); // not supported long long int screen_location = get_or_insert(Memory, SCREEN) + /*skip refcount*/1; int width_offset = find_element_name(get(Type_ordinal, "screen"), "num-columns", ""); long long int screen_width = get_or_insert(Memory, screen_location+width_offset); int height_offset = find_element_name(get(Type_ordinal, "screen"), "num-rows", ""); long long int screen_height = get_or_insert(Memory, screen_location+height_offset); int data_offset = find_element_name(get(Type_ordinal, "screen"), "data", ""); assert(data_offset >= 0); long long int screen_data_location = screen_location+data_offset; // type: address:shared:array:character long long int screen_data_start = get_or_insert(Memory, screen_data_location) + /*skip refcount*/1; // type: array:character assert(get_or_insert(Memory, screen_data_start) == screen_width*screen_height); long long int curr = screen_data_start+1; // skip length for (long long int row = 0; row < screen_height; ++row) { cerr << '.'; for (long long int col = 0; col < screen_width; ++col) { if (get_or_insert(Memory, curr)) cerr << to_unicode(static_cast(get_or_insert(Memory, curr))); else cerr << ' '; curr += /*size of screen-cell*/2; } cerr << ".\n"; } } void initialize_key_names() { Key["F1"] = TB_KEY_F1; Key["F2"] = TB_KEY_F2; Key["F3"] = TB_KEY_F3; Key["F4"] = TB_KEY_F4; Key["F5"] = TB_KEY_F5; Key["F6"] = TB_KEY_F6; Key["F7"] = TB_KEY_F7; Key["F8"] = TB_KEY_F8; Key["F9"] = TB_KEY_F9; Key["F10"] = TB_KEY_F10; Key["F11"] = TB_KEY_F11; Key["F12"] = TB_KEY_F12; Key["insert"] = TB_KEY_INSERT; Key["delete"] = TB_KEY_DELETE; Key["home"] = TB_KEY_HOME; Key["end"] = TB_KEY_END; Key["page-up"] = TB_KEY_PGUP; Key["page-down"] = TB_KEY_PGDN; Key["up-arrow"] = TB_KEY_ARROW_UP; Key["down-arrow"] = TB_KEY_ARROW_DOWN; Key["left-arrow"] = TB_KEY_ARROW_LEFT; Key["right-arrow"] = TB_KEY_ARROW_RIGHT; Key["ctrl-a"] = TB_KEY_CTRL_A; Key["ctrl-b"] = TB_KEY_CTRL_B; Key["ctrl-c"] = TB_KEY_CTRL_C; Key["ctrl-d"] = TB_KEY_CTRL_D; Key["ctrl-e"] = TB_KEY_CTRL_E; Key["ctrl-f"] = TB_KEY_CTRL_F; Key["ctrl-g"] = TB_KEY_CTRL_G; Key["backspace"] = TB_KEY_BACKSPACE; Key["ctrl-h"] = TB_KEY_CTRL_H; Key["tab"] = TB_KEY_TAB; Key["ctrl-i"] = TB_KEY_CTRL_I; Key["ctrl-j"] = TB_KEY_CTRL_J; Key["enter"] = TB_KEY_NEWLINE; // ignore CR/LF distinction; there is only 'enter' Key["ctrl-k"] = TB_KEY_CTRL_K; Key["ctrl-l"] = TB_KEY_CTRL_L; Key["ctrl-m"] = TB_KEY_CTRL_M; Key["ctrl-n"] = TB_KEY_CTRL_N; Key["ctrl-o"] = TB_KEY_CTRL_O; Key["ctrl-p"] = TB_KEY_CTRL_P; Key["ctrl-q"] = TB_KEY_CTRL_Q; Key["ctrl-r"] = TB_KEY_CTRL_R; Key["ctrl-s"] = TB_KEY_CTRL_S; Key["ctrl-t"] = TB_KEY_CTRL_T; Key["ctrl-u"] = TB_KEY_CTRL_U; Key["ctrl-v"] = TB_KEY_CTRL_V; Key["ctrl-w"] = TB_KEY_CTRL_W; Key["ctrl-x"] = TB_KEY_CTRL_X; Key["ctrl-y"] = TB_KEY_CTRL_Y; Key["ctrl-z"] = TB_KEY_CTRL_Z; Key["escape"] = TB_KEY_ESC; } long long int count_events(const recipe& r) { long long int result = 0; for (long long int i = 0; i < SIZE(r.steps); ++i) { const instruction& curr = r.steps.at(i); if (curr.name == "type") result += unicode_length(curr.ingredients.at(0).name); else result++; } return result; } long long int size_of_event() { // memoize result if already computed static long long int result = 0; if (result) return result; type_tree* type = new type_tree("event", get(Type_ordinal, "event")); result = size_of(type); delete type; return result; } long long int size_of_console() { // memoize result if already computed static long long int result = 0; if (result) return result; assert(get(Type_ordinal, "console")); type_tree* type = new type_tree("console", get(Type_ordinal, "console")); result = size_of(type)+/*refcount*/1; delete type; return result; } void start_trace_browser() { if (!Trace_stream) return; cerr << "computing min depth to display\n"; long long int min_depth = 9999; for (long long int i = 0; i < SIZE(Trace_stream->past_lines); ++i) { trace_line& curr_line = Trace_stream->past_lines.at(i); if (curr_line.depth < min_depth) min_depth = curr_line.depth; } cerr << "min depth is " << min_depth << '\n'; cerr << "computing lines to display\n"; for (long long int i = 0; i < SIZE(Trace_stream->past_lines); ++i) { if (Trace_stream->past_lines.at(i).depth == min_depth) Visible.insert(i); } tb_init(); Display_row = Display_column = 0; tb_event event; Top_of_screen = 0; refresh_screen_rows(); while (true) { render(); do { tb_poll_event(&event); } while (event.type != TB_EVENT_KEY); long long int key = event.key ? event.key : event.ch; if (key == 'q' || key == 'Q') break; if (key == 'j' || key == TB_KEY_ARROW_DOWN) { // move cursor one line down if (Display_row < Last_printed_row) ++Display_row; } if (key == 'k' || key == TB_KEY_ARROW_UP) { // move cursor one line up if (Display_row > 0) --Display_row; } if (key == 'H') { // move cursor to top of screen Display_row = 0; } if (key == 'M') { // move cursor to center of screen Display_row = tb_height()/2; } if (key == 'L') { // move cursor to bottom of screen Display_row = tb_height()-1; } if (key == 'J' || key == TB_KEY_PGDN) { // page-down if (Trace_index.find(tb_height()-1) != Trace_index.end()) { Top_of_screen = get(Trace_index, tb_height()-1) + 1; refresh_screen_rows(); } } if (key == 'K' || key == TB_KEY_PGUP) { // page-up is more convoluted for (int screen_row = tb_height(); screen_row > 0 && Top_of_screen > 0; --screen_row) { --Top_of_screen; if (Top_of_screen <= 0) break; while (Top_of_screen > 0 && !contains_key(Visible, Top_of_screen)) --Top_of_screen; } if (Top_of_screen >= 0) refresh_screen_rows(); } if (key == 'G') { // go to bottom of screen; largely like page-up, interestingly Top_of_screen = SIZE(Trace_stream->past_lines)-1; for (int screen_row = tb_height(); screen_row > 0 && Top_of_screen > 0; --screen_row) { --Top_of_screen; if (Top_of_screen <= 0) break; while (Top_of_screen > 0 && !contains_key(Visible, Top_of_screen)) --Top_of_screen; } refresh_screen_rows(); // move cursor to bottom Display_row = Last_printed_row; refresh_screen_rows(); } if (key == TB_KEY_CARRIAGE_RETURN) { // expand lines under current by one level assert(contains_key(Trace_index, Display_row)); long long int start_index = get(Trace_index, Display_row); long long int index = 0; // simultaneously compute end_index and min_depth int min_depth = 9999; for (index = start_index+1; index < SIZE(Trace_stream->past_lines); ++index) { if (contains_key(Visible, index)) break; trace_line& curr_line = Trace_stream->past_lines.at(index); assert(curr_line.depth > Trace_stream->past_lines.at(start_index).depth); if (curr_line.depth < min_depth) min_depth = curr_line.depth; } long long int end_index = index; // mark as visible all intervening indices at min_depth for (index = start_index; index < end_index; ++index) { trace_line& curr_line = Trace_stream->past_lines.at(index); if (curr_line.depth == min_depth) { Visible.insert(index); } } refresh_screen_rows(); } if (key == TB_KEY_BACKSPACE || key == TB_KEY_BACKSPACE2) { // collapse all lines under current assert(contains_key(Trace_index, Display_row)); long long int start_index = get(Trace_index, Display_row); long long int index = 0; // end_index is the next line at a depth same as or lower than start_index int initial_depth = Trace_stream->past_lines.at(start_index).depth; for (index = start_index+1; index < SIZE(Trace_stream->past_lines); ++index) { if (!contains_key(Visible, index)) continue; trace_line& curr_line = Trace_stream->past_lines.at(index); if (curr_line.depth <= initial_depth) break; } long long int end_index = index; // mark as visible all intervening indices at min_depth for (index = start_index+1; index < end_index; ++index) { Visible.erase(index); } refresh_screen_rows(); } } tb_shutdown(); } // update Trace_indices for each screen_row on the basis of Top_of_screen and Visible void refresh_screen_rows() { long long int screen_row = 0, index = 0; Trace_index.clear(); for (screen_row = 0, index = Top_of_screen; screen_row < tb_height() && index < SIZE(Trace_stream->past_lines); ++screen_row, ++index) { // skip lines without depth for now while (!contains_key(Visible, index)) { ++index; if (index >= SIZE(Trace_stream->past_lines)) goto done; } assert(index < SIZE(Trace_stream->past_lines)); put(Trace_index, screen_row, index); } done:; } void render() { long long int screen_row = 0; for (screen_row = 0; screen_row < tb_height(); ++screen_row) { if (!contains_key(Trace_index, screen_row)) break; trace_line& curr_line = Trace_stream->past_lines.at(get(Trace_index, screen_row)); ostringstream out; out << std::setw(4) << curr_line.depth << ' ' << curr_line.label << ": " << curr_line.contents; if (screen_row < tb_height()-1) { long long int delta = lines_hidden(screen_row); // home-brew escape sequence for red if (delta > 999) out << "{"; out << " (" << delta << ")"; if (delta > 999) out << "}"; } render_line(screen_row, out.str()); } // clear rest of screen Last_printed_row = screen_row-1; for (; screen_row < tb_height(); ++screen_row) { render_line(screen_row, "~"); } // move cursor back to display row at the end tb_set_cursor(0, Display_row); tb_present(); } long long int lines_hidden(long long int screen_row) { assert(contains_key(Trace_index, screen_row)); if (!contains_key(Trace_index, screen_row+1)) return SIZE(Trace_stream->past_lines) - get(Trace_index, screen_row); else return get(Trace_index, screen_row+1) - get(Trace_index, screen_row); } void render_line(int screen_row, const string& s) { long long int col = 0; int color = TB_WHITE; for (col = 0; col < tb_width() && col < SIZE(s); ++col) { char c = s.at(col); // todo: unicode if (c == '\n') c = ';'; // replace newlines with semi-colons // escapes. hack: can't start a line with them. if (c == '{') { color = /*red*/1; c = ' '; } if (c == '}') { color = TB_WHITE; c = ' '; } tb_change_cell(col, screen_row, c, color, TB_BLACK); } for (; col < tb_width(); ++col) { tb_change_cell(col, screen_row, ' ', TB_WHITE, TB_BLACK); } } void load_trace(const char* filename) { ifstream tin(filename); if (!tin) { cerr << "no such file: " << filename << '\n'; exit(1); } Trace_stream = new trace_stream; while (has_data(tin)) { tin >> std::noskipws; skip_whitespace_but_not_newline(tin); if (!isdigit(tin.peek())) { string dummy; getline(tin, dummy); continue; } tin >> std::skipws; int depth; tin >> depth; string label; tin >> label; if (*--label.end() == ':') label.erase(--label.end()); string line; getline(tin, line); Trace_stream->past_lines.push_back(trace_line(depth, label, line)); } cerr << "lines read: " << Trace_stream->past_lines.size() << '\n'; } void test_run_interactive_code() { Trace_file = "run_interactive_code"; run("recipe main [\n 1:number/raw <- copy 0\n 2:address:shared:array:character <- new [1:number/raw <- copy 34]\n run-interactive 2:address:shared:array:character\n 3:number/raw <- copy 1:number/raw\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 3"); } void test_run_interactive_empty() { Trace_file = "run_interactive_empty"; run("recipe main [\n 1:address:shared:array:character <- copy 0/unsafe\n 2:address:shared:array:character <- run-interactive 1:address:shared:array:character\n]\n# result is null\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 2"); } // reads a string, tries to call it as code (treating it as a test), saving // all warnings. // returns true if successfully called (no errors found during load and transform) bool run_interactive(long long int address) { assert(contains_key(Recipe_ordinal, "interactive") && get(Recipe_ordinal, "interactive") != 0); // try to sandbox the run as best you can // todo: test this if (!Current_scenario) { for (long long int i = 1; i < Reserved_for_tests; ++i) Memory.erase(i); } string command = trim(strip_comments(read_mu_string(address))); if (command.empty()) return false; Name[get(Recipe_ordinal, "interactive")].clear(); run_code_begin(/*snapshot_recently_added_recipes*/true); // don't kill the current routine on parse errors routine* save_current_routine = Current_routine; Current_routine = NULL; // call run(string) but without the scheduling load(string("recipe! interactive [\n") + "local-scope\n" + "screen:address:shared:screen <- next-ingredient\n" + "$start-tracking-products\n" + command + "\n" + "$stop-tracking-products\n" + "reply screen\n" + "]\n"); transform_all(); Current_routine = save_current_routine; if (trace_count("error") > 0) return false; // now call 'sandbox' which will run 'interactive' in a separate routine, // and wait for it if (Save_trace_stream) { ++Save_trace_stream->callstack_depth; trace(9999, "trace") << "run-interactive: incrementing callstack depth to " << Save_trace_stream->callstack_depth << end(); assert(Save_trace_stream->callstack_depth < 9000); // 9998-101 plus cushion } Current_routine->calls.push_front(call(get(Recipe_ordinal, "sandbox"))); return true; } void run_code_begin(bool snapshot_recently_added_recipes) { //? cerr << "loading new trace\n"; // stuff to undo later, in run_code_end() Hide_warnings = true; Hide_errors = true; Disable_redefine_warnings = true; if (snapshot_recently_added_recipes) { Save_recently_added_recipes = Recently_added_recipes; Recently_added_recipes.clear(); Save_recently_added_shape_shifting_recipes = Recently_added_shape_shifting_recipes; Recently_added_shape_shifting_recipes.clear(); } Save_trace_stream = Trace_stream; Save_trace_file = Trace_file; Trace_file = ""; Trace_stream = new trace_stream; Trace_stream->collect_depth = App_depth; } void run_code_end() { //? cerr << "back to old trace\n"; Hide_warnings = false; Hide_errors = false; Disable_redefine_warnings = false; delete Trace_stream; Trace_stream = Save_trace_stream; Save_trace_stream = NULL; Trace_file = Save_trace_file; Save_trace_file.clear(); Recipe.erase(get(Recipe_ordinal, "interactive")); // keep past sandboxes from inserting errors if (!Save_recently_added_recipes.empty()) { clear_recently_added_recipes(); Recently_added_recipes = Save_recently_added_recipes; Save_recently_added_recipes.clear(); Recently_added_shape_shifting_recipes = Save_recently_added_shape_shifting_recipes; Save_recently_added_shape_shifting_recipes.clear(); } } void test_run_interactive_comments() { Trace_file = "run_interactive_comments"; run("recipe main [\n 1:address:shared:array:character <- new [# ab\nadd 2, 2]\n 2:address:shared:array:character <- run-interactive 1:address:shared:array:character\n 3:array:character <- copy *2:address:shared:array:character\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 52 in location 4"); } void test_run_interactive_converts_result_to_text() { Trace_file = "run_interactive_converts_result_to_text"; run("recipe main [\n # try to interactively add 2 and 2\n 1:address:shared:array:character <- new [add 2, 2]\n 2:address:shared:array:character <- run-interactive 1:address:shared:array:character\n 10:array:character <- copy 2:address:shared:array:character/lookup\n]\n# first letter in the output should be '4' in unicode\n"); CHECK_TRACE_CONTENTS("mem: storing 52 in location 11"); } void test_run_interactive_returns_text() { Trace_file = "run_interactive_returns_text"; run("recipe main [\n # try to interactively add 2 and 2\n 1:address:shared:array:character <- new [\n x:address:shared:array:character <- new [a]\n y:address:shared:array:character <- new [b]\n z:address:shared:array:character <- append x:address:shared:array:character, y:address:shared:array:character\n ]\n 2:address:shared:array:character <- run-interactive 1:address:shared:array:character\n 10:array:character <- copy 2:address:shared:array:character/lookup\n]\n# output contains \"ab\"\n"); CHECK_TRACE_CONTENTS("mem: storing 97 in location 11mem: storing 98 in location 12"); } void test_run_interactive_returns_errors() { Trace_file = "run_interactive_returns_errors"; run("recipe main [\n # run a command that generates an error\n 1:address:shared:array:character <- new [x:number <- copy 34\nget x:number, foo:offset]\n 2:address:shared:array:character, 3:address:shared:array:character <- run-interactive 1:address:shared:array:character\n 10:array:character <- copy 3:address:shared:array:character/lookup\n]\n# error should be \"unknown element foo in container number\"\n"); CHECK_TRACE_CONTENTS("mem: storing 117 in location 11mem: storing 110 in location 12mem: storing 107 in location 13mem: storing 110 in location 14"); } void test_run_interactive_with_comment() { Trace_file = "run_interactive_with_comment"; run("recipe main [\n # 2 instructions, with a comment after the first\n 1:address:shared:array:number <- new [a:number <- copy 0 # abc\nb:number <- copy 0\n]\n 2:address:shared:array:character, 3:address:shared:array:character <- run-interactive 1:address:shared:array:character\n]\n# no errors\n"); CHECK_TRACE_CONTENTS("mem: storing 0 in location 3"); } void test_run_interactive_cleans_up_any_created_specializations() { // define a generic recipe assert(!contains_key(Recipe_ordinal, "foo")); load("recipe foo x:_elem -> n:number [\n" " reply 34\n" "]\n"); assert(SIZE(Recently_added_recipes) == 1); // foo assert(variant_count("foo") == 1); // run-interactive a call that specializes this recipe run("recipe main [\n" " 1:number/raw <- copy 0\n" " 2:address:shared:array:character <- new [foo 1:number/raw]\n" " run-interactive 2:address:shared:array:character\n" "]\n"); assert(SIZE(Recently_added_recipes) == 2); // foo, main // check that number of variants doesn't change CHECK_EQ(variant_count("foo"), 1); } long long int variant_count(string recipe_name) { if (!contains_key(Recipe_variants, recipe_name)) return 0; return non_ghost_size(get(Recipe_variants, recipe_name)); } void track_most_recent_products(const instruction& instruction, const vector >& products) { ostringstream out; for (long long int i = 0; i < SIZE(products); ++i) { // string if (i < SIZE(instruction.products)) { if (is_mu_string(instruction.products.at(i))) { if (!scalar(products.at(i))) { tb_shutdown(); cerr << read_mu_string(trace_error_warning_contents()) << '\n'; cerr << SIZE(products.at(i)) << ": "; for (long long int j = 0; j < SIZE(products.at(i)); ++j) cerr << no_scientific(products.at(i).at(j)) << ' '; cerr << '\n'; } assert(scalar(products.at(i))); out << read_mu_string(products.at(i).at(0)) << '\n'; continue; } // End Record Product Special-cases } for (long long int j = 0; j < SIZE(products.at(i)); ++j) out << no_scientific(products.at(i).at(j)) << ' '; out << '\n'; } Most_recent_products = out.str(); } string strip_comments(string in) { ostringstream result; for (long long int i = 0; i < SIZE(in); ++i) { if (in.at(i) != '#') { result << in.at(i); } else { while (i+1 < SIZE(in) && in.at(i+1) != '\n') ++i; } } return result.str(); } long long int stringified_value_of_location(long long int address) { // convert to string ostringstream out; out << no_scientific(get_or_insert(Memory, address)); return new_mu_string(out.str()); } long long int trace_error_warning_contents() { if (!Trace_stream) return 0; ostringstream out; for (vector::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) { if (p->depth > Warning_depth) continue; out << p->contents; if (*--p->contents.end() != '\n') out << '\n'; } string result = out.str(); if (result.empty()) return 0; truncate(result); return new_mu_string(result); } long long int trace_app_contents() { if (!Trace_stream) return 0; ostringstream out; for (vector::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) { if (p->depth != App_depth) continue; out << p->contents; if (*--p->contents.end() != '\n') out << '\n'; } string result = out.str(); if (result.empty()) return 0; truncate(result); return new_mu_string(result); } void truncate(string& x) { if (SIZE(x) > 512) { x.erase(512); *x.rbegin() = '\n'; *++x.rbegin() = '.'; *++++x.rbegin() = '.'; } } void test_reload_continues_past_error() { Trace_file = "reload_continues_past_error"; run("recipe main [\n local-scope\n x:address:shared:array:character <- new [recipe foo [\n get 1234:number, foo:offset\n]]\n reload x\n 1:number/raw <- copy 34\n]\n"); CHECK_TRACE_CONTENTS("mem: storing 34 in location 1"); } void test_reload_cleans_up_any_created_specializations() { // define a generic recipe and a call to it assert(!contains_key(Recipe_ordinal, "foo")); assert(variant_count("foo") == 0); // a call that specializes this recipe run("recipe main [\n" " local-scope\n" " x:address:shared:array:character <- new [recipe foo x:_elem -> n:number [\n" "local-scope\n" "load-ingredients\n" "reply 34\n" "]\n" "recipe main2 [\n" "local-scope\n" "load-ingredients\n" "x:number <- copy 34\n" "foo x:number\n" "]]\n" " reload x\n" "]\n"); // check that number of variants includes specialization assert(SIZE(Recently_added_recipes) == 4); // foo, main, main2, foo specialization CHECK_EQ(variant_count("foo"), 2); } string slurp(const string& filename) { ostringstream result; ifstream fin(filename.c_str()); fin.peek(); if (!fin) return result.str(); // don't bother checking errno const int N = 1024; char buf[N]; while (has_data(fin)) { bzero(buf, N); fin.read(buf, N-1); // leave at least one null result << buf; } fin.close(); return result.str(); } bool exists(const string& filename) { struct stat dummy; return 0 == stat(filename.c_str(), &dummy); } //? :(before "End Transform All") //? check_type_pointers(); //? //? :(code) //? void check_type_pointers() { //? for (map::iterator p = Recipe.begin(); p != Recipe.end(); ++p) { //? if (any_type_ingredient_in_header(p->first)) continue; //? const recipe& r = p->second; //? for (long long int i = 0; i < SIZE(r.steps); ++i) { //? const instruction& inst = r.steps.at(i); //? for (long long int j = 0; j < SIZE(inst.ingredients); ++j) { //? if (!inst.ingredients.at(j).type) { //? raise_error << maybe(r.name) << " '" << inst.to_string() << "' -- " << inst.ingredients.at(j).to_string() << " has no type\n" << end(); //? return; //? } //? if (!inst.ingredients.at(j).properties.at(0).second) { //? raise_error << maybe(r.name) << " '" << inst.to_string() << "' -- " << inst.ingredients.at(j).to_string() << " has no type name\n" << end(); //? return; //? } //? } //? for (long long int j = 0; j < SIZE(inst.products); ++j) { //? if (!inst.products.at(j).type) { //? raise_error << maybe(r.name) << " '" << inst.to_string() << "' -- " << inst.products.at(j).to_string() << " has no type\n" << end(); //? return; //? } //? if (!inst.products.at(j).properties.at(0).second) { //? raise_error << maybe(r.name) << " '" << inst.to_string() << "' -- " << inst.products.at(j).to_string() << " has no type name\n" << end(); //? return; //? } //? } //? } //? } //? }