about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2016-02-21 20:30:02 -0800
committerKartik K. Agaram <vc@akkartik.com>2016-02-21 20:40:06 -0800
commitc4e143d6ea0635cdb164cec1c62afd7461e605ad (patch)
tree06fbb672ce95f1d5152c113cdb40685148b57d0c
parentf22250a174d5ad5abf8bf99ad140ced52563aee2 (diff)
downloadmu-c4e143d6ea0635cdb164cec1c62afd7461e605ad.tar.gz
2681 - drop reagent types from reagent properties
All my attempts at staging this change failed with this humongous commit
that took all day and involved debugging three monstrous bugs. Two of
the bugs had to do with forgetting to check the type name in the
implementation of shape-shifting recipes. Bug #2 in particular would
cause core tests in layer 59 to fail -- only when I loaded up edit/! It
got me to just hack directly on mu.cc until I figured out the cause
(snapshot saved in mu.cc.modified). The problem turned out to be that I
accidentally saved a type ingredient in the Type table during
specialization. Now I know that that can be very bad.

I've checked the traces for any stray type numbers (rather than names).

I also found what might be a bug from last November (labeled TODO), but
we'll verify after this commit.
-rw-r--r--010vm.cc124
-rw-r--r--011load.cc77
-rw-r--r--014literal_string.cc23
-rw-r--r--015literal_noninteger.cc5
-rw-r--r--021check_instruction.cc6
-rw-r--r--030container.cc101
-rw-r--r--031address.cc10
-rw-r--r--032array.cc22
-rw-r--r--033exclusive_container.cc7
-rw-r--r--037new.cc14
-rw-r--r--040brace.cc1
-rw-r--r--044space_surround.cc2
-rw-r--r--046global.cc2
-rw-r--r--047check_type_by_name.cc28
-rw-r--r--050scenario.cc7
-rw-r--r--054dilated_reagent.cc27
-rw-r--r--055parse_tree.cc15
-rw-r--r--056recipe_header.cc16
-rw-r--r--057static_dispatch.cc39
-rw-r--r--058shape_shifting_container.cc53
-rw-r--r--059shape_shifting_recipe.cc165
-rw-r--r--061recipe.cc1
-rw-r--r--086scenario_console_test.mu5
-rw-r--r--mu.cc.modified12001
24 files changed, 12376 insertions, 375 deletions
diff --git a/010vm.cc b/010vm.cc
index 3a916e58..f698cc6c 100644
--- a/010vm.cc
+++ b/010vm.cc
@@ -57,7 +57,7 @@ struct reagent {
   double value;
   bool initialized;
   reagent(string s);
-  reagent();
+  reagent() :type(NULL), value(0), initialized(false) {}
   ~reagent();
   void clear();
   reagent(const reagent& old);
@@ -232,7 +232,19 @@ reagent::reagent(string s) :original_string(s), type(NULL), value(0), initialize
   // Parsing reagent(string s)
   istringstream in(s);
   in >> std::noskipws;
-  // properties
+  // 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;
@@ -240,19 +252,6 @@ reagent::reagent(string s) :original_string(s), type(NULL), value(0), initialize
     string_tree* value = parse_property_list(row);
     properties.push_back(pair<string, string_tree*>(key, value));
   }
-  // structures for the first row of properties: name and list of types
-  name = properties.at(0).first;
-  type = new_type_tree(properties.at(0).second);
-  if (is_integer(name) && type == NULL) {
-    assert(!properties.at(0).second);
-    properties.at(0).second = new string_tree("literal");
-    type = new type_tree("literal", get(Type_ordinal, "literal"));
-  }
-  if (name == "_" && type == NULL) {
-    assert(!properties.at(0).second);
-    properties.at(0).second = new string_tree("dummy");
-    type = new type_tree("literal", get(Type_ordinal, "literal"));
-  }
   // End Parsing reagent
 }
 
@@ -264,7 +263,6 @@ string_tree* parse_property_list(istream& 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);
@@ -345,13 +343,6 @@ string_tree::~string_tree() {
   delete right;
 }
 
