about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--010vm.cc161
-rw-r--r--017parse_tree.cc28
-rw-r--r--018type_abbreviations.cc52
-rw-r--r--020run.cc10
-rw-r--r--021check_instruction.cc76
-rw-r--r--027call_ingredient.cc16
-rw-r--r--028call_reply.cc2
-rw-r--r--030container.cc299
-rw-r--r--031merge.cc16
-rw-r--r--032array.cc48
-rw-r--r--033exclusive_container.cc35
-rw-r--r--034address.cc8
-rw-r--r--035lookup.cc8
-rw-r--r--036refcount.cc346
-rw-r--r--037abandon.cc2
-rw-r--r--039location_array.cc22
-rw-r--r--042name.cc10
-rw-r--r--043space.cc14
-rw-r--r--045closure_name.cc15
-rw-r--r--046global.cc17
-rw-r--r--047check_type_by_name.cc2
-rw-r--r--050scenario.cc4
-rw-r--r--054static_dispatch.cc5
-rw-r--r--055shape_shifting_container.cc203
-rw-r--r--056shape_shifting_recipe.cc130
-rw-r--r--057immutable.cc4
-rw-r--r--062rewrite_stash.cc7
-rw-r--r--069hash.cc8
-rw-r--r--071recipe.cc31
-rw-r--r--073wait.cc11
-rw-r--r--074deep_copy.cc2
31 files changed, 1104 insertions, 488 deletions
diff --git a/010vm.cc b/010vm.cc
index 23d4ef0b..a3dd3bc5 100644
--- a/010vm.cc
+++ b/010vm.cc
@@ -70,40 +70,37 @@ struct reagent {
 // 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;
+  bool atom;
+  string name;  // only if atom
+  type_ordinal value;  // only if atom
+  type_tree* left;  // only if !atom
+  type_tree* right;  // only if !atom
   ~type_tree();
   type_tree(const type_tree& old);
-  // simple: type ordinal
+  // atomic type ordinal
   explicit type_tree(string name);
-  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) {}
-  type_tree(string name, type_tree* r);
-  // advanced: tree containing type ordinals
-  type_tree(type_tree* l, type_tree* r) :value(0), left(l), right(r) {}
+  type_tree(string name, type_ordinal v) :atom(true), name(name), value(v), left(NULL), right(NULL) {}
+  // tree of type ordinals
+  type_tree(type_tree* l, type_tree* r) :atom(false), value(0), left(l), right(r) {}
+  type_tree& operator=(const type_tree& old);
 };
 
 struct string_tree {
-  string value;
-  string_tree* left;
-  string_tree* right;
+  bool atom;
+  string value;  // only if atom
+  string_tree* left;  // only if !atom
+  string_tree* right;  // only if !atom
   ~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) {}
+  // atomic string
+  explicit string_tree(string v) :atom(true), value(v), left(NULL), right(NULL) {}
+  // tree of strings
+  string_tree(string_tree* l, string_tree* r) :atom(false), left(l), right(r) {}
 };
 
 // End type_tree Definition
 :(code)
-type_tree::type_tree(string name) :name(name), value(get(Type_ordinal, name)), left(NULL), right(NULL) {}
-type_tree::type_tree(string name, type_tree* r) :name(name), value(get(Type_ordinal, name)), left(NULL), right(r) {}
+type_tree::type_tree(string name) :atom(true), name(name), value(get(Type_ordinal, name)), left(NULL), right(NULL) {}
 
 :(before "End Globals")
 // Locations refer to a common 'memory'. Each location can store a number.
@@ -296,26 +293,29 @@ void slurp_properties(istream& in, vector<pair<string, string_tree*> >& out) {
 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;
+  string_tree* left = new string_tree(slurp_until(in, ':'));
+  if (!has_data(in)) return left;
+  string_tree* right = parse_property_list(in);
+  return new string_tree(left, right);
 }
 
 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 (properties->atom) {
+    const string& type_name = properties->value;
+    int value = 0;
     if (contains_key(Type_ordinal, type_name))
-      result->value = get(Type_ordinal, type_name);
+      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
+      value = 0;
+    else if (properties->value == "->")  // used in recipe types
+      value = 0;
+    else
+      value = -1;  // should never happen; will trigger errors later
+    return new type_tree(type_name, value);
   }
-  result->left = new_type_tree(properties->left);
-  result->right = new_type_tree(properties->right);
-  return result;
+  return new type_tree(new_type_tree(properties->left),
+                       new_type_tree(properties->right));
 }
 
 //: avoid memory leaks for the type tree