-reagent::reagent() :type(NULL), value(0), initialized(false) {
-  // The first property is special, so ensure we always have it.
-  // Other properties can be pushed back, but the first must always be
-  // assigned to.
-  properties.push_back(pair<string, string_tree*>("", NULL));
-}
-
 string slurp_until(istream& in, char delim) {
   ostringstream out;
   char c;
@@ -366,14 +357,14 @@ string slurp_until(istream& in, char delim) {
 }
 
 bool has_property(reagent x, string name) {
-  for (long long int i = /*skip name:type*/1; i < SIZE(x.properties); ++i) {
+  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 = /*skip name:type*/1; p != SIZE(r.properties); ++p) {
+  for (long long int p = 0; p != SIZE(r.properties); ++p) {
     if (r.properties.at(p).first == name)
       return r.properties.at(p).second;
   }
@@ -450,8 +441,9 @@ string to_string(const instruction& inst) {
 
 string to_string(const reagent& r) {
   ostringstream out;
+  out << r.name << ": " << names_to_string(r.type);
   if (!r.properties.empty()) {
-    out << "{";
+    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);
@@ -498,25 +490,24 @@ 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;
-  if (!type->left && !type->right)
-    dump(type->value, out);
-  else
-    dump(type, out);
+  dump(type, out);
   return out.str();
 }
 
-void dump(const type_tree* type, ostream& out) {
-  out << "<";
-  if (type->left)
-    dump(type->left, out);
-  else
-    dump(type->value, out);
-  out << " : ";
-  if (type->right)
-    dump(type->right, out);
-  else
-    out << "<>";
-  out << ">";
+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) {
@@ -526,6 +517,54 @@ void dump(type_ordinal type, ostream& out) {
     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 << ')';
+}
+
 //: helper to print numbers without excessive precision
 
 :(before "End Types")
@@ -555,7 +594,6 @@ string trim_floating_point(const string& in) {
     --len;
   }
   if (in.at(len-1) == '.') --len;
-//?   cerr << in << ": " << in.substr(0, len) << '\n';
   return in.substr(0, len);
 }
 
diff --git a/011load.cc b/011load.cc
index 532eb417..f3fb8793 100644
--- a/011load.cc
+++ b/011load.cc
@@ -6,8 +6,8 @@ recipe main [
   1:number <- copy 23
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   product: {"1": "number"}
++parse:   ingredient: 23: "literal"
++parse:   product: 1: "number"
 
 :(code)
 vector<recipe_ordinal> load(string form) {
@@ -49,9 +49,8 @@ long long int slurp_recipe(istream& in) {
   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)) {
+  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))
@@ -103,9 +102,8 @@ bool next_instruction(istream& in, instruction* curr) {
     skip_whitespace_but_not_newline(in);
   }
   skip_whitespace_and_comments(in);
-  if (SIZE(words) == 1 && words.at(0) == "]") {
+  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;
@@ -120,9 +118,8 @@ bool next_instruction(istream& in, instruction* curr) {
 
   vector<string>::iterator p = words.begin();
   if (find(words.begin(), words.end(), "<-") != words.end()) {
-    for (; *p != "<-"; ++p) {
+    for (; *p != "<-"; ++p)
       curr->products.push_back(reagent(*p));
-    }
     ++p;  // skip <-
   }
 
@@ -133,18 +130,15 @@ bool next_instruction(istream& in, instruction* curr) {
   curr->old_name = curr->name = *p;  p++;
   // curr->operation will be set in a later layer
 
-  for (; p != words.end(); ++p) {
+  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<reagent>::iterator p = curr->ingredients.begin(); p != curr->ingredients.end(); ++p) {
+  for (vector<reagent>::iterator p = curr->ingredients.begin(); p != curr->ingredients.end(); ++p)
     trace(9993, "parse") << "  ingredient: " << to_string(*p) << end();
-  }
-  for (vector<reagent>::iterator p = curr->products.begin(); p != curr->products.end(); ++p) {
+  for (vector<reagent>::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;
@@ -228,9 +222,8 @@ bool warn_on_redefine(const string& recipe_name) {
 void show_rest_of_stream(istream& in) {
   cerr << '^';
   char c;
-  while (in >> c) {
+  while (in >> c)
     cerr << c;
-  }
   cerr << "$\n";
   exit(0);
 }
@@ -261,8 +254,8 @@ recipe main [
   1:number <- copy 23
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   product: {"1": "number"}
++parse:   ingredient: 23: "literal"
++parse:   product: 1: "number"
 
 :(scenario parse_comment_amongst_instruction)
 recipe main [
@@ -270,8 +263,8 @@ recipe main [
   1:number <- copy 23
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   product: {"1": "number"}
++parse:   ingredient: 23: "literal"
++parse:   product: 1: "number"
 
 :(scenario parse_comment_amongst_instruction_2)
 recipe main [
@@ -280,8 +273,8 @@ recipe main [
   # comment
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   product: {"1": "number"}
++parse:   ingredient: 23: "literal"
++parse:   product: 1: "number"
 
 :(scenario parse_comment_amongst_instruction_3)
 recipe main [
@@ -290,19 +283,19 @@ recipe main [
   2:number <- copy 23
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   product: {"1": "number"}
++parse:   ingredient: 23: "literal"
++parse:   product: 1: "number"
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   product: {"2": "number"}
++parse:   ingredient: 23: "literal"
++parse:   product: 2: "number"
 
 :(scenario parse_comment_after_instruction)
 recipe main [
   1:number <- copy 23  # comment
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   product: {"1": "number"}
++parse:   ingredient: 23: "literal"
++parse:   product: 1: "number"
 
 :(scenario parse_label)
 recipe main [
@@ -321,43 +314,43 @@ recipe main [
   1:number <- copy 23/foo:bar:baz
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal", "foo": ("bar" "baz")}
-+parse:   product: {"1": "number"}
++parse:   ingredient: 23: "literal", {"foo": ("bar" "baz")}
++parse:   product: 1: "number"
 
 :(scenario parse_multiple_products)
 recipe main [
   1:number, 2:number <- copy 23
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   product: {"1": "number"}
-+parse:   product: {"2": "number"}
++parse:   ingredient: 23: "literal"
++parse:   product: 1: "number"
++parse:   product: 2: "number"
 
 :(scenario parse_multiple_ingredients)
 recipe main [
   1:number, 2:number <- copy 23, 4:number
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   ingredient: {"4": "number"}
-+parse:   product: {"1": "number"}
-+parse:   product: {"2": "number"}
++parse:   ingredient: 23: "literal"
++parse:   ingredient: 4: "number"
++parse:   product: 1: "number"
++parse:   product: 2: "number"
 
 :(scenario parse_multiple_types)
 recipe main [
   1:number, 2:address:number <- copy 23, 4:number
 ]
 +parse: instruction: copy
-+parse:   ingredient: {"23": "literal"}
-+parse:   ingredient: {"4": "number"}
-+parse:   product: {"1": "number"}
-+parse:   product: {"2": ("address" "number")}
++parse:   ingredient: 23: "literal"
++parse:   ingredient: 4: "number"
++parse:   product: 1: "number"
++parse:   product: 2: ("address" "number")
 
 :(scenario parse_properties)
 recipe main [
   1:address:number/lookup <- copy 23
 ]
-+parse:   product: {"1": ("address" "number"), "lookup": ()}
++parse:   product: 1: ("address" "number"), {"lookup": ()}
 
 //: this test we can't represent with a scenario
 :(code)
diff --git a/014literal_string.cc b/014literal_string.cc
index 7975d63e..4250772b 100644
--- a/014literal_string.cc
+++ b/014literal_string.cc
@@ -10,13 +10,13 @@
 recipe main [
   1:address:array:character <- copy [abc def]  # copy can't really take a string
 ]
-+parse:   ingredient: {"abc def": "literal-string"}
++parse:   ingredient: "abc def": "literal-string"
 
 :(scenario string_literal_with_colons)
 recipe main [
   1:address:array:character <- copy [abc:def/ghi]
 ]
-+parse:   ingredient: {"abc:def/ghi": "literal-string"}
++parse:   ingredient: "abc:def/ghi": "literal-string"
 
 :(before "End Mu Types Initialization")
 put(Type_ordinal, "literal-string", 0);
@@ -111,7 +111,6 @@ if (s.at(0) == '[') {
   strip_last(s);
   name = s;
   type = new type_tree("literal-string", 0);
-  properties.push_back(pair<string, string_tree*>(name, new string_tree("literal-string")));
   return;
 }
 
@@ -131,7 +130,7 @@ string emit_literal_string(string name) {
   size_t pos = 0;
   while (pos != string::npos)
     pos = replace(name, "\n", "\\n", pos);
-  return "{\""+name+"\": \"literal-string\"}";
+  return '"'+name+"\": \"literal-string\"";
 }
 
 size_t replace(string& str, const string& from, const string& to, size_t n) {
@@ -149,20 +148,20 @@ void strip_last(string& s) {
 recipe main [
   1:address:array:character <- copy [abc [def]]
 ]
-+parse:   ingredient: {"abc [def]": "literal-string"}
++parse:   ingredient: "abc [def]": "literal-string"
 
 :(scenario string_literal_escaped)
 recipe main [
   1:address:array:character <- copy [abc \[def]
 ]
-+parse:   ingredient: {"abc [def": "literal-string"}
++parse:   ingredient: "abc [def": "literal-string"
 
 :(scenario string_literal_escaped_comment_aware)
 recipe main [
   1:address:array:character <- copy [
 abc \\\[def]
 ]
-+parse:   ingredient: {"\nabc \[def": "literal-string"}
++parse:   ingredient: "\nabc \[def": "literal-string"
 
 :(scenario string_literal_and_comment)
 recipe main [
@@ -171,15 +170,15 @@ recipe main [
 +parse: --- defining main
 +parse: instruction: copy
 +parse:   number of ingredients: 1
-+parse:   ingredient: {"abc": "literal-string"}
-+parse:   product: {"1": ("address" "array" "character")}
++parse:   ingredient: "abc": "literal-string"
++parse:   product: 1: ("address" "array" "character")
 
 :(scenario string_literal_escapes_newlines_in_trace)
 recipe main [
   copy [abc
 def]
 ]
-+parse:   ingredient: {"abc\ndef": "literal-string"}
++parse:   ingredient: "abc\ndef": "literal-string"
 
 :(scenario string_literal_can_skip_past_comments)
 recipe main [
@@ -188,10 +187,10 @@ recipe main [
     bar
   ]
 ]
-+parse:   ingredient: {"\n    # ']' inside comment\n    bar\n  ": "literal-string"}
++parse:   ingredient: "\n    # ']' inside comment\n    bar\n  ": "literal-string"
 
 :(scenario string_literal_empty)
 recipe main [
   copy []
 ]
-+parse:   ingredient: {"": "literal-string"}
++parse:   ingredient: "": "literal-string"
diff --git a/015literal_noninteger.cc b/015literal_noninteger.cc
index 4cea4104..fa30290d 100644
--- a/015literal_noninteger.cc
+++ b/015literal_noninteger.cc
@@ -5,13 +5,12 @@
 recipe main [
   1:number <- copy 3.14159
 ]
-+parse:   ingredient: {"3.14159": "literal-number"}
++parse:   ingredient: 3.14159: "literal-fractional-number"
 
 :(after "Parsing reagent(string s)")
 if (is_noninteger(s)) {
   name = s;
-  type = new type_tree("literal-number", 0);
-  properties.push_back(pair<string, string_tree*>(name, new string_tree("literal-number")));
+  type = new type_tree("literal-fractional-number", 0);
   set_value(to_double(s));
   return;
 }
diff --git a/021check_instruction.cc b/021check_instruction.cc
index f4016b7e..67dd800b 100644
--- a/021check_instruction.cc
+++ b/021check_instruction.cc
@@ -180,8 +180,8 @@ bool is_mu_boolean(reagent r) {
 bool is_mu_number(reagent r) {
   if (!r.type) return false;
   if (is_literal(r)) {
-    if (!r.properties.at(0).second) return false;
-    return r.type->name == "literal-number"
+    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
@@ -191,7 +191,7 @@ bool is_mu_number(reagent r) {
 bool is_mu_scalar(reagent r) {
   if (!r.type) return false;
   if (is_literal(r))
-    return !r.properties.at(0).second || r.type->name != "literal-string";
+    return !r.type || r.type->name != "literal-string";
   if (is_mu_array(r)) return false;
   return size_of(r) == 1;
 }
diff --git a/030container.cc b/030container.cc
index 5ad2268f..183c360c 100644
--- a/030container.cc
+++ b/030container.cc
@@ -162,7 +162,7 @@ case GET: {
   // Update GET product in Check
   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 " << to_string(element.type) << " but " << product.name << " has type " << to_string(product.type) << '\n' << end();
+    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;
@@ -187,7 +187,7 @@ case GET: {
   trace(9998, "run") << "address to copy is " << src << end();
   reagent tmp = element_type(base, offset);
   tmp.set_value(src);
-  trace(9998, "run") << "its type is " << to_string(tmp.type) << end();
+  trace(9998, "run") << "its type is " << names_to_string(tmp.type) << end();
   products.push_back(read_memory(tmp));
   break;
 }
@@ -241,7 +241,7 @@ recipe main [
   14:number <- copy 36
   15:address:number <- get 12:point-number/raw, 1:offset
 ]
-+error: main: 'get 12:point-number/raw, 1:offset' should write to number but 15 has type <address : <number : <>>>
++error: main: 'get 12:point-number/raw, 1:offset' should write to number but 15 has type (address number)
 
 //: we might want to call 'get' without saving the results, say in a sandbox
 
@@ -303,7 +303,7 @@ case GET_ADDRESS: {
   // ..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 " << to_string(element.type) << " but " << product.name << " has type " << to_string(product.type) << '\n' << end();
+    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;
@@ -362,7 +362,7 @@ recipe main [
   13:boolean <- copy 0
   15:boolean <- get-address 12:boolbool, 1:offset
 ]
-+error: main: 'get-address 12:boolbool, 1:offset' should write to <address : <boolean : <>>> but 15 has type boolean
++error: main: 'get-address 12:boolbool, 1:offset' should write to (address boolean) but 15 has type boolean
 
 //:: Allow containers to be defined in mu code.
 
@@ -373,8 +373,8 @@ container foo [
   y:number
 ]
 +parse: --- defining container foo
-+parse: element: {"x": "number"}
-+parse: element: {"y": "number"}
++parse: element: x: "number"
++parse: element: y: "number"
 
 :(scenario container_use_before_definition)
 container foo [
@@ -388,15 +388,15 @@ container bar [
 ]
 +parse: --- defining container foo
 +parse: type number: 1000
-+parse:   element: {"x": "number"}
++parse:   element: x: "number"
 # todo: brittle
 # type bar is unknown at this point, but we assign it a number
-+parse:   element: {"y": "bar"}
++parse:   element: y: "bar"
 # later type bar geon
 +parse: --- defining container bar
 +parse: type number: 1001
-+parse:   element: {"x": "number"}
-+parse:   element: {"y": "number"}
++parse:   element: x: "number"
++parse:   element: y: "number"
 
 :(before "End Command Handlers")
 else if (command == "container") {
@@ -424,35 +424,30 @@ void insert_container(const string& command, kind_of_type kind, istream& in) {
     string element = next_word(in);
     if (element == "]") break;
     info.elements.push_back(reagent(element));
-    // handle undefined types
-    delete info.elements.back().type;
-    info.elements.back().type = new_type_tree_with_new_types_for_unknown(info.elements.back().properties.at(0).second, info);
+    replace_unknown_types_with_unique_ordinals(info.elements.back().type, info);
     trace(9993, "parse") << "  element: " << to_string(info.elements.back()) << end();
     // End Load Container Element Definition
   }
   info.size = SIZE(info.elements);
 }
 
-type_tree* new_type_tree_with_new_types_for_unknown(const string_tree* properties, const type_info& info) {
-  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);
+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
-      result->value = 0;
+    else if (is_integer(type->name)) {  // sometimes types will contain non-type tags, like numbers for the size of an array
+      type->value = 0;
     }
     // End insert_container Special-cases
-    else if (properties->value != "->") {  // used in recipe types
-      put(Type_ordinal, type_name, Next_type_ordinal++);
-      result->value = get(Type_ordinal, type_name);
+    else if (type->name != "->") {  // used in recipe types
+      put(Type_ordinal, type->name, Next_type_ordinal++);
+      type->value = get(Type_ordinal, type->name);
     }
   }
-  result->left = new_type_tree_with_new_types_for_unknown(properties->left, info);
-  result->right = new_type_tree_with_new_types_for_unknown(properties->right, info);
-  return result;
+  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) {
@@ -548,33 +543,27 @@ void check_or_set_invalid_types(const recipe_ordinal 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, inst.ingredients.at(i).properties.at(0).second,
-                                 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, inst.products.at(i).properties.at(0).second,
-                                 maybe(caller.name), "'"+to_string(inst)+"'");
-    }
+    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)+"'");
   }
   // End check_or_set_invalid_types
 }
 
-void check_or_set_invalid_types(type_tree* type, const string_tree* type_name, const string& block, const string& name) {
-  // can't assert that type_name is non-null, even at the top of a recursive call tree
+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
   // End Container Type Checks
   if (type->value == 0) return;
   if (!contains_key(Type, type->value)) {
-    if (type_name && contains_key(Type_ordinal, type_name->value))
-      type->value = get(Type_ordinal, type_name->value);
-    else if (type_name)
-      raise_error << block << "unknown type " << type_name->value << " in " << name << '\n' << end();
+    assert(!type->name.empty());
+    if (contains_key(Type_ordinal, type->name))
+      type->value = get(Type_ordinal, type->name);
     else
-      raise_error << block << "missing type in " << name << '\n' << end();
+      raise_error << block << "unknown type " << type->name << " in " << name << '\n' << end();
   }
-  check_or_set_invalid_types(type->left, type_name ? type_name->left : NULL, block, name);
-  check_or_set_invalid_types(type->right, type_name ? type_name->right : NULL, block, name);
+  check_or_set_invalid_types(type->left, block, name);
+  check_or_set_invalid_types(type->right, block, name);
 }
 
 :(scenario container_unknown_field)
@@ -592,8 +581,8 @@ container foo [
   y:number
 ]
 +parse: --- defining container foo
-+parse: element: {"x": "number"}
-+parse: element: {"y": "number"}
++parse: element: x: "number"
++parse: element: y: "number"
 
 :(before "End Transform All")
 check_container_field_types();
@@ -608,23 +597,9 @@ void check_container_field_types() {
   }
 }
 
-void check_invalid_types(const recipe_ordinal r) {
-  for (long long int index = 0; index < SIZE(get(Recipe, r).steps); ++index) {
-    const instruction& inst = get(Recipe, r).steps.at(index);
-    for (long long int i = 0; i < SIZE(inst.ingredients); ++i) {
-      check_invalid_types(inst.ingredients.at(i).type,
-                          maybe(get(Recipe, r).name), "'"+to_string(inst)+"'");
-    }
-    for (long long int i = 0; i < SIZE(inst.products); ++i) {
-      check_invalid_types(inst.products.at(i).type,
-                          maybe(get(Recipe, r).name), "'"+to_string(inst)+"'");
-    }
-  }
-}
-
 void check_invalid_types(const type_tree* type, const string& block, const string& name) {
   if (!type) return;  // will throw a more precise error elsewhere
-  // End Container Type Checks
+  // End Container Type Checks2
   if (type->value == 0) {
     assert(!type->left && !type->right);
     return;
diff --git a/031address.cc b/031address.cc
index 384293b9..2628bc65 100644
--- a/031address.cc
+++ b/031address.cc
@@ -114,13 +114,6 @@ void drop_from_type(reagent& r, string expected_type) {
   r.type = tmp->right;
   tmp->right = NULL;
   delete tmp;
-  // property
-  if (r.properties.at(0).second) {
-    string_tree* tmp2 = r.properties.at(0).second;
-    r.properties.at(0).second = tmp2->right;
-    tmp2->right = NULL;
-    delete tmp2;
-  }
 }
 
 void drop_one_lookup(reagent& r) {
@@ -194,7 +187,7 @@ recipe main [
   2:number <- copy 34
   3:number <- copy *1:address:number
 ]
-+parse: ingredient: {"1": ("address" "number"), "lookup": ()}
++parse: ingredient: 1: ("address" "number"), {"lookup": ()}
 +mem: storing 34 in location 3
 
 :(before "End Parsing reagent")
@@ -205,7 +198,6 @@ recipe main [
   }
   if (name.empty())
     raise_error << "illegal name " << original_string << '\n' << end();
-  properties.at(0).first = name;
 }
 
 //:: helpers for debugging
diff --git a/032array.cc b/032array.cc
index c4676d96..8adefe30 100644
--- a/032array.cc
+++ b/032array.cc
@@ -34,12 +34,12 @@ case CREATE_ARRAY: {
     break;
   }
   // 'create-array' will need to check properties rather than types
-  if (!product.properties.at(0).second || !product.properties.at(0).second->right || !product.properties.at(0).second->right->right) {
+  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.properties.at(0).second->right->right->value)) {
-    raise_error << maybe(get(Recipe, r).name) << "'create-array' product should specify size of array after its element type, but got " << product.properties.at(0).second->right->right->value << '\n' << end();
+  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;
@@ -49,7 +49,7 @@ 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.properties.at(0).second->right->right->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
@@ -135,13 +135,13 @@ container foo [
 
 :(before "End Load Container Element Definition")
 {
-  const string_tree* type_name = info.elements.back().properties.at(0).second;
-  if (type_name->value == "array") {
-    if (!type_name->right) {
+  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_name->right->right) {  // array has no length
+    if (!type->right->right) {  // array has no length
       raise_error << "container '" << name << "' cannot determine size of element " << info.elements.back().name << '\n' << end();
       continue;
     }
@@ -193,7 +193,7 @@ case INDEX: {
   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 " << to_string(element.type) << '\n' << end();
+    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;
@@ -334,7 +334,7 @@ case INDEX_ADDRESS: {
   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 " << to_string(element.type) << '\n' << end();
+    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;
@@ -405,7 +405,7 @@ recipe main [
   8:address:array:point <- copy 1/unsafe
   9:address:number <- index-address *8:address:array:point, 0
 ]
-+error: main: 'index' on *8:address:array:point can't be saved in 9:address:number; type should be <address : <point : <>>>
++error: main: 'index' on *8:address:array:point can't be saved in 9:address:number; type should be (address point)
 
 //:: compute the length of an array
 
diff --git a/033exclusive_container.cc b/033exclusive_container.cc
index 26def074..7068ef18 100644
--- a/033exclusive_container.cc
+++ b/033exclusive_container.cc
@@ -153,8 +153,7 @@ recipe main [
   14:number <- copy 36
   20:address:number <- maybe-convert 12:number-or-point/unsafe, 1:variant
 ]
-+error: main: 'maybe-convert 12:number-or-point/unsafe, 1:variant' should write to <address : <point : <>>> but 20 has type <address : <number : <>>>
-
++error: main: 'maybe-convert 12:number-or-point/unsafe, 1:variant' should write to (address point) but 20 has type (address number)
 
 //:: Allow exclusive containers to be defined in mu code.
 
@@ -164,8 +163,8 @@ exclusive-container foo [
   y:number
 ]
 +parse: --- defining exclusive-container foo
-+parse: element: {"x": "number"}
-+parse: element: {"y": "number"}
++parse: element: x: "number"
++parse: element: y: "number"
 
 :(before "End Command Handlers")
 else if (command == "exclusive-container") {
diff --git a/037new.cc b/037new.cc
index 45d014c2..d88fc8fc 100644
--- a/037new.cc
+++ b/037new.cc
@@ -125,7 +125,6 @@ Transform.push_back(transform_new_to_allocate);  // idempotent
 :(code)
 void transform_new_to_allocate(const recipe_ordinal r) {
   trace(9991, "transform") << "--- convert 'new' to 'allocate' for recipe " << get(Recipe, r).name << end();
-//?   cerr << "--- convert 'new' to 'allocate' for recipe " << get(Recipe, r).name << '\n';
   for (long long int i = 0; i < SIZE(get(Recipe, r).steps); ++i) {
     instruction& inst = get(Recipe, r).steps.at(i);
     // Convert 'new' To 'allocate'
@@ -429,27 +428,21 @@ if (x.type->value == get(Type_ordinal, "address")
   // decrement refcount of old address
   if (old_address) {
     long long int old_refcount = get_or_insert(Memory, old_address);
-//?     cerr << old_refcount << '\n';
-//?     assert(old_refcount > 0);
     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") << "038new.cc:424: location " << x.value << " contains " << old_address << " with refcount " << get_or_insert(Memory, old_address) << end();
   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
+    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
-//?   if (get_or_insert(Memory, old_address) < 0) {
-//?     DUMP("");
-//?   }
   assert(get_or_insert(Memory, old_address) >= 0);
   if (old_address && get_or_insert(Memory, old_address) == 0) {
     // lookup_memory without drop_one_lookup {
@@ -459,7 +452,6 @@ if (x.type->value == get(Type_ordinal, "address")
     drop_from_type(x, "address");
     drop_from_type(x, "shared");
     // }
-//?     cerr << "ABANDON\n";
     abandon(old_address, size_of(x)+/*refcount*/1);
   }
   return;
@@ -653,7 +645,5 @@ string read_mu_string(long long int address) {
 }
 
 bool is_mu_type_literal(reagent r) {
-//?   if (!r.properties.empty())
-//?     dump_property(r.properties.at(0).second, cerr);
-  return is_literal(r) && !r.properties.empty() && r.properties.at(0).second && r.type->name == "type";
+  return is_literal(r) && r.type && r.type->name == "type";
 }
diff --git a/040brace.cc b/040brace.cc
index 873c604c..6458e8ef 100644
--- a/040brace.cc
+++ b/040brace.cc
@@ -115,7 +115,6 @@ void transform_braces(const recipe_ordinal r) {
     // if implicit, compute target
     reagent target;
     target.type = new type_tree("offset", get(Type_ordinal, "offset"));
-    target.properties.at(0).second = new string_tree("offset");
     target.set_value(0);
     if (open_braces.empty())
       raise_error << inst.old_name << " needs a '{' before\n" << end();
diff --git a/044space_surround.cc b/044space_surround.cc
index 684a69af..64047e5f 100644
--- a/044space_surround.cc
+++ b/044space_surround.cc
@@ -49,7 +49,7 @@ long long int space_base(const reagent& x, long long int space_index, long long
 }
 
 long long int space_index(const reagent& x) {
-  for (long long int i = /*skip name:type*/1; i < SIZE(x.properties); ++i) {
+  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();
diff --git a/046global.cc b/046global.cc
index ff91c88d..ca04741e 100644
--- a/046global.cc
+++ b/046global.cc
@@ -82,7 +82,7 @@ $error: 0
 
 :(code)
 bool is_global(const reagent& x) {
-  for (long long int i = /*skip name:type*/1; i < SIZE(x.properties); ++i) {
+  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";
   }
diff --git a/047check_type_by_name.cc b/047check_type_by_name.cc
index bd74e99d..5d63ca3d 100644
--- a/047check_type_by_name.cc
+++ b/047check_type_by_name.cc
@@ -20,51 +20,45 @@ Transform.push_back(check_or_set_types_by_name);  // idempotent
 :(code)
 void check_or_set_types_by_name(const recipe_ordinal r) {
   trace(9991, "transform") << "--- deduce types for recipe " << get(Recipe, r).name << end();
-//?   cerr << "--- deduce types for recipe " << get(Recipe, r).name << '\n';
   map<string, type_tree*> type;
-  map<string, string_tree*> type_name;
   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, type_name, inst.ingredients.at(in));
-      check_type(type, type_name, inst.ingredients.at(in), r);
+      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, type_name, inst.products.at(out));
-      check_type(type, type_name, inst.products.at(out), r);
+      deduce_missing_type(type, inst.products.at(out));
+      check_type(type, inst.products.at(out), r);
     }
   }
 }
 
-void deduce_missing_type(map<string, type_tree*>& type, map<string, string_tree*>& type_name, reagent& x) {
+void deduce_missing_type(map<string, type_tree*>& 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 << " <= " << to_string(x.type) << end();
-  assert(!x.properties.at(0).second);
-  x.properties.at(0).second = new string_tree(*get(type_name, x.name));
+  trace(9992, "transform") << x.name << " <= " << names_to_string(x.type) << end();
 }
 
-void check_type(map<string, type_tree*>& type, map<string, string_tree*>& type_name, const reagent& x, const recipe_ordinal r) {
+void check_type(map<string, type_tree*>& 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 << " => " << to_string(x.type) << end();
+    trace(9992, "transform") << x.name << " => " << names_to_string(x.type) << end();
     put(type, x.name, x.type);
   }
-  if (!contains_key(type_name, x.name))
-    put(type_name, x.name, x.properties.at(0).second);
   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_name, x.name)->value == "array") {
-    if (!get(type_name, x.name)->right) {
+  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_name, x.name)->right->right) {
+    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;
     }
diff --git a/050scenario.cc b/050scenario.cc
index 6606f09b..b02fe95e 100644
--- a/050scenario.cc
+++ b/050scenario.cc
@@ -319,10 +319,9 @@ void check_memory(const string& s) {
 
 void check_type(const string& lhs, istream& in) {
   reagent x(lhs);
-  const string_tree* type_name = x.properties.at(0).second;
-  if (type_name->value == "array"
-      && type_name->right && type_name->right->value == "character"
-      && !type_name->right->right) {
+  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);
diff --git a/054dilated_reagent.cc b/054dilated_reagent.cc
index cab72cfb..f7b772a3 100644
--- a/054dilated_reagent.cc
+++ b/054dilated_reagent.cc
@@ -7,7 +7,7 @@
 recipe main [
   {1: number, foo: bar} <- copy 34
 ]
-+parse:   product: {"1": "number", "foo": "bar"}
++parse:   product: 1: "number", {"foo": "bar"}
 
 :(scenario load_trailing_space_after_curly_bracket)
 recipe main [
@@ -22,7 +22,7 @@ recipe main [
 recipe main [
   {1: number, foo: bar} <- copy 34  # test comment
 ]
-+parse:   product: {"1": "number", "foo": "bar"}
++parse:   product: 1: "number", {"foo": "bar"}
 $error: 0
 
 :(scenario dilated_reagent_with_comment_immediately_following)
@@ -97,6 +97,21 @@ if (s.at(0) == '{') {
   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));
+    // 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;
@@ -105,14 +120,6 @@ if (s.at(0) == '{') {
     // End Parsing Reagent Property(value)
     properties.push_back(pair<string, string_tree*>(key, value));
   }
-  // structures for the first row of properties
-  name = properties.at(0).first;
-  string type_name = properties.at(0).second->value;
-  if (!contains_key(Type_ordinal, type_name)) {
-      // this type can't be an integer literal
-    put(Type_ordinal, type_name, Next_type_ordinal++);
-  }
-  type = new_type_tree(properties.at(0).second);
   return;
 }
 
diff --git a/055parse_tree.cc b/055parse_tree.cc
index 61cacc2f..5192e8a2 100644
--- a/055parse_tree.cc
+++ b/055parse_tree.cc
@@ -7,10 +7,12 @@
 recipe main [
   {1: number, foo: (bar (baz quux))} <- copy 34
 ]
-+parse:   product: {"1": "number", "foo": ("bar" ("baz" "quux"))}
++parse:   product: 1: "number", {"foo": ("bar" ("baz" "quux"))}
 
 :(before "End Parsing Reagent Property(value)")
 value = parse_string_tree(value);
+:(before "End Parsing Reagent Type Property(value)")
+value = parse_string_tree(value);
 
 :(code)
 string_tree* parse_string_tree(string_tree* s) {
@@ -65,7 +67,7 @@ container foo [
 ]
 container bar [
 ]
-+parse:   product: {"1": ("foo" ("address" "array" "character") ("bar" "number"))}
++parse:   product: 1: ("foo" ("address" "array" "character") ("bar" "number"))
 
 //: an exception is 'new', which takes a type tree as its ingredient *value*
 
@@ -76,8 +78,11 @@ recipe main [
 +new: size of ("address" "number") is 1
 
 :(before "End Post-processing(expected_product) When Checking 'new'")
-expected_product.properties.at(0).second = parse_string_tree(expected_product.properties.at(0).second);
-delete expected_product.type;
-expected_product.type = new_type_tree(expected_product.properties.at(0).second);
+{
+  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;
+}
 :(before "End Post-processing(type_name) When Converting 'new'")
 type_name = parse_string_tree(type_name);
diff --git a/056recipe_header.cc b/056recipe_header.cc
index 40952f69..046a607a 100644
--- a/056recipe_header.cc
+++ b/056recipe_header.cc
@@ -363,18 +363,14 @@ 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();
-//?   cerr << "--- deduce types from header for " << caller_recipe.name << '\n';
   map<string, const type_tree*> header_type;
-  map<string, const string_tree*> header_type_name;
   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);
-    put(header_type_name, caller_recipe.ingredients.at(i).name, caller_recipe.ingredients.at(i).properties.at(0).second);
-    trace(9993, "transform") << "type of " << caller_recipe.ingredients.at(i).name << " is " << to_string(caller_recipe.ingredients.at(i).type) << end();
+    trace(9993, "transform") << "type of " << caller_recipe.ingredients.at(i).name << " is " << names_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);
-    put(header_type_name, caller_recipe.products.at(i).name, caller_recipe.products.at(i).properties.at(0).second);
-    trace(9993, "transform") << "type of " << caller_recipe.products.at(i).name << " is " << to_string(caller_recipe.products.at(i).type) << end();
+    trace(9993, "transform") << "type of " << caller_recipe.products.at(i).name << " is " << names_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);
@@ -385,9 +381,7 @@ void deduce_types_from_header(const recipe_ordinal r) {
         continue;
       if (!inst.ingredients.at(i).type)
         inst.ingredients.at(i).type = new type_tree(*get(header_type, inst.ingredients.at(i).name));
-      if (!inst.ingredients.at(i).properties.at(0).second)
-        inst.ingredients.at(i).properties.at(0).second = new string_tree(*get(header_type_name, inst.ingredients.at(i).name));
-      trace(9993, "transform") << "type of " << inst.ingredients.at(i).name << " is " << to_string(inst.ingredients.at(i).type) << end();
+      trace(9993, "transform") << "type of " << inst.ingredients.at(i).name << " is " << names_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();
@@ -396,9 +390,7 @@ void deduce_types_from_header(const recipe_ordinal r) {
         continue;
       if (!inst.products.at(i).type)
         inst.products.at(i).type = new type_tree(*get(header_type, inst.products.at(i).name));
-      if (!inst.products.at(i).properties.at(0).second)
-        inst.products.at(i).properties.at(0).second = new string_tree(*get(header_type_name, inst.products.at(i).name));
-      trace(9993, "transform") << "type of " << inst.products.at(i).name << " is " << to_string(inst.products.at(i).type) << end();
+      trace(9993, "transform") << "type of " << inst.products.at(i).name << " is " << names_to_string(inst.products.at(i).type) << end();
     }
   }
 }
diff --git a/057static_dispatch.cc b/057static_dispatch.cc
index 576ef35f..9b1734b0 100644
--- a/057static_dispatch.cc
+++ b/057static_dispatch.cc
@@ -69,12 +69,12 @@ 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_types(r1.ingredients.at(i), r2.ingredients.at(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_types(r1.products.at(i), r2.products.at(i))) {
+    if (!deeply_equal_type_names(r1.products.at(i), r2.products.at(i))) {
       return false;
     }
   }
@@ -87,21 +87,21 @@ set<string> Literal_type_names;
 Literal_type_names.insert("number");
 Literal_type_names.insert("character");
 :(code)
-bool deeply_equal_types(const reagent& a, const reagent& b) {
-  return deeply_equal_types(a.properties.at(0).second, b.properties.at(0).second);
+bool deeply_equal_type_names(const reagent& a, const reagent& b) {
+  return deeply_equal_type_names(a.type, b.type);
 }
-bool deeply_equal_types(const string_tree* a, const string_tree* b) {
+bool deeply_equal_type_names(const type_tree* a, const type_tree* b) {
   if (!a) return !b;
   if (!b) return !a;
-  if (a->value == "literal" && b->value == "literal")
+  if (a->name == "literal" && b->name == "literal")
     return true;
-  if (a->value == "literal")
-    return Literal_type_names.find(b->value) != Literal_type_names.end();
-  if (b->value == "literal")
-    return Literal_type_names.find(a->value) != Literal_type_names.end();
-  return a->value == b->value
-      && deeply_equal_types(a->left, b->left)
-      && deeply_equal_types(a->right, b->right);
+  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) {
@@ -130,14 +130,10 @@ recipe test a:number, b:number -> z:number [
 
 //: support recipe headers in a previous transform to fill in missing types
 :(before "End check_or_set_invalid_types")
-for (long long int i = 0; i < SIZE(caller.ingredients); ++i) {
-  check_or_set_invalid_types(caller.ingredients.at(i).type, caller.ingredients.at(i).properties.at(0).second,
-                             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, caller.products.at(i).properties.at(0).second,
-                             maybe(caller.name), "recipe header product");
-}
+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");
 
 //: after filling in all missing types (because we'll be introducing 'blank' types in this transform in a later layer, for shape-shifting recipes)
 :(after "Transform.push_back(transform_names)")
@@ -156,7 +152,6 @@ list<call> resolve_stack;
 void resolve_ambiguous_calls(recipe_ordinal r) {
   recipe& caller_recipe = get(Recipe, r);
   trace(9991, "transform") << "--- resolve ambiguous calls for recipe " << caller_recipe.name << end();
-//?   cerr << "--- resolve ambiguous calls for recipe " << caller_recipe.name << '\n';
   for (long long int index = 0; index < SIZE(caller_recipe.steps); ++index) {
     instruction& inst = caller_recipe.steps.at(index);
     if (inst.is_label) continue;
diff --git a/058shape_shifting_container.cc b/058shape_shifting_container.cc
index 33b81333..74f97016 100644
--- a/058shape_shifting_container.cc
+++ b/058shape_shifting_container.cc
@@ -97,14 +97,24 @@ void read_type_ingredients(string& name) {
 
 :(before "End insert_container Special-cases")
 // check for use of type ingredients
-else if (!properties->value.empty() && properties->value.at(0) == '_') {
-  result->value = get(info.type_ingredient_names, properties->value);
+else if (is_type_ingredient_name(type->name)) {
+  type->value = get(info.type_ingredient_names, type->name);
+}
+:(code)
+bool is_type_ingredient_name(const string& type) {
+  return !type.empty() && type.at(0) == '_';
 }
 
 :(before "End Container Type Checks")
 if (type->value >= START_TYPE_INGREDIENTS
     && (type->value - START_TYPE_INGREDIENTS) < SIZE(get(Type, type->value).type_ingredient_names))
   return;
+:(code)
+//? //: TODO: is this necessary?
+//? :(before "End Container Type Checks2")
+//? if (type->value >= START_TYPE_INGREDIENTS
+//?     && (type->value - START_TYPE_INGREDIENTS) < SIZE(get(Type, type->value).type_ingredient_names))
+//?   return;
 
 :(scenario size_of_shape_shifting_exclusive_container)
 exclusive-container foo:_t [
@@ -226,8 +236,8 @@ recipe main [
 :(before "End element_type Special-cases")
 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, element.properties.at(0).second, canonized_base.type->right, canonized_base.properties.at(0).second ? canonized_base.properties.at(0).second->right : NULL, info);
+    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);
 }
 
 :(code)
@@ -242,23 +252,22 @@ bool contains_type_ingredient(const type_tree* type) {
 }
 
 // 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, string_tree* element_type_name, const type_tree* callsite_type, const string_tree* callsite_type_name, const type_info& container_info) {
+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, element_type_name ? element_type_name->left : NULL, callsite_type, callsite_type_name, container_info);
-  replace_type_ingredients(element_type->right, element_type_name ? element_type_name->right : NULL, callsite_type, callsite_type_name, container_info);
+  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 '" << to_string(callsite_type) << "' seems to be missing a type ingredient or three\n" << end();
+    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
-  // B1. update value/left/right of element_type
   const type_tree* replacement = NULL;
   bool splice_right = true ;
   {
@@ -287,28 +296,6 @@ void replace_type_ingredients(type_tree* element_type, string_tree* element_type
     element_type->right = replacement->right ? new type_tree(*replacement->right) : NULL;
     append(element_type->right, old_right);
   }
-
-  // B2. update value/left/right of element_type_name
-  if (!callsite_type_name || !element_type_name) return;
-  const string_tree* replacement_name = NULL;
-  // could compute splice_right again here, but why bother
-  {
-    const string_tree* curr = callsite_type_name;
-    for (long long int i = 0; i < type_ingredient_index; ++i)
-      curr = curr->right;
-    if (curr && curr->left)
-      replacement_name = curr->left;
-    else
-      replacement_name = curr;
-  }
-  element_type_name->value = replacement_name->value;
-  assert(!element_type_name->left);  // since value is set
-  element_type_name->left = replacement_name->left ? new string_tree(*replacement_name->left) : NULL;
-  if (splice_right) {
-    string_tree* old_right = element_type_name->right;
-    element_type_name->right = replacement_name->right ? new string_tree(*replacement_name->right) : NULL;
-    append(element_type_name->right, old_right);
-  }
 }
 
 bool final_type_ingredient(long long int type_ingredient_index, const type_info& container_info) {
@@ -464,7 +451,7 @@ recipe main [
   10:foo:point <- merge 14, 15, 16
   1:number <- get 10:foo, 1:offset
 ]
-+error: illegal type 'foo' seems to be missing a type ingredient or three
++error: illegal type "foo" seems to be missing a type ingredient or three
 
 //: get-address similarly
 
@@ -576,5 +563,5 @@ recipe main [
 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, element.properties.at(0).second, canonized_base.type->right, canonized_base.properties.at(0).second ? canonized_base.properties.at(0).second->right : NULL, info);
+  replace_type_ingredients(element.type, canonized_base.type->right, info);
 }
diff --git a/059shape_shifting_recipe.cc b/059shape_shifting_recipe.cc
index 79f4094c..d775686d 100644
--- a/059shape_shifting_recipe.cc
+++ b/059shape_shifting_recipe.cc
@@ -49,14 +49,12 @@ if (contains_type_ingredient_name(to)) return false;
 :(before "End Globals")
 vector<recipe_ordinal> Recently_added_shape_shifting_recipes;
 :(before "End Setup")
-//? cerr << "setup: clearing recently-added shape-shifting recipes\n";
 Recently_added_shape_shifting_recipes.clear();
 
 //: make sure we don't clear any of these recipes when we start running tests
 :(before "End Loading .mu Files")
 Recently_added_recipes.clear();
 Recently_added_types.clear();
-//? cerr << "clearing recently-added shape-shifting recipes\n";
 Recently_added_shape_shifting_recipes.clear();
 
 //: save original name of specialized recipes
@@ -222,7 +220,6 @@ bool concrete_type_names_strictly_match(const type_tree* to, const type_tree* fr
     return true;
   if (from->name == "literal" && to->name == "address")
     return rhs_reagent.name == "0";
-//?   cerr << to->name << " vs " << from->name << '\n';
   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);
@@ -238,10 +235,6 @@ bool contains_type_ingredient_name(const type_tree* type) {
   return contains_type_ingredient_name(type->left) || contains_type_ingredient_name(type->right);
 }
 
-bool is_type_ingredient_name(const string& type) {
-  return !type.empty() && type.at(0) == '_';
-}
-
 recipe_ordinal new_variant(recipe_ordinal exemplar, const instruction& inst, const recipe& caller_recipe) {
   string new_name = next_unused_recipe_name(inst.name);
   assert(!contains_key(Recipe_ordinal, new_name));
@@ -260,11 +253,11 @@ recipe_ordinal new_variant(recipe_ordinal exemplar, const instruction& inst, con
   compute_type_names(new_recipe);
   // that gives enough information to replace type-ingredients with concrete types
   {
-    map<string, const string_tree*> mappings;
+    map<string, const type_tree*> mappings;
     bool error = false;
     compute_type_ingredient_mappings(get(Recipe, exemplar), inst, mappings, caller_recipe, &error);
     if (!error) replace_type_ingredients(new_recipe, mappings);
-    for (map<string, const string_tree*>::iterator p = mappings.begin(); p != mappings.end(); ++p)
+    for (map<string, const type_tree*>::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
   }
@@ -274,7 +267,7 @@ recipe_ordinal new_variant(recipe_ordinal exemplar, const instruction& inst, con
 
 void compute_type_names(recipe& variant) {
   trace(9993, "transform") << "compute type names: " << variant.name << end();
-  map<string, string_tree*> type_names;
+  map<string, type_tree*> 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)
@@ -289,29 +282,28 @@ void compute_type_names(recipe& variant) {
   }
 }
 
-void save_or_deduce_type_name(reagent& x, map<string, string_tree*>& type_name, const recipe& variant) {
-  trace(9994, "transform") << "    checking " << to_string(x) << ": " << to_string(x.properties.at(0).second) << end();
-  if (!x.properties.at(0).second && contains_key(type_name, x.name)) {
-    x.properties.at(0).second = new string_tree(*get(type_name, x.name));
-    trace(9994, "transform") << "    deducing type to " << to_string(x.properties.at(0).second) << end();
+void save_or_deduce_type_name(reagent& x, map<string, type_tree*>& 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.properties.at(0).second) {
+  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_name, x.name)) 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_name, x.name, x.properties.at(0).second);
-  trace(9993, "transform") << "type of " << x.name << " is " << to_string(x.properties.at(0).second) << end();
+  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<string, const string_tree*>& mappings, const recipe& caller_recipe, bool* error) {
+void compute_type_ingredient_mappings(const recipe& exemplar, const instruction& inst, map<string, const type_tree*>& 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);
-    assert(ingredient.properties.at(0).second);
     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);
@@ -320,7 +312,6 @@ void compute_type_ingredient_mappings(const recipe& exemplar, const instruction&
   for (long long int i = 0; i < limit; ++i) {
     const reagent& exemplar_reagent = exemplar.products.at(i);
     reagent product = inst.products.at(i);
-    assert(product.properties.at(0).second);
     canonize_type(product);
     accumulate_type_ingredients(exemplar_reagent, product, mappings, exemplar, inst, caller_recipe, error);
   }
@@ -330,39 +321,37 @@ 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<string, const string_tree*>& mappings, const recipe& exemplar, const instruction& call_instruction, const recipe& caller_recipe, bool* error) {
-  assert(refinement.properties.at(0).second);
-  accumulate_type_ingredients(exemplar_reagent.properties.at(0).second, refinement.properties.at(0).second, mappings, exemplar, exemplar_reagent, call_instruction, caller_recipe, error);
+void accumulate_type_ingredients(const reagent& exemplar_reagent, reagent& refinement, map<string, const type_tree*>& 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 string_tree* exemplar_type, const string_tree* refinement_type, map<string, const string_tree*>& mappings, const recipe& exemplar, const reagent& exemplar_reagent, const instruction& call_instruction, const recipe& caller_recipe, bool* error) {
+void accumulate_type_ingredients(const type_tree* exemplar_type, const type_tree* refinement_type, map<string, const type_tree*>& 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 (!exemplar_type->value.empty() && exemplar_type->value.at(0) == '_') {
-    assert(!refinement_type->value.empty());
+  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->value)) {
-      trace(9993, "transform") << "adding mapping from " << exemplar_type->value << " to " << to_string(refinement_type) << end();
-      put(mappings, exemplar_type->value, new string_tree(*refinement_type));
+    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_types(get(mappings, exemplar_type->value), refinement_type)) {
+      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();
-//?         cerr << exemplar_type->value << ": " << debug_string(get(mappings, exemplar_type->value)) << " vs " << debug_string(refinement_type) << '\n';
         *error = true;
         return;
       }
-//?       cerr << exemplar_type->value << ": " << debug_string(get(mappings, exemplar_type->value)) << " <= " << debug_string(refinement_type) << '\n';
-      if (get(mappings, exemplar_type->value)->value == "literal") {
-        delete get(mappings, exemplar_type->value);
-        put(mappings, exemplar_type->value, new string_tree(*refinement_type));
+      if (get(mappings, exemplar_type->name)->name == "literal") {
+        delete get(mappings, exemplar_type->name);
+        put(mappings, exemplar_type->name, new type_tree(*refinement_type));
       }
     }
   }
@@ -372,7 +361,7 @@ void accumulate_type_ingredients(const string_tree* exemplar_type, const string_
   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<string, const string_tree*>& mappings) {
+void replace_type_ingredients(recipe& new_recipe, const map<string, const type_tree*>& mappings) {
   // update its header
   if (mappings.empty()) return;
   trace(9993, "transform") << "replacing in recipe header ingredients" << end();
@@ -391,63 +380,107 @@ void replace_type_ingredients(recipe& new_recipe, const map<string, const string
       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") {
-      string_tree* type_name = parse_string_tree(inst.ingredients.at(0).name);
-      replace_type_ingredients(type_name, mappings);
-      inst.ingredients.at(0).name = inspect(type_name);
-      delete type_name;
+      type_tree* type = parse_type_tree(inst.ingredients.at(0).name);
+      replace_type_ingredients(type, mappings);
+      inst.ingredients.at(0).name = inspect(type);
+      delete type;
     }
   }
 }
 
-void replace_type_ingredients(reagent& x, const map<string, const string_tree*>& mappings, const recipe& caller) {
+void replace_type_ingredients(reagent& x, const map<string, const type_tree*>& mappings, const recipe& caller) {
+  string before = to_string(x);
   trace(9993, "transform") << "replacing in ingredient " << x.original_string << end();
-  // replace properties
-  if (!x.properties.at(0).second) {
+  if (!x.type) {
     raise_error << "specializing " << caller.original_name << ": missing type for " << x.original_string << '\n' << end();
     return;
   }
-  replace_type_ingredients(x.properties.at(0).second, mappings);
-  // refresh types from properties
-  delete x.type;
-  x.type = new_type_tree(x.properties.at(0).second);
-  if (x.type)
-    trace(9993, "transform") << "  after: " << to_string(x.type) << end();
+  replace_type_ingredients(x.type, mappings);
 }
 
-void replace_type_ingredients(string_tree* type, const map<string, const string_tree*>& mappings) {
+void replace_type_ingredients(type_tree* type, const map<string, const type_tree*>& mappings) {
   if (!type) return;
-  if (is_type_ingredient_name(type->value) && contains_key(mappings, type->value)) {
-    const string_tree* replacement = get(mappings, type->value);
-    trace(9993, "transform") << type->value << " => " << to_string(replacement) << end();
-    if (replacement->value == "literal")
-      type->value = "number";
-    else
+  if (contains_key(Type_ordinal, type->name))  // todo: ugly side effect. bugfix #0
+    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);
+    trace(9993, "transform") << type->name << " => " << names_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 string_tree(*replacement->left);
-    if (replacement->right) type->right = new string_tree(*replacement->right);
+    }
+    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);
 }
 
-string inspect(const string_tree* x) {
+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)) {  // adding this condition was bugfix #2
+        if (!contains_key(Type_ordinal, (*curr)->name))
+          put(Type_ordinal, (*curr)->name, Next_type_ordinal++);
+        (*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 string_tree* x, ostream& out) {
+void dump_inspect(const type_tree* x, ostream& out) {
   if (!x->left && !x->right) {
-    out << x->value;
+    out << x->name;
     return;
   }
   out << '(';
-  for (const string_tree* curr = x; curr; curr = curr->right) {
+  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->value;
+      out << curr->name;
   }
   out << ')';
 }
@@ -467,7 +500,7 @@ void ensure_all_concrete_types(/*const*/ recipe& new_recipe, const recipe& exemp
 }
 
 void ensure_all_concrete_types(/*const*/ reagent& x, const recipe& exemplar) {
-  if (!x.type) {
+  if (!x.type || contains_type_ingredient_name(x.type)) {  // adding the second clause was bugfix #1
     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;
diff --git a/061recipe.cc b/061recipe.cc
index 890015e5..7c1b3d05 100644
--- a/061recipe.cc
+++ b/061recipe.cc
@@ -33,7 +33,6 @@ get_or_insert(Type, recipe).name = "recipe";
 
 :(before "End Null-type is_disqualified Exceptions")
 if (!x.type && contains_key(Recipe_ordinal, x.name)) {
-  x.properties.at(0).second = new string_tree("recipe-literal");
   x.type = new type_tree("recipe-literal", get(Type_ordinal, "recipe-literal"));
   x.set_value(get(Recipe_ordinal, x.name));
   return true;
diff --git a/086scenario_console_test.mu b/086scenario_console_test.mu
index a11b0091..535bfe7b 100644
--- a/086scenario_console_test.mu
+++ b/086scenario_console_test.mu
@@ -23,3 +23,8 @@ scenario read-key-in-mu [
     8 <- 1
   ]
 ]
+
+#? recipe foo-editor [
+#?   local-scope
+#?   init:address:shared:list:character <- push 97/a, 0
+#? ]
diff --git a/mu.cc.modified b/mu.cc.modified
new file mode 100644
index 00000000..e595855c
--- /dev/null
+++ b/mu.cc.modified
@@ -0,0 +1,12001 @@
+// Includes
+#include<stdlib.h>
+
+#define SIZE(X) (assert((X).size() < (1LL<<(sizeof(long long int)*8-2))), static_cast<long long int>((X).size()))
+#include<assert.h>
+
+#include<iostream>
+using std::istream;
+using std::ostream;
+using std::iostream;
+using std::cin;
+using std::cout;
+using std::cerr;
+
+#include<cstring>
+#include<string>
+using std::string;
+
+#include<cstdlib>
+
+#define CHECK_TRACE_CONTENTS(...)  check_trace_contents(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__)
+
+#include<vector>
+using std::vector;
+#include<list>
+using std::list;
+#include<map>
+using std::map;
+#include<set>
+using std::set;
+#include<algorithm>
+
+#include<iostream>
+using std::istream;
+using std::ostream;
+using std::cin;
+using std::cout;
+using std::cerr;
+#include<iomanip>
+
+#include<sstream>
+using std::istringstream;
+using std::ostringstream;
+
+#include<fstream>
+using std::ifstream;
+using std::ofstream;
+
+#include"termbox/termbox.h"
+
+#define unused  __attribute__((unused))
+
+#include<utility>
+using std::pair;
+#include<math.h>
+
+#include<dirent.h>
+#include<sys/stat.h>
+
+
+#include <stack>
+using std::stack;
+
+using std::min;
+using std::max;
+
+using std::abs;
+
+#include<math.h>
+
+// 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<string> 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<pair<string, string_tree*> > 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<reagent> ingredients;  // only if !is_label
+  vector<reagent> products;  // only if !is_label
+  mutable bool tangle_done;
+  // End instruction Fields
+  instruction();
+  void clear();
+  bool is_empty();
+};
+
+struct recipe {
+  string name;
+  vector<instruction> steps;
+  long long int transformed_until;
+  bool has_header;
+  vector<reagent> ingredients;
+  vector<reagent> products;
+  map<string, int> 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<reagent> elements;
+  map<string, type_ordinal> 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<vector<double> > ingredient_atoms;
+  vector<reagent> 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> 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<instruction>& 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<merge_check_point> 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> 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> 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> 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<typename T> bool contains_key(T& map, typename T::key_type const& key) {
+  return map.find(key) != map.end();
+}
+template<typename T> 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_ordinal, recipe> Recipe;
+map<string, recipe_ordinal> Recipe_ordinal;
+recipe_ordinal Next_recipe_ordinal = 1;
+
+// Locations refer to a common 'memory'. Each location can store a number.
+map<long long int, double> Memory;
+map<string, type_ordinal> Type_ordinal;
+map<type_ordinal, type_info> 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<recipe_ordinal> Recently_added_recipes;
+long long int Reserved_for_tests = 1000;
+vector<transform_fn> Transform;
+
+routine* Current_routine = NULL;
+map<string, long long int> Instructions_running;
+map<string, long long int> Locations_read;
+map<string, long long int> Locations_read_by_instruction;
+
+ofstream LOG;
+vector<type_ordinal> 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<long long int, long long int> Free_list;
+map<recipe_ordinal, map<string, long long int> > Name;
+bool Warn_on_missing_default_space = false;
+map<recipe_ordinal, recipe_ordinal> Surrounding_space;
+
+vector<scenario> Scenarios;
+set<string> Scenario_names;
+
+
+long long int Num_core_mu_tests = 0;
+const scenario* Current_scenario = NULL;
+bool Scenario_testing_scenario = false;
+map<string /*label*/, recipe> Before_fragments, After_fragments;
+set<string /*label*/> Fragments_used;
+bool Transform_check_insert_fragments_Ran = false;
+map<string, vector<recipe_ordinal> > Recipe_variants;
+set<string> Literal_type_names;
+list<call> resolve_stack;
+
+// We'll use large type ordinals to mean "the following type of the variable".
+const int START_TYPE_INGREDIENTS = 2000;
+vector<recipe_ordinal> Recently_added_shape_shifting_recipes;
+vector<routine*> 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<string, long long int> Key;
+set<long long int> Visible;
+long long int Top_of_screen = 0;
+long long int Last_printed_row = 0;
+map<int, long long int> 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<trace_line> 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<trace_line>::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<string> 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<trace_line>::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<trace_line>::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<string> 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<string> split(string s, string delim) {
+  vector<string> 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<string> split_first(string s, string delim) {
+  vector<string> 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<recipe_ordinal> Save_recently_added_recipes;
+vector<recipe_ordinal> 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<recipe_ordinal>());  // 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<recipe_ordinal, recipe>::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<recipe_ordinal, recipe>::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<string> result = split("", ",");
+  CHECK_EQ(result.size(), 1);
+  CHECK_EQ(result.at(0), "");
+}
+
+void test_split_returns_entire_input_when_no_delim() {
+  vector<string> result = split("abc", ",");
+  CHECK_EQ(result.size(), 1);
+  CHECK_EQ(result.at(0), "abc");
+}
+
+void test_split_works() {
+  vector<string> 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<string> 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<string> 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<type_ordinal, type_info>::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 <name>:<type>:<type>:.../<property>/<property>/...
+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<string, string_tree*>(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<string, string_tree*>(key, value));
+  }
+  {
+    while (!name.empty() && name.at(0) == '*') {
+      name.erase(0, 1);
+      properties.push_back(pair<string, string_tree*>("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<string, string_tree*>(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<string, string_tree*>(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<long long int, double>::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<recipe_ordinal> load(string form) {
+  istringstream in(form);
+  in >> std::noskipws;
+  return load(in);
+}
+
+vector<recipe_ordinal> load(istream& in) {
+  in >> std::noskipws;
+  vector<recipe_ordinal> 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<reagent> 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<reagent> 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<string> 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<string>::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<reagent>::iterator p = curr->ingredients.begin(); p != curr->ingredients.end(); ++p)
+    trace(9993, "parse") << "  ingredient: " << to_string(*p) << end();
+  for (vector<reagent>::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<string, vector<recipe_ordinal> >::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<recipe_ordinal, recipe>::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<recipe_ordinal, recipe>::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<char>(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<char>(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<char>(in.get());
+      continue;
+    }
+    if (c == '#') {
+      out << c;
+      while (has_data(in) && in.peek() != '\n') out << static_cast<char>(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<vector<double> > 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<vector<double> > 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<long long int>(ingredients.at(0).at(0));
+        long long int b = static_cast<long long int>(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<long long int>(ingredients.at(0).at(0));
+        long long int b = static_cast<long long int>(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 SHIFT_RIGHT: {
+        // ingredients must be integers
+        long long int a = static_cast<long long int>(ingredients.at(0).at(0));
+        long long int b = static_cast<long long int>(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<long long int>(ingredients.at(0).at(0));
+        long long int b = static_cast<long long int>(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<long long int>(ingredients.at(0).at(0));
+        long long int b = static_cast<long long int>(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<long long int>(ingredients.at(0).at(0));
+        long long int b = static_cast<long long int>(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<long long int>(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<double>& 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<string, string_tree*>("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<double> 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<string, string_tree*>("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<double> 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<long long int>(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<long long int>(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<recipe_ordinal> 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<size_t>(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<string, vector<recipe_ordinal> >::iterator p = Recipe_variants.begin(); p != Recipe_variants.end(); ++p) {
+      //?     cerr << p->first << ":\n";
+          vector<recipe_ordinal>& 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<recipe_ordinal> 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<instruction>& 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<double> arg;
+    arg.push_back(new_mu_string(argv[i]));
+    current_call().ingredient_atoms.push_back(arg);
+  }
+  run(main_routine);
+}
+
+
+
+void dump_profile() {
+  for (map<string, long long int>::iterator p = Instructions_running.begin(); p != Instructions_running.end(); ++p) {
+    cerr << p->first << ": " << p->second << '\n';
+  }
+  cerr << "== locations read\n";
+  for (map<string, long long int>::iterator p = Locations_read.begin(); p != Locations_read.end(); ++p) {
+    cerr << p->first << ": " << p->second << '\n';
+  }
+  cerr << "== locations read by instruction\n";
+  for (map<string, long long int>::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<double> read_memory(reagent x) {
+  if (x.name == "number-of-locals") {
+    vector<double> 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<double> result;
+    result.push_back(current_call().default_space);
+    return result;
+  }
+
+
+  vector<double> 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<double> 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<double>& 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<long long int>& x) {
+  return SIZE(x) == 1;
+}
+inline bool scalar(const vector<double>& x) {
+  return SIZE(x) == 1;
+}
+
+// helper for tests
+void run(string form) {
+  vector<recipe_ordinal> 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<string, vector<type_ordinal> > 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<double>& 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<type_ordinal, type_info>::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<reagent>& 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<pair<string, string_tree*> >::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<vector<double> >& 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<double>& 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<uint32_t>(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<pair<int/*OPEN/CLOSE*/, /*step*/long long int> > 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<int,long long int>(OPEN, index));
+    }
+    if (inst.label == "}") {
+      trace(9993, "transform") << "push (close, " << index << ")" << end();
+      braces.push_back(pair<int,long long int>(CLOSE, index));
+    }
+  }
+  stack</*step*/long long int> 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<pair<int, long long int> >& braces, recipe_ordinal r) {
+  int stacksize = 0;
+  for (list<pair<int, long long int> >::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<string, long long int> 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<string, long long int>& 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<string, long long int>& 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<string, long long int>& 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<recipe_ordinal> done;
+  vector<recipe_ordinal> 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<recipe_ordinal>& done, vector<recipe_ordinal>& 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<string, string_tree*>("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<string, long long int>::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<string, type_tree*> 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<string, type_tree*>& 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<string, type_tree*>& 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<long long int> 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<char>(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<trace_line> expected_lines = parse_trace(expected);
+  if (expected_lines.empty()) return;
+  long long int curr_expected_line = 0;
+  for (vector<trace_line>::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<trace_line> parse_trace(const string& expected) {
+  vector<string> buf = split(expected, "\n");
+  vector<trace_line> 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<trace_line> 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  <label1>\n  3:number <- copy 0\n]\nbefore <label1> [\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<instruction> 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<instruction>& base, const vector<instruction>& 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<string> 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<string, recipe>::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<string, recipe>::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  <label1>\n  4:number <- copy 0\n]\nbefore <label1> [\n  2:number <- copy 0\n]\nafter <label1> [\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  <label1>\n  <label2>\n  6:number <- copy 0\n]\nbefore <label1> [\n  2:number <- copy 0\n]\nafter <label1> [\n  3:number <- copy 0\n]\nbefore <label2> [\n  4:number <- copy 0\n]\nafter <label2> [\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  <label1>\n  6:number <- copy 0\n]\nbefore <label1> [\n  2:number <- copy 0\n]\nafter <label1> [\n  3:number <- copy 0\n]\nbefore <label1> [\n  4:number <- copy 0\n]\nafter <label1> [\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  <label1>\n  6:number <- copy 0\n]\nbefore <label1> [\n  2:number <- copy 0\n  3:number <- copy 0\n]\nafter <label1> [\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  <label1>\n  4:number <- copy 10\n  recipe2\n]\nrecipe recipe2 [\n  1:number <- copy 11\n  <label1>\n  4:number <- copy 11\n]\nbefore <label1> [\n  2:number <- copy 12\n]\nafter <label1> [\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  <label1>\n  <label1>\n  4:number <- copy 10\n]\nbefore <label1> [\n  2:number <- copy 12\n]\nafter <label1> [\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  <label1>\n  <foo>\n  4:number <- copy 10\n]\nbefore <label1> [\n  2:number <- copy 12\n]\nafter <label1> [\n  3:number <- copy 12\n]\nafter <foo> [\n  <label1>\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  <label1>\n  4:number <- copy 10\n]\nbefore <label1> [\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  <label1>\n  +label2\n  4:number <- copy 10\n]\nbefore <label1> [\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  <label1>\n  +label2\n  4:number <- copy 10\n]\nbefore <label1> [\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<instruction> 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<char> 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<string, const type_tree*> 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<string, string_tree*>("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<recipe_ordinal>& 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<recipe_ordinal>& variants = get(Recipe_variants, inst.name);
+  vector<recipe_ordinal> 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<call>::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<recipe_ordinal> strictly_matching_variants(const instruction& inst, vector<recipe_ordinal>& variants) {
+  vector<recipe_ordinal> 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<recipe_ordinal> strictly_matching_variants_except_literal_against_boolean(const instruction& inst, vector<recipe_ordinal>& variants) {
+  vector<recipe_ordinal> 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<recipe_ordinal> matching_variants(const instruction& inst, vector<recipe_ordinal>& variants) {
+  vector<recipe_ordinal> 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<recipe_ordinal>& 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<recipe_ordinal>& 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<string, type_ordinal>::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<string, type_ordinal>::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<recipe_ordinal> strictly_matching_shape_shifting_variants(const instruction& inst, vector<recipe_ordinal>& variants) {
+  vector<recipe_ordinal> 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<recipe_ordinal>& 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<string, const type_tree*> 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<string, const type_tree*>::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<string, type_tree*> 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<string, type_tree*>& 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<string, const type_tree*>& 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<string, const type_tree*>& 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<string, const type_tree*>& 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<string, const type_tree*>& 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<string, const type_tree*>& 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<string, const type_tree*>& 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<string> 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<string>& current_ingredient_and_aliases) {
+  set<long long int> current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases);
+  if (!contains_key(Recipe, inst.operation)) {
+    // primitive recipe
+    if (inst.operation == COPY) {
+      for (set<long long int>::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<long long int> contained_in_product_indices = scan_contained_in_product_indices(inst, current_ingredient_indices);
+    for (set<long long int>::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<long long int> scan_contained_in_product_indices(const instruction& inst, set<long long int>& ingredient_indices) {
+  set<string> selected_ingredient_names;
+  const recipe& callee = get(Recipe, inst.operation);
+  for (set<long long int>::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<long long int> 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<string>& current_ingredient_and_aliases, const string& original_ingredient_name, const recipe& caller) {
+  set<long long int> current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases);
+  if (current_ingredient_indices.empty()) return;  // ingredient not found in call
+  for (set<long long int>::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<long long int> ingredient_indices(const instruction& inst, const set<string>& ingredient_names) {
+  set<long long int> 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<size_t>(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<size_t>(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)) {
+        // " ('<curr>')"
+        expected_pretty[0] = ' ', expected_pretty[1] = '(', expected_pretty[2] = '\'', expected_pretty[3] = static_cast<unsigned char>(curr), expected_pretty[4] = '\'', expected_pretty[5] = ')', expected_pretty[6] = '\0';
+      }
+      char actual_pretty[10] = {0};
+      if (get_or_insert(Memory, addr) < 256 && !iscntrl(get_or_insert(Memory, addr))) {
+        // " ('<curr>')"
+        actual_pretty[0] = ' ', actual_pretty[1] = '(', actual_pretty[2] = '\'', actual_pretty[3] = static_cast<unsigned char>(get_or_insert(Memory, addr)), actual_pretty[4] = '\'', actual_pretty[5] = ')', actual_pretty[6] = '\0';
+      }
+
+      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<int>(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<uint32_t>(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<vector<double> >& 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<trace_line>::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<trace_line>::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<recipe_ordinal, recipe>::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;
+//?         }
+//?       }
+//?     }
+//?   }
+//? }
+