@@ -334,13 +334,24 @@ reagent::reagent(const reagent& other) {
 }
 
 type_tree::type_tree(const type_tree& old) {
+  atom = old.atom;
+  name = old.name;
+  value = old.value;
+  left = old.left ? new type_tree(*old.left) : NULL;
+  right = old.right ? new type_tree(*old.right) : NULL;
+}
+
+type_tree& type_tree::operator=(const type_tree& old) {
+  atom = old.atom;
   name = old.name;
   value = old.value;
   left = old.left ? new type_tree(*old.left) : NULL;
   right = old.right ? new type_tree(*old.right) : NULL;
+  return *this;
 }
 
 string_tree::string_tree(const string_tree& old) {
+  atom = old.atom;
   value = old.value;
   left = old.left ? new string_tree(*old.left) : NULL;
   right = old.right ? new string_tree(*old.right) : NULL;
@@ -543,27 +554,25 @@ string debug_string(const reagent& x) {
 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);
+  dump(property, out);
   return out.str();
 }
 
 void dump(const string_tree* x, ostream& out) {
-  if (!x->left && !x->right) {
-    out << x->value;
+  if (!x) return;
+  if (x->atom) {
+    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 << '"';
+  const string_tree* curr = x;
+  while (curr && !curr->atom) {
+    dump(curr->left, out);
+    if (curr->right) out << ' ';
+    curr = curr->right;
   }
+  // final right
+  dump(curr, out);
   out << ')';
 }
 
@@ -576,18 +585,20 @@ string to_string(const type_tree* type) {
 }
 
 void dump(const type_tree* x, ostream& out) {
-  if (!x->left && !x->right) {
+  if (!x) return;
+  if (x->atom) {
     dump(x->value, out);
     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);
+  const type_tree* curr = x;
+  while (curr && !curr->atom) {
+    dump(curr->left, out);
+    if (curr->right) out << ' ';
+    curr = curr->right;
   }
+  // final right
+  dump(curr, out);
   out << ')';
 }
 
@@ -606,19 +617,21 @@ string names_to_string(const type_tree* type) {
   return out.str();
 }
 
-void dump_names(const type_tree* type, ostream& out) {
-  if (!type->left && !type->right) {
-    out << '"' << type->name << '"';
+void dump_names(const type_tree* x, ostream& out) {
+  if (!x) return;
+  if (x->atom) {
+    out << '"' << x->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 << '"';
+  const type_tree* curr = x;
+  while (curr && !curr->atom) {
+    dump_names(curr->left, out);
+    if (curr->right) out << ' ';
+    curr = curr->right;
   }
+  // final right
+  dump_names(curr, out);
   out << ')';
 }
 
@@ -630,19 +643,21 @@ string names_to_string_without_quotes(const type_tree* type) {
   return out.str();
 }
 
-void dump_names_without_quotes(const type_tree* type, ostream& out) {
-  if (!type->left && !type->right) {
-    out << type->name;
+void dump_names_without_quotes(const type_tree* x, ostream& out) {
+  if (!x) return;
+  if (x->atom) {
+    out << x->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;
+  const type_tree* curr = x;
+  while (curr && !curr->atom) {
+    dump_names_without_quotes(curr->left, out);
+    if (curr->right) out << ' ';
+    curr = curr->right;
   }
+  // final right
+  dump_names_without_quotes(curr, out);
   out << ')';
 }
 
diff --git a/017parse_tree.cc b/017parse_tree.cc
index ce744f0f..8efcb83e 100644
--- a/017parse_tree.cc
+++ b/017parse_tree.cc
@@ -2,6 +2,11 @@
 // 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)".
+//
+// Type trees aren't as general as s-expressions even if they look like them:
+// the first element of a type tree is always an atom, and left and right
+// pointers of non-atoms are never NULL. All type trees are 'dotted' in lisp
+// parlance.
 
 :(scenarios load)
 :(scenario dilated_reagent_with_nested_brackets)
@@ -17,7 +22,7 @@ type_names = parse_string_tree(type_names);
 
 :(code)
 string_tree* parse_string_tree(string_tree* s) {
-  assert(!s->left && !s->right);
+  assert(s->atom);
   if (s->value.at(0) != '(') return s;
   string_tree* result = parse_string_tree(s->value);
   delete s;
@@ -44,17 +49,30 @@ string_tree* parse_string_tree(istream& in) {
   in.get();  // skip '('
   string_tree* result = NULL;
   string_tree** curr = &result;
-  while (in.peek() != ')') {
-    assert(has_data(in));
-    *curr = new string_tree("");
+  while (true) {
     skip_whitespace_but_not_newline(in);
+    assert(has_data(in));
+    if (in.peek() == ')') break;
+    *curr = new string_tree(NULL, NULL);
     if (in.peek() == '(')
       (*curr)->left = parse_string_tree(in);
     else
-      (*curr)->value = next_word(in);
+      (*curr)->left = new string_tree(next_word(in));
     curr = &(*curr)->right;
   }
   in.get();  // skip ')'
+  assert(*curr == NULL);
+  // standardize the final element to always be on the right if it's an atom
+  // (a b c) => (a b . c) in s-expression parlance
+  string_tree* tmp = result;
+  while (tmp->right && tmp->right->right) tmp = tmp->right;
+  assert(!tmp->right->atom);
+  if (!tmp->right->left->atom) return result;
+  string_tree* tmp2 = tmp->right;
+  tmp->right = tmp2->left;
+  tmp2->left = NULL;
+  assert(tmp2->right == NULL);
+  delete tmp2;
   return result;
 }
 
diff --git a/018type_abbreviations.cc b/018type_abbreviations.cc
index c4871d8c..a13d97a8 100644
--- a/018type_abbreviations.cc
+++ b/018type_abbreviations.cc
@@ -16,8 +16,7 @@ def main [
   f 4:&&@&@number  # ..any number of times
   f 5:array:&number:3  # abbreviations take precedence over ':'
   f {6: (array &number 3)}  # support for dilated reagents and more complex parse trees
-  f 7:&&@&  # abbreviations without payload
-  f 8:@number:3  # *not* the same as array:number:3
+  f 7:@number:3  # *not* the same as array:number:3
 ]
 +parse:   ingredient: {1: ("address" "number")}
 +parse:   ingredient: {2: ("array" "number")}
@@ -25,32 +24,33 @@ def main [
 +parse:   ingredient: {4: ("address" "address" "array" "address" "array" "number")}
 +parse:   ingredient: {5: ("array" ("address" "number") "3")}
 +parse:   ingredient: {6: ("array" ("address" "number") "3")}
-# an error that will be raised elsewhere
-+parse:   ingredient: {7: ("address" "address" "array" "address")}
 # not what you want
-+parse:   ingredient: {8: (("array" "number") "3")}
++parse:   ingredient: {7: (("array" "number") "3")}
+
+:(scenario abbreviation_error)
+% Hide_errors = true;
+def main [
+  f 1:&&@&  # abbreviations without payload
+]
++error: invalid type abbreviation &&@&
 
 :(before "End Parsing Reagent Type Property(type_names)")
-type_names = replace_address_and_array_symbols(type_names);
+string_tree* new_type_names = replace_address_and_array_symbols(type_names);
+delete type_names;
+type_names = new_type_names;
 :(before "End Parsing Dilated Reagent Type Property(type_names)")
-type_names = replace_address_and_array_symbols(type_names);
+string_tree* new_type_names = replace_address_and_array_symbols(type_names);
+delete type_names;
+type_names = new_type_names;
 
 :(code)
 // simple version; lots of unnecessary allocations; always creates a new pointer
-string_tree* replace_address_and_array_symbols(string_tree*& orig) {
+string_tree* replace_address_and_array_symbols(string_tree* orig) {
   if (orig == NULL) return NULL;
-  string_tree* new_left = replace_address_and_array_symbols(orig->left);
-  string_tree* new_right = replace_address_and_array_symbols(orig->right);
-  if (orig->value.empty()) {
-    delete orig;  orig = NULL;
-    return new string_tree(new_left, new_right);
-  }
-  assert(new_left == NULL);
-  new_left = replace_address_and_array_symbols(orig->value);
-  assert(new_left);
-  delete orig;  orig = NULL;
-  append(new_left, new_right);
-  return new_left;
+  if (orig->atom)
+    return replace_address_and_array_symbols(orig->value);
+  return new string_tree(replace_address_and_array_symbols(orig->left),
+                         replace_address_and_array_symbols(orig->right));
 }
 
 // todo: unicode
@@ -69,14 +69,18 @@ string_tree* replace_address_and_array_symbols(const string& type_name) {
       new_node = new string_tree("array");
     else
       break;
-    if (!curr)
-      result = curr = new_node;
-    else
-      curr->right = new_node, curr = curr->right;
+    if (result == NULL)
+      result = curr = new string_tree(new_node, NULL);
+    else {
+      curr->right = new string_tree(new_node, NULL);
+      curr = curr->right;
+    }
     ++i;
   }
   if (i < SIZE(type_name))
     curr->right = new string_tree(type_name.substr(i));
+  else
+    raise << "invalid type abbreviation " << type_name << "\n" << end();
   return result;
 }
 
diff --git a/020run.cc b/020run.cc
index 51944784..7c8f292d 100644
--- a/020run.cc
+++ b/020run.cc
@@ -326,10 +326,12 @@ bool size_mismatch(const reagent& x, const vector<double>& data) {
 }
 
 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;
+  return is_literal(r.type);
+}
+bool is_literal(const type_tree* type) {
+  if (!type) return false;
+  if (!type->atom) return false;
+  return type->value == 0;
 }
 
 bool scalar(const vector<int>& x) {
diff --git a/021check_instruction.cc b/021check_instruction.cc
index 3554d253..de5476a0 100644
--- a/021check_instruction.cc
+++ b/021check_instruction.cc
@@ -102,7 +102,6 @@ bool types_coercible(const reagent& to, const reagent& from) {
   if (is_mu_address(from) && is_mu_number(to)) return true;
   if (is_mu_boolean(from) && is_mu_number(to)) return true;
   if (is_mu_number(from) && is_mu_boolean(to)) return true;
-  // End types_coercible Special-cases
   return false;
 }
 
@@ -116,20 +115,13 @@ bool types_match(const reagent& to, const reagent& from) {
     // 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);
+    if (to.type->atom && to.type->value == get(Type_ordinal, "boolean"))
+      return from.name == "0" || from.name == "1";
     return size_of(to) == 1;  // literals are always scalars
   }
   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/*copy*/ to, reagent/*copy*/ from) {
@@ -147,10 +139,14 @@ bool types_strictly_match(reagent/*copy*/ to, reagent/*copy*/ from) {
 // two types match if the second begins like the first
 // (trees perform the same check recursively on each subtree)
 bool types_strictly_match(const type_tree* to, const type_tree* from) {
-  if (!to) return true;
-  if (!from) return to->value == 0;
-  if (from->value == -1) return from->name == to->name;
-  if (to->value != from->value) return false;
+  if (from == to) return true;
+  if (!from) return to->atom && to->value == 0;
+  if (to->atom && !from->atom) return from->left->atom && from->left->name == to->name;
+  if (from->atom) {
+    if (!to->atom) return false;
+    if (from->value == -1) return from->name == to->name;
+    return from->value == to->value;
+  }
   return types_strictly_match(to->left, from->left) && types_strictly_match(to->right, from->right);
 }
 
@@ -172,30 +168,43 @@ bool is_unsafe(const reagent& r) {
 
 bool is_mu_array(reagent/*copy*/ r) {
   // End Preprocess is_mu_array(reagent r)
-  if (!r.type) return false;
-  if (is_literal(r)) return false;
-  return r.type->value == get(Type_ordinal, "array");
+  return is_mu_array(r.type);
+}
+
+bool is_mu_array(const type_tree* type) {
+  if (!type) return false;
+  if (is_literal(type)) return false;
+  if (type->atom) return false;
+  assert(type->left->atom);
+  return type->left->value == get(Type_ordinal, "array");
 }
 
 bool is_mu_address(reagent/*copy*/ r) {
   // End Preprocess is_mu_address(reagent r)
-  if (!r.type) return false;
-  if (is_literal(r)) return false;
-  return r.type->value == get(Type_ordinal, "address");
+  return is_mu_address(r.type);
+}
+
+bool is_mu_address(const type_tree* type) {
+  if (!type) return false;
+  if (is_literal(type)) return false;
+  if (type->atom) return false;
+  assert(type->left->atom);
+  return type->left->value == get(Type_ordinal, "address");
 }
 
 bool is_mu_boolean(reagent/*copy*/ r) {
   // End Preprocess is_mu_boolean(reagent r)
   if (!r.type) return false;
   if (is_literal(r)) return false;
+  if (!r.type->atom) return false;
   return r.type->value == get(Type_ordinal, "boolean");
 }
 
 bool is_mu_number(reagent/*copy*/ r) {
   // End Preprocess is_mu_number(reagent r)
   if (!r.type) return false;
+  if (!r.type->atom) return false;
   if (is_literal(r)) {
-    if (!r.type) return false;
     return r.type->name == "literal-fractional-number"
         || r.type->name == "literal";
   }
@@ -205,15 +214,24 @@ bool is_mu_number(reagent/*copy*/ r) {
 
 bool is_mu_character(reagent/*copy*/ r) {
   // End Preprocess is_mu_character(reagent r)
-  if (!r.type) return false;
-  if (is_literal(r)) return false;
-  return r.type->value == get(Type_ordinal, "character");
+  return is_mu_character(r.type);
+}
+bool is_mu_character(const type_tree* type) {
+  if (!type) return false;
+  if (!type->atom) return false;
+  if (is_literal(type)) return false;
+  return type->value == get(Type_ordinal, "character");
 }
 
 bool is_mu_scalar(reagent/*copy*/ 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;
+  return is_mu_scalar(r.type);
+}
+
+bool is_mu_scalar(const type_tree* type) {
+  if (!type) return false;
+  if (is_mu_address(type)) return true;
+  if (!type->atom) return false;
+  if (is_literal(type))
+    return type->name != "literal-string";
+  return size_of(type) == 1;
 }
diff --git a/027call_ingredient.cc b/027call_ingredient.cc
index 1bc3a563..c9917614 100644
--- a/027call_ingredient.cc
+++ b/027call_ingredient.cc
@@ -176,10 +176,14 @@ case INGREDIENT: {
 bool is_mu_string(reagent/*copy*/ x) {
   // End Preprocess is_mu_string(reagent x)
   return x.type
-    && x.type->value == get(Type_ordinal, "address")
-    && x.type->right
-    && x.type->right->value == get(Type_ordinal, "array")
-    && x.type->right->right
-    && x.type->right->right->value == get(Type_ordinal, "character")
-    && x.type->right->right->right == NULL;
+      && !x.type->atom
+      && x.type->left->atom
+      && x.type->left->value == get(Type_ordinal, "address")
+      && x.type->right
+      && !x.type->right->atom
+      && x.type->right->left->atom
+      && x.type->right->left->value == get(Type_ordinal, "array")
+      && x.type->right->right
+      && x.type->right->right->atom
+      && x.type->right->right->value == get(Type_ordinal, "character");
 }
diff --git a/028call_reply.cc b/028call_reply.cc
index 507357c6..f5e61384 100644
--- a/028call_reply.cc
+++ b/028call_reply.cc
@@ -80,7 +80,7 @@ void check_types_of_reply_instructions(recipe_ordinal r) {
       for (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) {
+          if (!tmp || !tmp->atom) {
             raise << maybe(caller.name) << "'same-as-ingredient' metadata should take exactly one value in '" << to_original_string(reply_inst) << "'\n" << end();
             goto finish_reply_check;
           }
diff --git a/030container.cc b/030container.cc
index c24d6f7d..bb7ce08f 100644
--- a/030container.cc
+++ b/030container.cc
@@ -148,32 +148,41 @@ void clear_container_metadata() {
 if (r.metadata.size) return r.metadata.size;
 
 :(before "End size_of(type) Cases")
-if (type->value == -1) return 1;  // error value, but we'll raise it elsewhere
-if (type->value == 0) {
-  assert(!type->left && !type->right);
-  return 1;
+if (type->atom) {
+  if (type->value == -1) return 1;  // error value, but we'll raise it elsewhere
+  if (type->value == 0) return 1;
 }
-if (!contains_key(Type, type->value)) {
-  raise << "no such type " << type->value << '\n' << end();
+const type_tree* root = root_type(type);
+if (!contains_key(Type, root->value)) {
+  raise << "no such type " << root->value << '\n' << end();
   return 0;
 }
-type_info t = get(Type, type->value);
+type_info t = get(Type, root->value);
 if (t.kind == CONTAINER) {
   // Compute size_of Container
+  if (!contains_key(Container_metadata, type)) return 1;  // error raised elsewhere
   return get(Container_metadata, type).size;
 }
 
+:(code)
+const type_tree* root_type(const type_tree* t) {
+  const type_tree* result = t->atom ? t : t->left;
+  assert(result->atom);
+  return result;
+}
+
 //: precompute Container_metadata before we need size_of
 //: also store a copy in each reagent in each instruction in each recipe
-//: sometimes does unnecessary work for meaningless types
 
 :(after "Begin Instruction Modifying Transforms")  // needs to happen before transform_names, therefore after Type Modifying Transforms below
 Transform.push_back(compute_container_sizes);
 :(code)
 void compute_container_sizes(recipe_ordinal r) {
   recipe& caller = get(Recipe, r);
+  trace(9992, "transform") << "--- compute container sizes for " << caller.name << end();
   for (int i = 0; i < SIZE(caller.steps); ++i) {
     instruction& inst = caller.steps.at(i);
+    trace(9993, "transform") << "- compute container sizes for " << to_string(inst) << end();
     for (int i = 0; i < SIZE(inst.ingredients); ++i)
       compute_container_sizes(inst.ingredients.at(i));
     for (int i = 0; i < SIZE(inst.products); ++i)
@@ -185,37 +194,64 @@ void compute_container_sizes(reagent& r) {
   if (is_literal(r) || is_dummy(r)) return;
   reagent rcopy = r;
   // Compute Container Size(reagent rcopy)
-  set<type_ordinal> pending_metadata;
+  set<string> pending_metadata;
   compute_container_sizes(rcopy.type, pending_metadata);
   if (contains_key(Container_metadata, rcopy.type))
     r.metadata = get(Container_metadata, rcopy.type);
 }
 
-void compute_container_sizes(const type_tree* type, set<type_ordinal>& pending_metadata) {
+void compute_container_sizes(const type_tree* type, set<string>& pending_metadata) {
   if (!type) return;
-  if (contains_key(pending_metadata, type->value)) return;
-  if (type->value) pending_metadata.insert(type->value);
+  trace(9993, "transform") << "compute container sizes for " << to_string(type) << end();
   if (contains_key(Container_metadata, type)) return;
-  // might be needed by later layers, but we haven't found a need for it yet
-//?   if (type->left) compute_container_sizes(type->left, pending_metadata);
-  if (type->right) compute_container_sizes(type->right, pending_metadata);
+  if (contains_key(pending_metadata, names_to_string_without_quotes(type))) return;
+  pending_metadata.insert(names_to_string_without_quotes(type));
+//?   cerr << to_string(type) << '\n';
+  if (!type->atom) {
+    assert(type->left->atom);
+    if (type->left->name == "address") {
+//?       cerr << "  address\n";
+      compute_container_sizes(type->right, pending_metadata);
+    }
+    else if (type->left->name == "array") {
+      const type_tree* element_type = type->right;
+      // hack: support both array:number:3 and array:address:number
+//?       cerr << "  array\n";
+//?       cerr << "    " << to_string(type) << '\n';
+//?       cerr << "    " << to_string(element_type) << ' ' << element_type->atom << ' ' << element_type->right;
+//?       if (element_type->right)
+//?         cerr << " -- " << to_string(element_type->right) << ' ' << element_type->right->atom << ' ' << is_integer(element_type->right->name);
+//?       cerr << '\n';
+      if (!element_type->atom && element_type->right && element_type->right->atom && is_integer(element_type->right->name))
+        element_type = element_type->left;
+      compute_container_sizes(element_type, pending_metadata);
+    }
+    // End compute_container_sizes Non-atom Cases
+    return;
+  }
+  assert(type->atom);
   if (!contains_key(Type, type->value)) return;  // error raised elsewhere
   type_info& info = get(Type, type->value);
   if (info.kind == CONTAINER) {
-    // size of a container is the sum of the sizes of its element
-    // (So it can only contain arrays if they're static and include their
-    // length in the type.)
-    container_metadata metadata;
-    for (int i = 0; i < SIZE(info.elements); ++i) {
-      reagent/*copy*/ element = info.elements.at(i);
-      // Compute Container Size(element)
-      compute_container_sizes(element.type, pending_metadata);
-      metadata.offset.push_back(metadata.size);  // save previous size as offset
-      metadata.size += size_of(element.type);
-    }
-    Container_metadata.push_back(pair<type_tree*, container_metadata>(new type_tree(*type), metadata));
+    compute_container_sizes(info, type, pending_metadata);
+  }
+  // End compute_container_sizes Atom Cases
+}
+
+void compute_container_sizes(const type_info& container_info, const type_tree* full_type, set<string>& pending_metadata) {
+  assert(container_info.kind == CONTAINER);
+  // size of a container is the sum of the sizes of its element
+  // (So it can only contain arrays if they're static and include their
+  // length in the type.)
+  container_metadata metadata;
+  for (int i = 0; i < SIZE(container_info.elements); ++i) {
+    reagent/*copy*/ element = container_info.elements.at(i);
+    // Compute Container Size(element, full_type)
+    compute_container_sizes(element.type, pending_metadata);
+    metadata.offset.push_back(metadata.size);  // save previous size as offset
+    metadata.size += size_of(element.type);
   }
-  // End compute_container_sizes Cases
+  Container_metadata.push_back(pair<type_tree*, container_metadata>(new type_tree(*full_type), metadata));
 }
 
 container_metadata& get(vector<pair<type_tree*, container_metadata> >& all, const type_tree* key) {
@@ -239,7 +275,8 @@ bool contains_key(const vector<pair<type_tree*, container_metadata> >& all, cons
 bool matches(const type_tree* a, const type_tree* b) {
   if (a == b) return true;
   if (!a || !b) return false;
-  if (a->value != b->value) return false;
+  if (a->atom != b->atom) return false;
+  if (a->atom) return a->value == b->value;
   return matches(a->left, b->left) && matches(a->right, b->right);
 }
 
@@ -252,7 +289,122 @@ def main [
 ]
 +app: foo: 34 35 36
 
+//: for the following unit tests we'll do the work of the transform by hand
+
+:(before "End Unit Tests")
+void test_container_sizes() {
+  // a container we don't have the size for
+  reagent r("x:point");
+  CHECK(!contains_key(Container_metadata, r.type));
+  // scan
+  compute_container_sizes(r);
+  // the reagent we scanned knows its size
+  CHECK_EQ(r.metadata.size, 2);
+  // the global table also knows its size
+  CHECK(contains_key(Container_metadata, r.type));
+  CHECK_EQ(get(Container_metadata, r.type).size, 2);
+}
+
+void test_container_sizes_nested() {
+  // a container we don't have the size for
+  reagent r("x:point-number");
+  CHECK(!contains_key(Container_metadata, r.type));
+  // scan
+  compute_container_sizes(r);
+  // the reagent we scanned knows its size
+  CHECK_EQ(r.metadata.size, 3);
+  // the global table also knows its size
+  CHECK(contains_key(Container_metadata, r.type));
+  CHECK_EQ(get(Container_metadata, r.type).size, 3);
+}
+
+void test_container_sizes_recursive() {
+  // define a container containing an address to itself
+  run("container foo [\n"
+      "  x:number\n"
+      "  y:address:foo\n"
+      "]\n");
+  reagent r("x:foo");
+  compute_container_sizes(r);
+  CHECK_EQ(r.metadata.size, 2);
+}
+
+void test_container_sizes_from_address() {
+  // a container we don't have the size for
+  reagent container("x:point");
+  CHECK(!contains_key(Container_metadata, container.type));
+  // scanning an address to the container precomputes the size of the container
+  reagent r("x:address:point");
+  compute_container_sizes(r);
+  CHECK(contains_key(Container_metadata, container.type));
+  CHECK_EQ(get(Container_metadata, container.type).size, 2);
+}
+
+void test_container_sizes_from_array() {
+  // a container we don't have the size for
+  reagent container("x:point");
+  CHECK(!contains_key(Container_metadata, container.type));
+  // scanning an array of the container precomputes the size of the container
+  reagent r("x:array:point");
+  compute_container_sizes(r);
+  CHECK(contains_key(Container_metadata, container.type));
+  CHECK_EQ(get(Container_metadata, container.type).size, 2);
+}
+
+void test_container_sizes_from_address_to_array() {
+  // a container we don't have the size for
+  reagent container("x:point");
+  CHECK(!contains_key(Container_metadata, container.type));
+  // scanning an address to an array of the container precomputes the size of the container
+  reagent r("x:address:array:point");
+  compute_container_sizes(r);
+  CHECK(contains_key(Container_metadata, container.type));
+  CHECK_EQ(get(Container_metadata, container.type).size, 2);
+}
+
+void test_container_sizes_from_static_array() {
+  // a container we don't have the size for
+  reagent container("x:point");
+  int old_size = SIZE(Container_metadata);
+  // scanning an address to an array of the container precomputes the size of the container
+  reagent r("x:array:point:10");
+  compute_container_sizes(r);
+  CHECK(contains_key(Container_metadata, container.type));
+  CHECK_EQ(get(Container_metadata, container.type).size, 2);
+  // no non-container types precomputed
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+}
+
+void test_container_sizes_from_address_to_static_array() {
+  // a container we don't have the size for
+  reagent container("x:point");
+  int old_size = SIZE(Container_metadata);
+  // scanning an address to an array of the container precomputes the size of the container
+  reagent r("x:address:array:point:10");
+  compute_container_sizes(r);
+  CHECK(contains_key(Container_metadata, container.type));
+  CHECK_EQ(get(Container_metadata, container.type).size, 2);
+  // no non-container types precomputed
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+}
+
+void test_container_sizes_from_repeated_address_and_array_types() {
+  // a container we don't have the size for
+  reagent container("x:point");
+  int old_size = SIZE(Container_metadata);
+  // scanning repeated address and array types modifying the container precomputes the size of the container
+  reagent r("x:address:array:address:array:point:10");
+  compute_container_sizes(r);
+  CHECK(contains_key(Container_metadata, container.type));
+  CHECK_EQ(get(Container_metadata, container.type).size, 2);
+  // no non-container types precomputed
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+}
+
 //:: To access elements of a container, use 'get'
+//: 'get' takes a 'base' container and an 'offset' into it and returns the
+//: appropriate element of the container value.
+
 :(scenario get)
 def main [
   12:number <- copy 34
@@ -273,11 +425,15 @@ case GET: {
   }
   reagent/*copy*/ base = inst.ingredients.at(0);  // new copy for every invocation
   // Update GET base in Check
-  if (!base.type || !base.type->value || !contains_key(Type, base.type->value) || get(Type, base.type->value).kind != CONTAINER) {
+  if (!base.type) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'get' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  const type_tree* base_root_type = base.type->atom ? base.type : base.type->left;
+  if (!base_root_type->atom || base_root_type->value == 0 || !contains_key(Type, base_root_type->value) || get(Type, base_root_type->value).kind != CONTAINER) {
     raise << 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;
   const reagent& offset = inst.ingredients.at(1);
   if (!is_literal(offset) || !is_mu_scalar(offset)) {
     raise << maybe(get(Recipe, r).name) << "second ingredient of 'get' should have type 'offset', but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
@@ -288,14 +444,14 @@ case GET: {
     offset_value = to_integer(offset.name);
   else
     offset_value = offset.value;
-  if (offset_value < 0 || offset_value >= SIZE(get(Type, base_type).elements)) {
-    raise << maybe(get(Recipe, r).name) << "invalid offset '" << offset_value << "' for '" << get(Type, base_type).name << "'\n" << end();
+  if (offset_value < 0 || offset_value >= SIZE(get(Type, base_root_type->value).elements)) {
+    raise << maybe(get(Recipe, r).name) << "invalid offset '" << offset_value << "' for '" << get(Type, base_root_type->value).name << "'\n" << end();
     break;
   }
   if (inst.products.empty()) break;
   reagent/*copy*/ product = inst.products.at(0);
   // Update GET product in Check
-  const reagent/*copy*/ element = element_type(base.type, offset_value);
+  const reagent/*copy*/ element = element_type(base.type, offset_value);  // not just base_root_type because later layers will introduce compound types
   if (!types_coercible(product, element)) {
     raise << 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;
@@ -311,13 +467,13 @@ case GET: {
     raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
     break;
   }
-  type_ordinal base_type = base.type->value;
+  const type_tree* base_root_type = root_type(base.type);
   int offset = ingredients.at(1).at(0);
-  if (offset < 0 || offset >= SIZE(get(Type, base_type).elements)) break;  // copied from Check above
+  if (offset < 0 || offset >= SIZE(get(Type, base_root_type->value).elements)) break;  // copied from Check above
   assert(base.metadata.size);
   int src = base_address + base.metadata.offset.at(offset);
   trace(9998, "run") << "address to copy is " << src << end();
-  reagent/*copy*/ element = element_type(base.type, offset);
+  reagent/*copy*/ element = element_type(base.type, offset);  // not just base_root_type because later layers will introduce compound types
   element.set_value(src);
   trace(9998, "run") << "its type is " << names_to_string(element.type) << end();
   // Read element
@@ -328,9 +484,10 @@ case GET: {
 :(code)
 const reagent element_type(const type_tree* type, int offset_value) {
   assert(offset_value >= 0);
-  assert(contains_key(Type, type->value));
-  assert(!get(Type, type->value).name.empty());
-  const type_info& info = get(Type, type->value);
+  const type_tree* root = root_type(type);
+  assert(contains_key(Type, root->value));
+  assert(!get(Type, root->value).name.empty());
+  const type_info& info = get(Type, root->value);
   assert(info.kind == CONTAINER);
   if (offset_value >= SIZE(info.elements)) return reagent();  // error handled elsewhere
   reagent/*copy*/ element = info.elements.at(offset_value);
@@ -411,11 +568,15 @@ case PUT: {
   }
   reagent/*copy*/ base = inst.ingredients.at(0);
   // Update PUT base in Check
-  if (!base.type || !base.type->value || !contains_key(Type, base.type->value) || get(Type, base.type->value).kind != CONTAINER) {
+  if (!base.type) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'put' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  const type_tree* base_root_type = base.type->atom ? base.type : base.type->left;
+  if (!base_root_type->atom || base_root_type->value == 0 || !contains_key(Type, base_root_type->value) || get(Type, base_root_type->value).kind != CONTAINER) {
     raise << maybe(get(Recipe, r).name) << "first ingredient of 'put' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
     break;
   }
-  type_ordinal base_type = base.type->value;
   reagent/*copy*/ offset = inst.ingredients.at(1);
   // Update PUT offset in Check
   if (!is_literal(offset) || !is_mu_scalar(offset)) {
@@ -425,8 +586,8 @@ case PUT: {
   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 << maybe(get(Recipe, r).name) << "invalid offset '" << offset_value << "' for '" << get(Type, base_type).name << "'\n" << end();
+    if (offset_value < 0 || offset_value >= SIZE(get(Type, base_root_type->value).elements)) {
+      raise << maybe(get(Recipe, r).name) << "invalid offset '" << offset_value << "' for '" << get(Type, base_root_type->value).name << "'\n" << end();
       break;
     }
   }
@@ -434,7 +595,7 @@ case PUT: {
     offset_value = offset.value;
   }
   const reagent& value = inst.ingredients.at(2);
-  const reagent& element = element_type(base.type, offset_value);
+  const reagent& element = element_type(base.type, offset_value);  // not just base_root_type because later layers will introduce compound types
   if (!types_coercible(element, value)) {
     raise << maybe(get(Recipe, r).name) << "'put " << base.original_string << ", " << offset.original_string << "' should write to " << names_to_string_without_quotes(element.type) << " but '" << value.name << "' has type " << names_to_string_without_quotes(value.type) << '\n' << end();
     break;
@@ -456,9 +617,9 @@ case PUT: {
     raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
     break;
   }
-  type_ordinal base_type = base.type->value;
+  const type_tree* base_root_type = root_type(base.type);
   int offset = ingredients.at(1).at(0);
-  if (offset < 0 || offset >= SIZE(get(Type, base_type).elements)) break;  // copied from Check above
+  if (offset < 0 || offset >= SIZE(get(Type, base_root_type->value).elements)) break;  // copied from Check above
   int address = base_address + base.metadata.offset.at(offset);
   trace(9998, "run") << "address to copy to is " << address << end();
   // optimization: directly write the element rather than updating 'product'
@@ -592,21 +753,23 @@ void insert_container(const string& command, kind_of_type kind, istream& in) {
 
 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;
-    }
-    // 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);
-    }
+  if (!type->atom) {
+    replace_unknown_types_with_unique_ordinals(type->left, info);
+    replace_unknown_types_with_unique_ordinals(type->right, info);
+    return;
+  }
+  assert(!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;
+  }
+  // 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) {
@@ -687,6 +850,11 @@ void check_or_set_invalid_types(const recipe_ordinal r) {
 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->atom) {
+    check_or_set_invalid_types(type->left, block, name);
+    check_or_set_invalid_types(type->right, block, name);
+    return;
+  }
   if (type->value == 0) return;
   if (!contains_key(Type, type->value)) {
     assert(!type->name.empty());
@@ -695,8 +863,6 @@ void check_or_set_invalid_types(type_tree* type, const string& block, const stri
     else
       raise << 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);
 }
 
 :(scenario container_unknown_field)
@@ -738,10 +904,13 @@ void check_container_field_types() {
 
 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->atom) {
+    check_invalid_types(type->left, block, name);
+    check_invalid_types(type->right, block, name);
+    return;
+  }
   if (type->value != 0) {  // value 0 = compound types (layer parse_tree) or type ingredients (layer shape_shifting_container)
     if (!contains_key(Type, type->value))
       raise << block << "unknown type in " << name << '\n' << end();
   }
-  check_invalid_types(type->left, block, name);
-  check_invalid_types(type->right, block, name);
 }
diff --git a/031merge.cc b/031merge.cc
index 21c036c8..f8f07c1a 100644
--- a/031merge.cc
+++ b/031merge.cc
@@ -124,12 +124,13 @@ void check_merge_calls(const recipe_ordinal r) {
     }
     reagent/*copy*/ product = inst.products.at(0);
     // Update product While Type-checking Merge
-    type_ordinal product_type = product.type->value;
-    if (product_type == 0 || !contains_key(Type, product_type)) {
+    const type_tree* product_base_type = product.type->atom ? product.type : product.type->left;
+    assert(product_base_type->atom);
+    if (product_base_type->value == 0 || !contains_key(Type, product_base_type->value)) {
       raise << maybe(caller.name) << "'merge' should yield a container in '" << inst.original_string << "'\n" << end();
       continue;
     }
-    const type_info& info = get(Type, product_type);
+    const type_info& info = get(Type, product_base_type->value);
     if (info.kind != CONTAINER && info.kind != EXCLUSIVE_CONTAINER) {
       raise << maybe(caller.name) << "'merge' should yield a container in '" << inst.original_string << "'\n" << end();
       continue;
@@ -151,7 +152,9 @@ void check_merge_call(const vector<reagent>& ingredients, const reagent& product
     }
     reagent& container = state.data.top().container;
     if (!container.type) return;  // error handled elsewhere
-    type_info& container_info = get(Type, container.type->value);
+    const type_tree* top_root_type = container.type->atom ? container.type : container.type->left;
+    assert(top_root_type->atom);
+    type_info& container_info = get(Type, top_root_type->value);
     switch (container_info.kind) {
       case CONTAINER: {
         // degenerate case: merge with the same type always succeeds
@@ -163,7 +166,7 @@ void check_merge_call(const vector<reagent>& ingredients, const reagent& product
         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)) {
+          while (state.data.top().container_element_index >= SIZE(get(Type, root_type(state.data.top().container.type)->value).elements)) {
             state.data.pop();
             if (state.data.empty()) {
               if (ingredient_index < SIZE(ingredients))
@@ -198,7 +201,7 @@ void check_merge_call(const vector<reagent>& ingredients, const reagent& product
             return;
           }
           ++state.data.top().container_element_index;
-        } while (state.data.top().container_element_index >= SIZE(get(Type, state.data.top().container.type->value).elements));
+        } while (state.data.top().container_element_index >= SIZE(get(Type, root_type(state.data.top().container.type)->value).elements));
       }
     }
   }
@@ -216,4 +219,3 @@ def main [
 :(before "End Includes")
 #include <stack>
 using std::stack;
-
diff --git a/032array.cc b/032array.cc
index 0cb27bd3..f8c26234 100644
--- a/032array.cc
+++ b/032array.cc
@@ -34,11 +34,14 @@ case CREATE_ARRAY: {
     break;
   }
   // 'create-array' will need to check properties rather than types
-  if (!product.type->right->right) {
+  type_tree* array_length_from_type = product.type->right->right;
+  if (!array_length_from_type) {
     raise << maybe(get(Recipe, r).name) << "create array of what size? '" << inst.original_string << "'\n" << end();
     break;
   }
-  if (!is_integer(product.type->right->right->name)) {
+  if (!product.type->right->right->atom)
+    array_length_from_type = array_length_from_type->left;
+  if (!is_integer(array_length_from_type->name)) {
     raise << 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;
   }
@@ -49,7 +52,10 @@ case CREATE_ARRAY: {
   reagent/*copy*/ product = current_instruction().products.at(0);
   // Update CREATE_ARRAY product in Run
   int base_address = product.value;
-  int array_length = to_integer(product.type->right->right->name);
+  type_tree* array_length_from_type = product.type->right->right;
+  if (!product.type->right->right->atom)
+    array_length_from_type = array_length_from_type->left;
+  int array_length = to_integer(array_length_from_type->name);
   // initialize array length, so that size_of will work
   trace(9999, "mem") << "storing " << array_length << " in location " << base_address << end();
   put(Memory, base_address, array_length);  // in array elements
@@ -93,7 +99,7 @@ def main [
 +app: foo: 3 14 15 16
 
 :(before "End size_of(reagent r) Cases")
-if (r.type && r.type->value == get(Type_ordinal, "array")) {
+if (!r.type->atom && r.type->left->atom && r.type->left->value == get(Type_ordinal, "array")) {
   if (!r.type->right) {
     raise << maybe(current_recipe_name()) << "'" << r.original_string << "' is an array of what?\n" << end();
     return 1;
@@ -107,7 +113,7 @@ if (r.type && r.type->value == get(Type_ordinal, "array")) {
 //: disable the size mismatch check for arrays since the destination array
 //: need not be initialized
 :(before "End size_mismatch(x) Cases")
-if (x.type && x.type->value == get(Type_ordinal, "array")) return false;
+if (x.type && !x.type->atom && x.type->left->value == get(Type_ordinal, "array")) return false;
 
 //: arrays are disallowed inside containers unless their length is fixed in
 //: advance
@@ -147,12 +153,16 @@ def main [
 :(before "End Load Container Element Definition")
 {
   const type_tree* type = info.elements.back().type;
-  if (type && type->name == "array") {
+  if (type && type->atom && type->name == "array") {
+    raise << "container '" << name << "' doesn't specify type of array elements for '" << info.elements.back().name << "'\n" << end();
+    continue;
+  }
+  if (type && !type->atom && type->left->atom && type->left->name == "array") {
     if (!type->right) {
       raise << "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
+    if (type->right->atom) {  // array has no length
       raise << "container '" << name << "' cannot determine size of element '" << info.elements.back().name << "'\n" << end();
       continue;
     }
@@ -182,6 +192,16 @@ def main [
 ]
 +mem: storing 14 in location 5
 
+:(scenario index_compound_element)
+def main [
+  {1: (array (address number) 3)} <- create-array
+  2:number <- copy 14
+  3:number <- copy 15
+  4:number <- copy 16
+  5:address:number <- index {1: (array (address number) 3)}, 0
+]
++mem: storing 14 in location 5
+
 :(scenario index_direct_offset)
 def main [
   1:array:number:3 <- create-array
@@ -246,7 +266,7 @@ case INDEX: {
   type_tree* element_type = copy_array_element(base.type);
   int src = base_address + 1 + index_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();
+  trace(9998, "run") << "its type is " << to_string(element_type) << end();
   reagent element;
   element.set_value(src);
   element.type = element_type;
@@ -257,19 +277,15 @@ case INDEX: {
 
 :(code)
 type_tree* copy_array_element(const type_tree* type) {
-  if (type->right->left) {
-    assert(!type->right->left->left);
-    return new type_tree(*type->right->left);
-  }
   assert(type->right);
-  // array:<type>:<size>? return just <type>
-  if (type->right->right && is_integer(type->right->right->name))
-    return new type_tree(type->right->name, type->right->value);  // snip type->right->right
+  // hack: don't require parens for either array:number:3 array:address:number
+  if (!type->right->atom && type->right->right && type->right->right->atom && is_integer(type->right->right->name))
+    return new type_tree(*type->right->left);
   return new type_tree(*type->right);
 }
 
 int array_length(const reagent& x) {
-  if (x.type->right->right && !x.type->right->right->right  // exactly 3 types
+  if (!x.type->atom && !x.type->right->atom && x.type->right->right->atom  // exactly 3 types
       && is_integer(x.type->right->right->name)) {  // third 'type' is a number
     // get size from type
     return to_integer(x.type->right->right->name);
diff --git a/033exclusive_container.cc b/033exclusive_container.cc
index ff384de5..77968966 100644
--- a/033exclusive_container.cc
+++ b/033exclusive_container.cc
@@ -35,22 +35,27 @@ if (t.kind == EXCLUSIVE_CONTAINER) {
   // Compute size_of Exclusive Container
   return get(Container_metadata, type).size;
 }
-:(before "End compute_container_sizes Cases")
+:(before "End compute_container_sizes Atom Cases")
 if (info.kind == EXCLUSIVE_CONTAINER) {
+  compute_exclusive_container_sizes(info, type, pending_metadata);
+}
+
+:(code)
+void compute_exclusive_container_sizes(const type_info& exclusive_container_info, const type_tree* full_type, set<string>& pending_metadata) {
   // size of an exclusive container is the size of its largest variant
   // (So, like containers, it can only contain arrays if they're static and
   // include their length in the type.)
   container_metadata metadata;
-  for (int i = 0; i < SIZE(info.elements); ++i) {
-    reagent/*copy*/ element = info.elements.at(i);
-    // Compute Exclusive Container Size(element)
+  for (int i = 0; i < SIZE(exclusive_container_info.elements); ++i) {
+    reagent/*copy*/ element = exclusive_container_info.elements.at(i);
+    // Compute Exclusive Container Size(element, full_type)
     compute_container_sizes(element.type, pending_metadata);
     int variant_size = size_of(element);
     if (variant_size > metadata.size) metadata.size = variant_size;
   }
   // ...+1 for its tag.
   ++metadata.size;
-  Container_metadata.push_back(pair<type_tree*, container_metadata>(new type_tree(*type), metadata));
+  Container_metadata.push_back(pair<type_tree*, container_metadata>(new type_tree(*full_type), metadata));
 }
 
 //:: To access variants of an exclusive container, use 'maybe-convert'.
@@ -100,7 +105,12 @@ case MAYBE_CONVERT: {
   }
   reagent/*copy*/ base = inst.ingredients.at(0);
   // Update MAYBE_CONVERT base in Check
-  if (!base.type || !base.type->value || get(Type, base.type->value).kind != EXCLUSIVE_CONTAINER) {
+  if (!base.type) {
+    raise << maybe(caller.name) << "first ingredient of 'maybe-convert' should be an exclusive-container, but got '" << base.original_string << "'\n" << end();
+    break;
+  }
+  const type_tree* root_type = base.type->atom ? base.type : base.type->left;
+  if (!root_type->atom || root_type->value == 0 || !contains_key(Type, root_type->value) || get(Type, root_type->value).kind != EXCLUSIVE_CONTAINER) {
     raise << maybe(caller.name) << "first ingredient of 'maybe-convert' should be an exclusive-container, but got '" << base.original_string << "'\n" << end();
     break;
   }
@@ -117,7 +127,7 @@ case MAYBE_CONVERT: {
   // Update MAYBE_CONVERT product in Check
   reagent& offset = inst.ingredients.at(1);
   populate_value(offset);
-  if (offset.value >= SIZE(get(Type, base.type->value).elements)) {
+  if (offset.value >= SIZE(get(Type, root_type->value).elements)) {
     raise << maybe(caller.name) << "invalid tag " << offset.value << " in '" << inst.original_string << '\n' << end();
     break;
   }
@@ -176,9 +186,10 @@ const reagent variant_type(const reagent& base, int tag) {
 
 const reagent variant_type(const type_tree* type, int tag) {
   assert(tag >= 0);
-  assert(contains_key(Type, type->value));
-  assert(!get(Type, type->value).name.empty());
-  const type_info& info = get(Type, type->value);
+  const type_tree* root_type = type->atom ? type : type->left;
+  assert(contains_key(Type, root_type->value));
+  assert(!get(Type, root_type->value).name.empty());
+  const type_info& info = get(Type, root_type->value);
   assert(info.kind == EXCLUSIVE_CONTAINER);
   reagent/*copy*/ element = info.elements.at(tag);
   // End variant_type Special-cases
@@ -430,7 +441,9 @@ if (current_step_index() < SIZE(Current_routine->steps())
     && current_instruction().products.at(0).type) {
   reagent/*copy*/ x = current_instruction().products.at(0);
   // Update size_mismatch Check for MERGE(x)
-  if (get(Type, x.type->value).kind == EXCLUSIVE_CONTAINER)
+  const type_tree* root_type = x.type->atom ? x.type : x.type->left;
+  assert(root_type->atom);
+  if (get(Type, root_type->value).kind == EXCLUSIVE_CONTAINER)
     return size_of(x) < SIZE(data);
 }
 
diff --git a/034address.cc b/034address.cc
index aa47cc1c..efa53c72 100644
--- a/034address.cc
+++ b/034address.cc
@@ -174,12 +174,13 @@ case NEW: {
 bool product_of_new_is_valid(const instruction& inst) {
   reagent/*copy*/ product = inst.products.at(0);
   // Update NEW product in Check
-  if (!product.type || product.type->value != get(Type_ordinal, "address"))
+  if (!product.type || product.type->atom || product.type->left->value != get(Type_ordinal, "address"))
     return false;
   drop_from_type(product, "address");
   if (SIZE(inst.ingredients) > 1) {
     // array allocation
-    if (!product.type || product.type->value != get(Type_ordinal, "array")) return false;
+    if (!product.type || product.type->atom || product.type->left->value != get(Type_ordinal, "array"))
+      return false;
     drop_from_type(product, "array");
   }
   reagent/*copy*/ expected_product("x:"+inst.ingredients.at(0).name);
@@ -193,7 +194,8 @@ bool product_of_new_is_valid(const instruction& inst) {
 }
 
 void drop_from_type(reagent& r, string expected_type) {
-  if (r.type->name != expected_type) {
+  assert(!r.type->atom);
+  if (r.type->left->name != expected_type) {
     raise << "can't drop2 " << expected_type << " from '" << to_string(r) << "'\n" << end();
     return;
   }
diff --git a/035lookup.cc b/035lookup.cc
index 91314aee..7dbf301b 100644
--- a/035lookup.cc
+++ b/035lookup.cc
@@ -76,7 +76,7 @@ void canonize(reagent& x) {
 }
 
 void lookup_memory(reagent& x) {
-  if (!x.type || x.type->value != get(Type_ordinal, "address")) {
+  if (!x.type || x.type->atom || x.type->left->value != get(Type_ordinal, "address")) {
     raise << maybe(current_recipe_name()) << "tried to /lookup '" << x.original_string << "' but it isn't an address\n" << end();
     return;
   }
@@ -149,15 +149,15 @@ canonize_type(rhs);
 :(before "Compute Container Size(reagent rcopy)")
 if (!canonize_type(rcopy)) return;
 
-:(before "Compute Container Size(element)")
+:(before "Compute Container Size(element, full_type)")
 assert(!has_property(element, "lookup"));
-:(before "Compute Exclusive Container Size(element)")
+:(before "Compute Exclusive Container Size(element, full_type)")
 assert(!has_property(element, "lookup"));
 
 :(code)
 bool canonize_type(reagent& r) {
   while (has_property(r, "lookup")) {
-    if (!r.type || r.type->value != get(Type_ordinal, "address")) {
+    if (!r.type || r.type->atom || !r.type->left || !r.type->left->atom || r.type->left->value != get(Type_ordinal, "address")) {
       raise << "can't lookup non-address: '" << to_string(r) << "': '" << to_string(r.type) << "'\n" << end();
       return false;
     }
diff --git a/036refcount.cc b/036refcount.cc
index 05ddf47e..c983290f 100644
--- a/036refcount.cc
+++ b/036refcount.cc
@@ -279,7 +279,6 @@ bool operator<(const address_element_info& a, const address_element_info& b) {
 
 //: populate metadata.address in a separate transform, because it requires
 //: already knowing the sizes of all types
-//: sometimes does unnecessary work for meaningless types
 
 :(after "Transform.push_back(compute_container_sizes)")
 Transform.push_back(compute_container_address_offsets);
@@ -296,46 +295,70 @@ void compute_container_address_offsets(const recipe_ordinal r) {
       compute_container_address_offsets(inst.products.at(i));
   }
 }
+
 void compute_container_address_offsets(reagent& r) {
   if (is_literal(r) || is_dummy(r)) return;
   compute_container_address_offsets(r.type);
   if (contains_key(Container_metadata, r.type))
     r.metadata = get(Container_metadata, r.type);
 }
-void compute_container_address_offsets(type_tree* type) {
+
+// the recursive structure of this function needs to exactly match
+// compute_container_sizes
+void compute_container_address_offsets(const type_tree* type) {
   if (!type) return;
-  // might be needed by later layers, but we haven't found a need for it yet
-//?   if (type->left) compute_container_address_offsets(type->left);
-  if (type->right) compute_container_address_offsets(type->right);
-  if (!contains_key(Type, type->value)) return;  // error raised elsewhere
-  type_info& info = get(Type, type->value);
+  if (!type->atom) {
+    assert(type->left->atom);
+    if (type->left->name == "address") {
+      compute_container_address_offsets(type->right);
+    }
+    else if (type->left->name == "array") {
+      const type_tree* element_type = type->right;
+      // hack: support both array:number:3 and array:address:number
+      if (!element_type->atom && element_type->right && element_type->right->atom && is_integer(element_type->right->name))
+        element_type = element_type->left;
+      compute_container_address_offsets(element_type);
+    }
+    // End compute_container_address_offsets Non-atom Cases
+  }
+  if (!contains_key(Type, root_type(type)->value)) return;  // error raised elsewhere
+  type_info& info = get(Type, root_type(type)->value);
   if (info.kind == CONTAINER) {
-    container_metadata& metadata = get(Container_metadata, type);
-    if (!metadata.address.empty()) return;
-    trace(9994, "transform") << "compute address offsets for container " << info.name << end();
-    append_addresses(0, type, metadata.address, set<tag_condition_info>());
+    compute_container_address_offsets(info, type);
   }
   if (info.kind == EXCLUSIVE_CONTAINER) {
-    container_metadata& metadata = get(Container_metadata, type);
-    trace(9994, "transform") << "compute address offsets for exclusive container " << info.name << end();
-    for (int tag = 0; tag < SIZE(info.elements); ++tag) {
-      set<tag_condition_info> key;
-      key.insert(tag_condition_info(/*tag is at offset*/0, tag));
-      append_addresses(/*skip tag offset*/1, variant_type(type, tag).type, metadata.address, key);
-    }
+    compute_exclusive_container_address_offsets(info, type);
+  }
+}
+
+void compute_container_address_offsets(const type_info& container_info, const type_tree* full_type) {
+  container_metadata& metadata = get(Container_metadata, full_type);
+  if (!metadata.address.empty()) return;
+  trace(9994, "transform") << "compute address offsets for container " << container_info.name << end();
+  append_addresses(0, full_type, metadata.address, set<tag_condition_info>());
+}
+
+void compute_exclusive_container_address_offsets(const type_info& exclusive_container_info, const type_tree* full_type) {
+  container_metadata& metadata = get(Container_metadata, full_type);
+  trace(9994, "transform") << "compute address offsets for exclusive container " << exclusive_container_info.name << end();
+  for (int tag = 0; tag < SIZE(exclusive_container_info.elements); ++tag) {
+    set<tag_condition_info> key;
+    key.insert(tag_condition_info(/*tag is at offset*/0, tag));
+    append_addresses(/*skip tag offset*/1, variant_type(full_type, tag).type, metadata.address, key);
   }
 }
 
 void append_addresses(int base_offset, const type_tree* type, map<set<tag_condition_info>, set<address_element_info> >& out, const set<tag_condition_info>& key) {
-  const type_info& info = get(Type, type->value);
-  if (type->name == "address") {
+  if (is_mu_address(type)) {
     get_or_insert(out, key).insert(address_element_info(base_offset, new type_tree(*type->right)));
     return;
   }
+  const type_tree* root = root_type(type);
+  const type_info& info = get(Type, root->value);
   if (info.kind == CONTAINER) {
     for (int curr_index = 0, curr_offset = base_offset; curr_index < SIZE(info.elements); ++curr_index) {
-      trace(9993, "transform") << "checking container " << type->name << ", element " << curr_index << end();
-      reagent/*copy*/ element = element_type(type, curr_index);
+      trace(9993, "transform") << "checking container " << root->name << ", element " << curr_index << end();
+      reagent/*copy*/ element = element_type(type, curr_index);  // not root
       // Compute Container Address Offset(element)
       if (is_mu_address(element)) {
         trace(9993, "transform") << "address at offset " << curr_offset << end();
@@ -347,7 +370,8 @@ void append_addresses(int base_offset, const type_tree* type, map<set<tag_condit
         curr_offset += size_of(element);
       }
       else if (is_mu_exclusive_container(element)) {
-        const type_info& element_info = get(Type, element.type->value);
+        const type_tree* element_root_type = root_type(element.type);
+        const type_info& element_info = get(Type, element_root_type->value);
         for (int tag = 0; tag < SIZE(element_info.elements); ++tag) {
           set<tag_condition_info> new_key = key;
           new_key.insert(tag_condition_info(curr_offset, tag));
@@ -378,6 +402,280 @@ int payload_size(const type_tree* type) {
   return size_of(type->right) + /*refcount*/1;
 }
 
+//: for the following unit tests we'll do the work of the transform by hand
+
+:(before "End Unit Tests")
+void test_container_address_offsets_empty() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with no addresses
+  reagent r("x:point");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // scan
+  compute_container_address_offsets(r);
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // the reagent we scanned knows it has no addresses
+  CHECK(r.metadata.address.empty());
+  // the global table contains an identical entry
+  CHECK(contains_key(Container_metadata, r.type));
+  CHECK(get(Container_metadata, r.type).address.empty());
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+}
+
+void test_container_address_offsets() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with an address at offset 0 that we have the size for
+  run("container foo [\n"
+      "  x:address:number\n"
+      "]\n");
+  reagent r("x:foo");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // scan
+  compute_container_address_offsets(r);
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // the reagent we scanned knows it has an address at offset 0
+  CHECK_EQ(SIZE(r.metadata.address), 1);
+  CHECK(contains_key(r.metadata.address, set<tag_condition_info>()));
+  const set<address_element_info>& address_offsets = get(r.metadata.address, set<tag_condition_info>());  // unconditional for containers
+  CHECK_EQ(SIZE(address_offsets), 1);
+  CHECK_EQ(address_offsets.begin()->offset, 0);
+  CHECK(address_offsets.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets.begin()->payload_type->name, "number");
+  // the global table contains an identical entry
+  CHECK(contains_key(Container_metadata, r.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, r.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 0);
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+}
+
+void test_container_address_offsets_2() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with an address at offset 1 that we have the size for
+  run("container foo [\n"
+      "  x:number\n"
+      "  y:address:number\n"
+      "]\n");
+  reagent r("x:foo");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scan
+  compute_container_address_offsets(r);
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // the reagent we scanned knows it has an address at offset 1
+  CHECK_EQ(SIZE(r.metadata.address), 1);
+  CHECK(contains_key(r.metadata.address, set<tag_condition_info>()));
+  const set<address_element_info>& address_offsets = get(r.metadata.address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets), 1);
+  CHECK_EQ(address_offsets.begin()->offset, 1);  //
+  CHECK(address_offsets.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets.begin()->payload_type->name, "number");
+  // the global table contains an identical entry
+  CHECK(contains_key(Container_metadata, r.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, r.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 1);  //
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+}
+
+void test_container_address_offsets_nested() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with a nested container containing an address
+  run("container foo [\n"
+      "  x:address:number\n"
+      "  y:number\n"
+      "]\n"
+      "container bar [\n"
+      "  p:point\n"
+      "  f:foo\n"  // nested container containing address
+      "]\n");
+  reagent r("x:bar");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // global metadata contains entries for bar and included types: point and foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 3);
+  // scan
+  compute_container_address_offsets(r);
+  // the reagent we scanned knows it has an address at offset 2
+  CHECK_EQ(SIZE(r.metadata.address), 1);
+  CHECK(contains_key(r.metadata.address, set<tag_condition_info>()));
+  const set<address_element_info>& address_offsets = get(r.metadata.address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets), 1);
+  CHECK_EQ(address_offsets.begin()->offset, 2);  //
+  CHECK(address_offsets.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets.begin()->payload_type->name, "number");
+  // the global table also knows its address offset
+  CHECK(contains_key(Container_metadata, r.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, r.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 2);  //
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 3);
+}
+
+void test_container_address_offsets_from_address() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with an address at offset 0
+  run("container foo [\n"
+      "  x:address:number\n"
+      "]\n");
+  reagent r("x:address:foo");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scan an address to the container
+  compute_container_address_offsets(r);
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scanning precomputed metadata for the container
+  reagent container("x:foo");
+  CHECK(contains_key(Container_metadata, container.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, container.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 0);
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+}
+
+void test_container_address_offsets_from_array() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with an address at offset 0
+  run("container foo [\n"
+      "  x:address:number\n"
+      "]\n");
+  reagent r("x:array:foo");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scan an array of the container
+  compute_container_address_offsets(r);
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scanning precomputed metadata for the container
+  reagent container("x:foo");
+  CHECK(contains_key(Container_metadata, container.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, container.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 0);
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+}
+
+void test_container_address_offsets_from_address_to_array() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with an address at offset 0
+  run("container foo [\n"
+      "  x:address:number\n"
+      "]\n");
+  reagent r("x:address:array:foo");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scan an address to an array of the container
+  compute_container_address_offsets(r);
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scanning precomputed metadata for the container
+  reagent container("x:foo");
+  CHECK(contains_key(Container_metadata, container.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, container.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 0);
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+}
+
+void test_container_address_offsets_from_static_array() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with an address at offset 0
+  run("container foo [\n"
+      "  x:address:number\n"
+      "]\n");
+  reagent r("x:array:foo:10");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scan a static array of the container
+  compute_container_address_offsets(r);
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scanning precomputed metadata for the container
+  reagent container("x:foo");
+  CHECK(contains_key(Container_metadata, container.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, container.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 0);
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+}
+
+void test_container_address_offsets_from_address_to_static_array() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with an address at offset 0
+  run("container foo [\n"
+      "  x:address:number\n"
+      "]\n");
+  reagent r("x:address:array:foo:10");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scan an address to a static array of the container
+  compute_container_address_offsets(r);
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scanning precomputed metadata for the container
+  reagent container("x:foo");
+  CHECK(contains_key(Container_metadata, container.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, container.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 0);
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+}
+
+void test_container_address_offsets_from_repeated_address_and_array_types() {
+  int old_size = SIZE(Container_metadata);
+  // define a container with an address at offset 0
+  run("container foo [\n"
+      "  x:address:number\n"
+      "]\n");
+  // scan a deep nest of 'address' and 'array' types modifying a container
+  reagent r("x:address:array:address:address:array:foo:10");
+  compute_container_sizes(r);  // need to first pre-populate the metadata
+  // global metadata contains just the entry for foo
+  // no entries for non-container types or other junk
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  compute_container_address_offsets(r);
+  // compute_container_address_offsets creates no new entries
+  CHECK_EQ(SIZE(Container_metadata)-old_size, 1);
+  // scanning precomputed metadata for the container
+  reagent container("x:foo");
+  CHECK(contains_key(Container_metadata, container.type));
+  const set<address_element_info>& address_offsets2 = get(get(Container_metadata, container.type).address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(address_offsets2), 1);
+  CHECK_EQ(address_offsets2.begin()->offset, 0);
+  CHECK(address_offsets2.begin()->payload_type->atom);
+  CHECK_EQ(address_offsets2.begin()->payload_type->name, "number");
+}
+
 //: use metadata.address to update refcounts within containers, arrays and
 //: exclusive containers
 
@@ -648,6 +946,7 @@ bool is_mu_container(const reagent& r) {
 }
 bool is_mu_container(const type_tree* type) {
   if (!type) return false;
+  // End is_mu_container(type) Special-cases
   if (type->value == 0) return false;
   type_info& info = get(Type, type->value);
   return info.kind == CONTAINER;
@@ -658,6 +957,7 @@ bool is_mu_exclusive_container(const reagent& r) {
 }
 bool is_mu_exclusive_container(const type_tree* type) {
   if (!type) return false;
+  // End is_mu_exclusive_container(type) Special-cases
   if (type->value == 0) return false;
   type_info& info = get(Type, type->value);
   return info.kind == EXCLUSIVE_CONTAINER;
diff --git a/037abandon.cc b/037abandon.cc
index d34a493b..4c8121fb 100644
--- a/037abandon.cc
+++ b/037abandon.cc
@@ -31,7 +31,7 @@ void abandon(int address, const type_tree* payload_type, int payload_size) {
 //?   ++Num_free;
 //?   cerr << "abandon: " << size << '\n';
   // decrement any contained refcounts
-  if (payload_type->name == "array") {
+  if (is_mu_array(payload_type)) {
     reagent element;
     element.type = copy_array_element(payload_type);
     int array_length = get_or_insert(Memory, address+/*skip refcount*/1);
diff --git a/039location_array.cc b/039location_array.cc
index 814fb922..e43717a5 100644
--- a/039location_array.cc
+++ b/039location_array.cc
@@ -12,15 +12,21 @@ case TO_LOCATION_ARRAY: {
   break;
 }
 :(code)
-bool is_address_of_array_of_numbers(reagent/*copy*/ 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, "array")) return false;
-  drop_from_type(product, "array");
-  if (!product.type || product.type->value != get(Type_ordinal, "number")) return false;
-  return true;
+bool is_address_of_array_of_numbers(reagent/*copy*/ x) {
+  canonize_type(x);
+  if (!is_compound_type_starting_with(x.type, "address")) return false;
+  drop_from_type(x, "address");
+  if (!is_compound_type_starting_with(x.type, "array")) return false;
+  drop_from_type(x, "array");
+  return x.type && x.type->atom && x.type->value == get(Type_ordinal, "number");
 }
+bool is_compound_type_starting_with(const type_tree* type, const string& expected_name) {
+  if (!type) return false;
+  if (type->atom) return false;
+  if (!type->left->atom) return false;
+  return type->left->value == get(Type_ordinal, expected_name);
+}
+
 :(before "End Primitive Recipe Implementations")
 case TO_LOCATION_ARRAY: {
   int array_size = SIZE(ingredients.at(0));
diff --git a/042name.cc b/042name.cc
index 2711b94d..c3e9e0ef 100644
--- a/042name.cc
+++ b/042name.cc
@@ -117,12 +117,10 @@ int lookup_name(const reagent& r, const recipe_ordinal default_recipe) {
 }
 
 type_ordinal skip_addresses(type_tree* type) {
-  type_ordinal address = get(Type_ordinal, "address");
-  for (; type; type = type->right) {
-    if (type->value != address)
-      return type->value;
-  }
-  return -1;
+  while (type && is_compound_type_starting_with(type, "address"))
+    type = type->right;
+  if (!type) return -1;  // error handled elsewhere
+  return root_type(type)->value;
 }
 
 int find_element_name(const type_ordinal t, const string& name, const string& recipe_name) {
diff --git a/043space.cc b/043space.cc
index 15325c3c..1c26cc2f 100644
--- a/043space.cc
+++ b/043space.cc
@@ -83,19 +83,15 @@ int address(int offset, int base) {
 
 :(after "Begin Preprocess write_memory(x, data)")
 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, "array")
-      || !x.type->right->right
-      || x.type->right->right->value != get(Type_ordinal, "location")
-      || x.type->right->right->right) {
+  if (!scalar(data) || !is_space(x))
     raise << maybe(current_recipe_name()) << "'default-space' should be of type address:array:location, but is " << to_string(x.type) << '\n' << end();
-  }
   current_call().default_space = data.at(0);
   return;
 }
+:(code)
+bool is_space(const reagent& r) {
+  return is_address_of_array_of_numbers(r);
+}
 
 :(scenario get_default_space)
 def main [
diff --git a/045closure_name.cc b/045closure_name.cc
index 77389b3a..b14030a9 100644
--- a/045closure_name.cc
+++ b/045closure_name.cc
@@ -45,14 +45,7 @@ void collect_surrounding_spaces(const recipe_ordinal r) {
     for (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, "array")
-          || !type->right->right
-          || type->right->right->value != get(Type_ordinal, "location")
-          || type->right->right->right) {
+      if (!is_space(inst.products.at(j))) {
         raise << "slot 0 should always have type address:array:location, but is '" << to_string(inst.products.at(j)) << "'\n" << end();
         continue;
       }
@@ -61,7 +54,7 @@ void collect_surrounding_spaces(const recipe_ordinal r) {
         raise << "slot 0 requires a /names property in recipe '" << get(Recipe, r).name << "'\n" << end();
         continue;
       }
-      if (s->right) raise << "slot 0 should have a single value in /names, but got '" << to_string(inst.products.at(j)) << "'\n" << end();
+      if (!s->atom) raise << "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 << "slot 0 doesn't initialize its /names property in recipe '" << get(Recipe, r).name << "'\n" << end();
@@ -92,7 +85,7 @@ int lookup_name(const reagent& x, const recipe_ordinal default_recipe) {
     return Name[default_recipe][x.name];
   }
   string_tree* p = property(x, "space");
-  if (!p || p->right) raise << "/space property should have exactly one (non-negative integer) value\n" << end();
+  if (!p || !p->atom) raise << "/space property should have exactly one (non-negative integer) value\n" << end();
   int n = to_integer(p->value);
   assert(n >= 0);
   recipe_ordinal surrounding_recipe = lookup_surrounding_recipe(default_recipe, n);
@@ -136,7 +129,7 @@ recipe_ordinal lookup_surrounding_recipe(const recipe_ordinal r, int n) {
 bool already_transformed(const reagent& r, const map<string, int>& names) {
   if (has_property(r, "space")) {
     string_tree* p = property(r, "space");
-    if (!p || p->right) {
+    if (!p || !p->atom) {
       raise << "/space property should have exactly one (non-negative integer) value in '" << r.original_string << "'\n" << end();
       return false;
     }
diff --git a/046global.cc b/046global.cc
index 8c57236a..8f36918a 100644
--- a/046global.cc
+++ b/046global.cc
@@ -42,16 +42,8 @@ int global_space;
 global_space = 0;
 :(after "Begin Preprocess write_memory(x, 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, "array")
-      || !x.type->right->right
-      || x.type->right->right->value != get(Type_ordinal, "location")
-      || x.type->right->right->right) {
+  if (!scalar(data) || !is_space(x))
     raise << maybe(current_recipe_name()) << "'global-space' should be of type address:array:location, but tried to write '" << to_string(x.type) << "'\n" << end();
-  }
   if (Current_routine->global_space)
     raise << "routine already has a global-space; you can't over-write your globals" << end();
   Current_routine->global_space = data.at(0);
@@ -85,9 +77,6 @@ $error: 0
 
 :(code)
 bool is_global(const reagent& x) {
-  for (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;
+  string_tree* s = property(x, "space");
+  return s && s->atom && s->value == "global";
 }
diff --git a/047check_type_by_name.cc b/047check_type_by_name.cc
index 7812044b..780392ad 100644
--- a/047check_type_by_name.cc
+++ b/047check_type_by_name.cc
@@ -54,7 +54,7 @@ void check_type(set<reagent>& known, const reagent& x, const recipe& caller) {
     raise << maybe(caller.name) << "'" << x.name << "' used with multiple types\n" << end();
     return;
   }
-  if (x.type->name == "array") {
+  if (is_mu_array(x)) {
     if (!x.type->right) {
       raise << maybe(caller.name) << "'" << x.name << ": can't be just an array. What is it an array of?\n" << end();
       return;
diff --git a/050scenario.cc b/050scenario.cc
index 7a2ea7a8..3c033c94 100644
--- a/050scenario.cc
+++ b/050scenario.cc
@@ -388,9 +388,7 @@ void check_memory(const string& s) {
 
 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) {
+  if (is_mu_array(x.type) && is_mu_character(x.type->right)) {
     x.set_value(to_integer(x.name));
     skip_whitespace_and_comments(in);
     string _assign = next_word(in);
diff --git a/054static_dispatch.cc b/054static_dispatch.cc
index ca2bcba9..29cb259f 100644
--- a/054static_dispatch.cc
+++ b/054static_dispatch.cc
@@ -310,9 +310,8 @@ bool all_header_reagents_strictly_match_except_literal_against_address_or_boolea
 }
 
 bool types_strictly_match_except_literal_against_address_or_boolean(const reagent& to, const reagent& from) {
-  if (is_literal(from)
-      && to.type && to.type->value == get(Type_ordinal, "boolean"))
-    return boolean_matches_literal(to, from);
+  if (is_literal(from) && is_mu_boolean(to))
+    return from.name == "0" || from.name == "1";
   return types_strictly_match_except_literal_zero_against_address(to, from);
 }
 
diff --git a/055shape_shifting_container.cc b/055shape_shifting_container.cc
index a539e34f..131028a5 100644
--- a/055shape_shifting_container.cc
+++ b/055shape_shifting_container.cc
@@ -1,5 +1,14 @@
 //:: Container definitions can contain 'type ingredients'
 
+//: pre-requisite: extend our notion of containers to not necessarily be
+//: atomic types
+:(before "End is_mu_container(type) Special-cases")
+if (!type->atom)
+  return is_mu_container(root_type(type));
+:(before "End is_mu_exclusive_container(type) Special-cases")
+if (!type->atom)
+  return is_mu_exclusive_container(root_type(type));
+
 :(scenario size_of_shape_shifting_container)
 container foo:_t [
   x:_t
@@ -267,10 +276,10 @@ def main [
 
 :(before "End element_type Special-cases")
 replace_type_ingredients(element, type, info);
-:(before "Compute Container Size(element)")
-replace_type_ingredients(element, type, info);
-:(before "Compute Exclusive Container Size(element)")
-replace_type_ingredients(element, type, info);
+:(before "Compute Container Size(element, full_type)")
+replace_type_ingredients(element, full_type, container_info);
+:(before "Compute Exclusive Container Size(element, full_type)")
+replace_type_ingredients(element, full_type, exclusive_container_info);
 :(before "Compute Container Address Offset(element)")
 replace_type_ingredients(element, type, info);
 if (contains_type_ingredient(element)) return;  // error raised elsewhere
@@ -296,63 +305,42 @@ bool contains_type_ingredient(const reagent& x) {
 
 bool contains_type_ingredient(const type_tree* type) {
   if (!type) return false;
-  if (type->value >= START_TYPE_INGREDIENTS) return true;
-  assert(!is_type_ingredient_name(type->name));
+  if (type->atom) return type->value >= START_TYPE_INGREDIENTS;
   return contains_type_ingredient(type->left) || contains_type_ingredient(type->right);
 }
 
 // replace all type_ingredients in element_type with corresponding elements of callsite_type
-// todo: too complicated and likely incomplete; maybe avoid replacing in place?
 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->atom) {
+    replace_type_ingredients(element_type->left, callsite_type, container_info);
+    replace_type_ingredients(element_type->right, callsite_type, container_info);
+    return;
+  }
   if (element_type->value < START_TYPE_INGREDIENTS) return;
-
   const int type_ingredient_index = element_type->value-START_TYPE_INGREDIENTS;
   if (!has_nth_type(callsite_type, type_ingredient_index)) {
     raise << "illegal type " << names_to_string(callsite_type) << " seems to be missing a type ingredient or three\n" << end();
     return;
   }
+  *element_type = *nth_type_ingredient(callsite_type, type_ingredient_index, container_info);
+}
 
-  // B. replace the current location
-  const type_tree* replacement = NULL;
-  bool zig_left = false;
-  {
-    const type_tree* curr = callsite_type;
-    for (int i = 0; i < type_ingredient_index; ++i)
-      curr = curr->right;
-    if (curr && curr->left) {
-      replacement = curr->left;
-      zig_left = true;
-    }
-    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 (element_type->right && replacement->right && zig_left) {  // ZERO confidence that this condition is accurate
-    element_type->name = "";
-    element_type->value = 0;
-    element_type->left = new type_tree(*replacement);
-  }
-  else {
-    string old_name = element_type->name;
-    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 (zig_left || final_type_ingredient(type_ingredient_index, container_info)) {
-      type_tree* old_right = element_type->right;
-      element_type->right = replacement->right ? new type_tree(*replacement->right) : NULL;
-      append(element_type->right, old_right);
-    }
+const type_tree* nth_type_ingredient(const type_tree* callsite_type, int type_ingredient_index, const type_info& container_info) {
+  bool final = final_type_ingredient(type_ingredient_index, container_info);
+  const type_tree* curr = callsite_type;
+  for (int i = 0; i < type_ingredient_index; ++i) {
+    assert(curr);
+    assert(!curr->atom);
+//?     cerr << "type ingredient " << i << " is " << to_string(curr->left) << '\n';
+    curr = curr->right;
   }
+  assert(curr);
+  if (curr->atom) return curr;
+  if (!final) return curr->left;
+  if (!curr->right) return curr->left;
+  return curr;
 }
 
 bool final_type_ingredient(int type_ingredient_index, const type_info& container_info) {
@@ -482,7 +470,128 @@ def main [
 ]
 +error: illegal type "foo" seems to be missing a type ingredient or three
 
-//: 'merge' on shape-shifting containers
+//:: fix up previous layers
+
+//: We have two transforms in previous layers -- for computing sizes and
+//: offsets containing addresses for containers and exclusive containers --
+//: that we need to teach about type ingredients.
+
+:(before "End compute_container_sizes Non-atom Cases")
+const type_tree* root = root_type(type);
+type_info& info = get(Type, root->value);
+if (info.kind == CONTAINER) {
+  compute_container_sizes(info, type, pending_metadata);
+  return;
+}
+if (info.kind == EXCLUSIVE_CONTAINER) {
+  compute_exclusive_container_sizes(info, type, pending_metadata);
+  return;
+}
+
+:(before "End Unit Tests")
+void test_container_sizes_shape_shifting_container() {
+  run("container foo:_t [\n"
+      "  x:number\n"
+      "  y:_t\n"
+      "]\n");
+  reagent r("x:foo:point");
+  compute_container_sizes(r);
+  CHECK_EQ(r.metadata.size, 3);
+}
+
+void test_container_sizes_shape_shifting_exclusive_container() {
+  run("exclusive-container foo:_t [\n"
+      "  x:number\n"
+      "  y:_t\n"
+      "]\n");
+  reagent r("x:foo:point");
+  compute_container_sizes(r);
+  CHECK_EQ(r.metadata.size, 3);
+  reagent r2("x:foo:number");
+  compute_container_sizes(r2);
+  CHECK_EQ(r2.metadata.size, 2);
+}
+
+void test_container_sizes_compound_type_ingredient() {
+  run("container foo:_t [\n"
+      "  x:number\n"
+      "  y:_t\n"
+      "]\n");
+  reagent r("x:foo:address:point");
+  compute_container_sizes(r);
+  CHECK_EQ(r.metadata.size, 2);
+  // scan also pre-computes metadata for type ingredient
+  reagent point("x:point");
+  CHECK(contains_key(Container_metadata, point.type));
+  CHECK_EQ(get(Container_metadata, point.type).size, 2);
+}
+
+void test_container_sizes_recursive_shape_shifting_container() {
+  run("container foo:_t [\n"
+      "  x:number\n"
+      "  y:address:foo:_t\n"
+      "]\n");
+  reagent r2("x:foo:number");
+  compute_container_sizes(r2);
+  CHECK_EQ(r2.metadata.size, 2);
+}
+
+:(before "End compute_container_address_offsets Non-atom Cases")
+const type_tree* root = root_type(type);
+type_info& info = get(Type, root->value);
+if (info.kind == CONTAINER) {
+  compute_container_address_offsets(info, type);
+  return;
+}
+if (info.kind == EXCLUSIVE_CONTAINER) {
+  compute_exclusive_container_address_offsets(info, type);
+  return;
+}
+
+:(before "End Unit Tests")
+void test_container_address_offsets_in_shape_shifting_container() {
+  run("container foo:_t [\n"
+      "  x:number\n"
+      "  y:_t\n"
+      "]\n");
+  reagent r("x:foo:address:number");
+  compute_container_sizes(r);
+  compute_container_address_offsets(r);
+  CHECK_EQ(SIZE(r.metadata.address), 1);
+  CHECK(contains_key(r.metadata.address, set<tag_condition_info>()));
+  set<address_element_info>& offset_info = get(r.metadata.address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(offset_info), 1);
+  CHECK_EQ(offset_info.begin()->offset, 1);  //
+  CHECK(offset_info.begin()->payload_type->atom);
+  CHECK_EQ(offset_info.begin()->payload_type->name, "number");
+}
+
+void test_container_address_offsets_in_nested_shape_shifting_container() {
+  run("container foo:_t [\n"
+      "  x:number\n"
+      "  y:_t\n"
+      "]\n"
+      "container bar:_t [\n"
+      "  x:_t\n"
+      "  y:foo:_t\n"
+      "]\n");
+  reagent r("x:bar:address:number");
+  CLEAR_TRACE;
+  compute_container_sizes(r);
+  compute_container_address_offsets(r);
+  CHECK_EQ(SIZE(r.metadata.address), 1);
+  CHECK(contains_key(r.metadata.address, set<tag_condition_info>()));
+  set<address_element_info>& offset_info = get(r.metadata.address, set<tag_condition_info>());
+  CHECK_EQ(SIZE(offset_info), 2);
+  CHECK_EQ(offset_info.begin()->offset, 0);  //
+  CHECK(offset_info.begin()->payload_type->atom);
+  CHECK_EQ(offset_info.begin()->payload_type->name, "number");
+  CHECK_EQ((++offset_info.begin())->offset, 2);  //
+  CHECK((++offset_info.begin())->payload_type->atom);
+  CHECK_EQ((++offset_info.begin())->payload_type->name, "number");
+}
+
+//:: 'merge' on shape-shifting containers
 
 :(scenario merge_check_shape_shifting_container_containing_exclusive_container)
 container foo:_elem [
diff --git a/056shape_shifting_recipe.cc b/056shape_shifting_recipe.cc
index 3441829b..428c3194 100644
--- a/056shape_shifting_recipe.cc
+++ b/056shape_shifting_recipe.cc
@@ -97,8 +97,8 @@ vector<recipe_ordinal> strictly_matching_shape_shifting_variants(const instructi
   for (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));
+    if (!all_concrete_header_reagents_strictly_match(inst, get(Recipe, variants.at(i)))) continue;
+    result.push_back(variants.at(i));
   }
   return result;
 }
@@ -192,31 +192,28 @@ int number_of_concrete_type_names(const reagent& r) {
 
 int number_of_concrete_type_names(const type_tree* type) {
   if (!type) return 0;
-  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;
+  if (type->atom)
+    return is_type_ingredient_name(type->name) ? 0 : 1;
+  return number_of_concrete_type_names(type->left)
+       + number_of_concrete_type_names(type->right);
 }
 
 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);
+  if (to->atom && is_type_ingredient_name(to->name)) return true;  // type ingredient matches anything
+  if (from->atom && is_mu_address(to))
+    return from->name == "literal" && rhs_reagent.name == "0";
+  if (!from->atom && !to->atom)
+    return concrete_type_names_strictly_match(to->left, from->left, rhs_reagent)
+        && concrete_type_names_strictly_match(to->right, from->right, rhs_reagent);
+  if (from->atom != to->atom) return false;
+  // both from and to are atoms
+  if (from->name == "literal")
+    return Literal_type_names.find(to->name) != Literal_type_names.end();
+  if (to->name == "literal")
+    return Literal_type_names.find(from->name) != Literal_type_names.end();
+  return to->name == from->name;
 }
 
 bool contains_type_ingredient_name(const reagent& x) {
@@ -337,14 +334,12 @@ void accumulate_type_ingredients(const type_tree* exemplar_type, const type_tree
   }
   if (is_type_ingredient_name(exemplar_type->name)) {
     const type_tree* curr_refinement_type = NULL;  // temporary heap allocation; must always be deleted before it goes out of scope
-    if (refinement_type->left)
-      curr_refinement_type = new type_tree(*refinement_type->left);
-    else if (exemplar_type->right)
-      // splice out refinement_type->right, it'll be used later by the exemplar_type->right
-      curr_refinement_type = new type_tree(refinement_type->name, refinement_type->value, NULL);
-    else
+    if (exemplar_type->atom)
       curr_refinement_type = new type_tree(*refinement_type);
-    assert(!curr_refinement_type->left);
+    else {
+      assert(!refinement_type->atom);
+      curr_refinement_type = new type_tree(*refinement_type->left);
+    }
     if (!contains_key(mappings, exemplar_type->name)) {
       trace(9993, "transform") << "adding mapping from " << exemplar_type->name << " to " << to_string(curr_refinement_type) << end();
       put(mappings, exemplar_type->name, new type_tree(*curr_refinement_type));
@@ -409,49 +404,27 @@ void replace_type_ingredients(reagent& x, const map<string, const type_tree*>& m
 // todo: too complicated and likely incomplete; maybe avoid replacing in place?
 void replace_type_ingredients(type_tree* type, const map<string, const type_tree*>& mappings) {
   if (!type) return;
-  if (contains_key(Type_ordinal, type->name))  // todo: ugly side effect
-    type->value = get(Type_ordinal, type->name);
-  if (!is_type_ingredient_name(type->name) || !contains_key(mappings, type->name)) {
+  if (!type->atom) {
     replace_type_ingredients(type->left, mappings);
     replace_type_ingredients(type->right, mappings);
     return;
   }
-
+  if (contains_key(Type_ordinal, type->name))  // todo: ugly side effect
+    type->value = get(Type_ordinal, type->name);
+  if (!contains_key(mappings, type->name))
+    return;
   const type_tree* replacement = get(mappings, type->name);
   trace(9993, "transform") << type->name << " => " << names_to_string(replacement) << end();
-  if (!contains_key(Type_ordinal, replacement->name)) {
-    // error in program; should be reported elsewhere
-    return;
-  }
-
-  // type is a single type ingredient
-  assert(!type->left);
-  if (!type->right) assert(!replacement->left);
-
-  if (!replacement->right) {
-    if (!replacement->left) {
-      type->name = (replacement->name == "literal") ? "number" : replacement->name;
-      type->value = get(Type_ordinal, type->name);
-    }
-    else {
-      type->name = "";
-      type->value = 0;
-      type->left = new type_tree(*replacement);
+  if (replacement->atom) {
+    if (!contains_key(Type_ordinal, replacement->name)) {
+      // error in program; should be reported elsewhere
+      return;
     }
-    replace_type_ingredients(type->right, mappings);
-  }
-  // replace non-last type?
-  else if (type->right) {
-    type->name = "";
-    type->value = 0;
-    type->left = new type_tree(*replacement);
-    replace_type_ingredients(type->right, mappings);
+    type->name = (replacement->name == "literal") ? "number" : replacement->name;
+    type->value = get(Type_ordinal, type->name);
   }
-  // replace last type?
   else {
-    type->name = replacement->name;
-    type->value = get(Type_ordinal, type->name);
-    type->right = new type_tree(*replacement->right);
+    *type = *replacement;
   }
 }
 
@@ -473,34 +446,9 @@ void accumulate_type_ingredients(const type_tree* type, set<string>& out) {
 }
 
 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() != '(')
-    return new type_tree(next_word(in), 0);
-  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);
-    curr = &(*curr)->right;
-  }
-  in.get();  // skip ')'
+  string_tree* s2 = parse_string_tree(s);
+  type_tree* result = new_type_tree(s2);
+  delete s2;
   return result;
 }
 
diff --git a/057immutable.cc b/057immutable.cc
index 0d82c010..739f6a56 100644
--- a/057immutable.cc
+++ b/057immutable.cc
@@ -555,9 +555,9 @@ $error: 0
 :(before "End Immutable Ingredients Special-cases")
 if (has_property(current_ingredient, "contained-in")) {
   const string_tree* tmp = property(current_ingredient, "contained-in");
-  if (tmp->left || tmp->right
+  if (!tmp->atom
       || !is_present_in_ingredients(caller, tmp->value)
       || !is_present_in_products(caller, tmp->value))
-    raise << maybe(caller.name) << "/contained-in can only point to another ingredient+product, but got '" << to_string(property(current_ingredient, "contained-in")) << "'\n" << end();
+    raise << maybe(caller.name) << "/contained-in can only point to another ingredient or product, but got '" << to_string(property(current_ingredient, "contained-in")) << "'\n" << end();
   continue;
 }
diff --git a/062rewrite_stash.cc b/062rewrite_stash.cc
index 68738341..7ef0f38c 100644
--- a/062rewrite_stash.cc
+++ b/062rewrite_stash.cc
@@ -117,14 +117,15 @@ void rewrite_stash_to_text(reagent& r, vector<instruction>& out, const string& t
 }
 
 bool is_lookup_of_address_of_array(reagent/*copy*/ x) {
-  if (x.type->name != "address") return false;
+  if (x.type->atom) return false;
+  if (x.type->left->name != "address") return false;
   if (!canonize_type(x)) return false;
-  return x.type->name == "array";
+  return is_mu_array(x);
 }
 
 bool is_static_array(const reagent& x) {
   // no canonize_type()
-  return x.type->name == "array";
+  return !x.type->atom && x.type->left->atom && x.type->left->name == "array";
 }
 
 //: Make sure that the new system is strictly better than just the 'stash'
diff --git a/069hash.cc b/069hash.cc
index 87cf666c..9133fa6d 100644
--- a/069hash.cc
+++ b/069hash.cc
@@ -89,8 +89,7 @@ size_t hash_mu_array(size_t h, const reagent& r) {
 }
 
 size_t hash_mu_container(size_t h, const reagent& r) {
-  assert(r.type->value);
-  type_info& info = get(Type, r.type->value);
+  type_info& info = get(Type, root_type(r.type)->value);
   int address = r.value;
   int offset = 0;
   for (int i = 0; i < SIZE(info.elements); ++i) {
@@ -105,12 +104,13 @@ size_t hash_mu_container(size_t h, const reagent& r) {
 }
 
 size_t hash_mu_exclusive_container(size_t h, const reagent& r) {
-  assert(r.type->value);
+  const type_tree* type = root_type(r.type);
+  assert(type->value);
   int tag = get(Memory, r.value);
   reagent/*copy*/ variant = variant_type(r, tag);
   // todo: move this error 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" << end();
+    raise << get(Type, type->value).name << ": /ignore-for-hash won't work in exclusive containers\n" << end();
   variant.set_value(r.value + /*skip tag*/1);
   h = hash(h, variant);
   return h;
diff --git a/071recipe.cc b/071recipe.cc
index 5a4377cb..30df827a 100644
--- a/071recipe.cc
+++ b/071recipe.cc
@@ -70,8 +70,8 @@ bool contains_reagent_with_type(const recipe& caller, const string& name) {
 bool is_matching_non_recipe_literal(const reagent& x, const string& name) {
   if (x.name != name) return false;
   if (!x.type) return false;
-  if (x.type->value == get(Type_ordinal, "recipe-literal")) return false;
-  return true;
+  if (!x.type->atom) return false;
+  return x.type->value != get(Type_ordinal, "recipe-literal");
 }
 
 //: It's confusing to use variable names that are also recipe names. Always
@@ -177,22 +177,32 @@ void check_indirect_calls_against_header(const recipe_ordinal r) {
 }
 
 recipe from_reagent(const reagent& r) {
-  assert(r.type->name == "recipe");
+  assert(!r.type->atom && r.type->left->atom && r.type->left->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 == "->") {
+  for (/*nada*/; curr && !curr->atom; curr = curr->right) {
+    if (curr->left->atom && curr->left->name == "->") {
       curr = curr->right;  // skip delimiter
       break;
     }
-    result_header.ingredients.push_back(next_recipe_reagent(curr));
+    result_header.ingredients.push_back(next_recipe_reagent(curr->left));
+    if (curr->right && curr->right->atom) {
+      result_header.ingredients.push_back(next_recipe_reagent(curr->right));
+      return result_header;  // no products
+    }
   }
-  for (; curr; curr=curr->right)
+  for (; curr && !curr->atom; curr=curr->right)
+    result_header.products.push_back(next_recipe_reagent(curr->left));
+  if (curr) {
+    assert(curr->atom);
     result_header.products.push_back(next_recipe_reagent(curr));
+  }
   return result_header;
 }
 
+// todo: unit test: 'recipe number' vs 'recipe -> number'
+
 reagent next_recipe_reagent(const type_tree* curr) {
   if (!curr->left) return reagent("recipe:"+curr->name);
   reagent result;
@@ -203,9 +213,10 @@ reagent next_recipe_reagent(const type_tree* curr) {
 
 bool is_mu_recipe(const 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
+  if (r.type->atom)
+    return r.type->name == "recipe-literal";
+  if (!r.type->left->atom) return false;
+  if (r.type->left->name == "recipe") return true;
   return false;
 }
 
diff --git a/073wait.cc b/073wait.cc
index 46086589..a642c86a 100644
--- a/073wait.cc
+++ b/073wait.cc
@@ -123,7 +123,12 @@ case GET_LOCATION: {
   }
   reagent/*copy*/ base = inst.ingredients.at(0);
   if (!canonize_type(base)) break;
-  if (!base.type || !base.type->value || !contains_key(Type, base.type->value) || get(Type, base.type->value).kind != CONTAINER) {
+  if (!base.type) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'get-location' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  const type_tree* base_root_type = base.type->atom ? base.type : base.type->left;
+  if (!base_root_type->atom || base_root_type->value == 0 || !contains_key(Type, base_root_type->value) || get(Type, base_root_type->value).kind != CONTAINER) {
     raise << maybe(get(Recipe, r).name) << "first ingredient of 'get-location' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
     break;
   }
@@ -160,9 +165,9 @@ case GET_LOCATION: {
     raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
     break;
   }
-  type_ordinal base_type = base.type->value;
+  const type_tree* base_root_type = root_type(base.type);
   int offset = ingredients.at(1).at(0);
-  if (offset < 0 || offset >= SIZE(get(Type, base_type).elements)) break;  // copied from Check above
+  if (offset < 0 || offset >= SIZE(get(Type, base_root_type->value).elements)) break;  // copied from Check above
   int result = base_address;
   for (int i = 0; i < offset; ++i)
     result += size_of(element_type(base.type, i));
diff --git a/074deep_copy.cc b/074deep_copy.cc
index 4452549f..a3d7c48b 100644
--- a/074deep_copy.cc
+++ b/074deep_copy.cc
@@ -283,7 +283,7 @@ void deep_copy(const reagent& canonized_in, map<int, int>& addresses_copied, con
       // construct a fake reagent that reads directly from the appropriate
       // field of the container
       reagent curr;
-      curr.type = new type_tree("address", new type_tree(*info->payload_type));
+      curr.type = new type_tree(new type_tree("address"), new type_tree(*info->payload_type));
       curr.set_value(canonized_in.value + info->offset);
       curr.properties.push_back(pair<string, string_tree*>("raw", NULL));
       trace(9991, "run") << "deep-copy: copying address " << curr.value << end();