about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2018-06-15 22:12:03 -0700
committerKartik Agaram <vc@akkartik.com>2018-06-15 22:12:03 -0700
commit0edd9b9fc60440213e4df926ea511419ee291f1e (patch)
tree84b22f7afdeb9110ad7105c5fc070dacff178502
parent3f34ac9369978b396d00a4fd02c9fb06b8eea621 (diff)
downloadmu-0edd9b9fc60440213e4df926ea511419ee291f1e.tar.gz
4257 - abortive attempt at safe fat pointers
I've been working on this slowly over several weeks, but it's too hard
to support 0 as the null value for addresses. I constantly have to add
exceptions for scalar value corresponding to an address type (now
occupying 2 locations). The final straw is the test for 'reload':

  x:num <- reload text

'reload' returns an address. But there's no way to know that for
arbitrary instructions.

New plan: let's put this off for a bit and first create support for
literals. Then use 'null' instead of '0' for addresses everywhere. Then
it'll be easy to just change what 'null' means.
-rw-r--r--020run.cc14
-rw-r--r--021check_instruction.cc25
-rw-r--r--022arithmetic.cc15
-rw-r--r--023boolean.cc10
-rw-r--r--024jump.cc21
-rw-r--r--025compare.cc57
-rw-r--r--027call_ingredient.cc17
-rw-r--r--028call_return.cc4
-rw-r--r--030container.cc49
-rw-r--r--032array.cc68
-rw-r--r--033exclusive_container.cc8
-rw-r--r--034address.cc85
-rw-r--r--035lookup.cc106
-rw-r--r--037abandon.cc36
-rw-r--r--038new_text.cc72
-rw-r--r--042name.cc38
-rw-r--r--043space.cc69
-rw-r--r--044space_surround.cc31
-rw-r--r--045closure_name.cc8
-rw-r--r--046check_type_by_name.cc18
-rw-r--r--050scenario.cc1
-rw-r--r--053recipe_header.cc3
-rw-r--r--055shape_shifting_container.cc14
-rw-r--r--058to_text.cc1
-rw-r--r--060rewrite_literal_string.cc1
-rw-r--r--065duplex_list.mu53
-rw-r--r--069hash.cc12
-rw-r--r--074wait.cc17
-rw-r--r--082scenario_screen.cc69
-rw-r--r--085scenario_console.cc12
-rw-r--r--087file.cc4
-rw-r--r--089scenario_filesystem.cc16
-rw-r--r--091socket.cc2
-rw-r--r--101run_sandboxed.cc111
-rw-r--r--edit/001-editor.mu22
-rw-r--r--edit/002-typing.mu4
-rw-r--r--edit/003-shortcuts.mu23
-rw-r--r--index.html22
38 files changed, 757 insertions, 381 deletions
diff --git a/020run.cc b/020run.cc
index 3739ad2c..0bba6bb2 100644
--- a/020run.cc
+++ b/020run.cc
@@ -87,6 +87,12 @@ void run_current_routine() {
       // Primitive Recipe Implementations
       case COPY: {
         copy(ingredients.begin(), ingredients.end(), inserter(products, products.begin()));
+        for (int i = 0;  i < SIZE(current_instruction().products);  ++i) {
+          if (is_mu_scalar(current_instruction().products.at(i)) && is_mu_address(current_instruction().ingredients.at(i)))
+            products.at(i).erase(products.at(i).begin());  // ignore alloc id
+          if (is_mu_address(current_instruction().products.at(i)) && is_mu_scalar(current_instruction().ingredients.at(i)))
+            products.at(i).insert(products.at(i).begin(), /*alloc id*/0);
+        }
         break;
       }
       // End Primitive Recipe Implementations
@@ -196,7 +202,7 @@ if (argc > 1) {
 }
 transform_all();
 //? cerr << to_original_string(get(Type_ordinal, "editor")) << '\n';
-//? cerr << to_original_string(get(Recipe, get(Recipe_ordinal, "event-loop"))) << '\n';
+//? cerr << to_original_string(get(Recipe, get(Recipe_ordinal, "handle-keyboard-event"))) << '\n';
 //? DUMP("");
 //? exit(0);
 if (trace_contains_errors()) {
@@ -341,8 +347,9 @@ void write_memory(reagent/*copy*/ x, const vector<double>& data) {
 }
 
 :(code)
-int size_of(const reagent& r) {
+int size_of(reagent/*copy*/ r) {
   if (!r.type) return 0;
+  // Begin size_of(reagent r) Special-cases
   // End size_of(reagent r) Special-cases
   return size_of(r.type);
 }
@@ -351,6 +358,7 @@ int size_of(const type_tree* type) {
   if (type->atom) {
     if (type->value == -1) return 1;  // error value, but we'll raise it elsewhere
     if (type->value == 0) return 1;
+//?     if (type->value == Address_type_ordinal) return 2;  // address and alloc id
     // End size_of(type) Atom Special-cases
   }
   else {
@@ -358,7 +366,7 @@ int size_of(const type_tree* type) {
       raise << "invalid type " << to_string(type) << '\n' << end();
       return 0;
     }
-    if (type->left->value == Address_type_ordinal) return 1;
+    if (type->left->value == Address_type_ordinal) return 2;  // address and alloc id
     // End size_of(type) Non-atom Special-cases
   }
   // End size_of(type) Special-cases
diff --git a/021check_instruction.cc b/021check_instruction.cc
index d7628008..4bba31e5 100644
--- a/021check_instruction.cc
+++ b/021check_instruction.cc
@@ -112,8 +112,9 @@ def main [
 
 :(code)
 // types_match with some leniency
-bool types_coercible(const reagent& to, const reagent& from) {
-  if (types_match(to, from)) return true;
+bool types_coercible(reagent/*copy*/ to, reagent/*copy*/ from) {
+  // Begin types_coercible(reagent to, reagent from)
+  if (types_match_sub(to, from)) return true;
   if (is_mu_address(from) && is_real_mu_number(to)) return true;
   if (is_mu_boolean(from) && is_real_mu_number(to)) return true;
   if (is_real_mu_number(from) && is_mu_character(to)) return true;
@@ -121,10 +122,11 @@ bool types_coercible(const reagent& to, const reagent& from) {
   return false;
 }
 
-bool types_match(const reagent& to, const reagent& from) {
+bool types_match_sub(const reagent& to, const reagent& from) {
   // to sidestep type-checking, use /unsafe in the source.
   // this will be highlighted in red inside vim. just for setting up some tests.
   if (is_unsafe(from)) return true;
+
   if (is_literal(from)) {
     if (is_mu_array(to)) return false;
     // End Matching Types For Literal(to)
@@ -134,12 +136,16 @@ bool types_match(const reagent& to, const reagent& from) {
     if (is_mu_boolean(to)) return from.name == "0" || from.name == "1";
     return size_of(to) == 1;  // literals are always scalars
   }
-  return types_strictly_match(to, from);
+  return types_strictly_match_sub(to, from);
+}
+// variant for others to call
+bool types_match(reagent/*copy*/ to, reagent/*copy*/ from) {
+  // Begin types_match(reagent to, reagent from)
+  return types_match_sub(to, from);
 }
 
 //: copy arguments for later layers
-bool types_strictly_match(reagent/*copy*/ to, reagent/*copy*/ from) {
-  // End Preprocess types_strictly_match(reagent to, reagent from)
+bool types_strictly_match_sub(const reagent& to, const reagent& from) {
   if (to.type == NULL) return false;  // error
   if (is_literal(from) && to.type->value == Number_type_ordinal) return true;
   // to sidestep type-checking, use /unsafe in the source.
@@ -150,6 +156,11 @@ bool types_strictly_match(reagent/*copy*/ to, reagent/*copy*/ from) {
   if (!to.type) return !from.type;
   return types_strictly_match(to.type, from.type);
 }
+// variant for others to call
+bool types_strictly_match(reagent/*copy*/ to, reagent/*copy*/ from) {
+  // Begin types_strictly_match(reagent to, reagent from)
+  return types_strictly_match_sub(to, from);
+}
 
 bool types_strictly_match(const type_tree* to, const type_tree* from) {
   if (from == to) return true;
@@ -268,7 +279,7 @@ bool is_mu_scalar(reagent/*copy*/ r) {
 }
 bool is_mu_scalar(const type_tree* type) {
   if (!type) return false;
-  if (is_mu_address(type)) return true;
+  if (is_mu_address(type)) return false;
   if (!type->atom) return false;
   if (is_literal(type))
     return type->name != "literal-string";
diff --git a/022arithmetic.cc b/022arithmetic.cc
index 530541aa..50a54578 100644
--- a/022arithmetic.cc
+++ b/022arithmetic.cc
@@ -95,18 +95,25 @@ case SUBTRACT: {
   }
   break;
 }
+:(code)
+bool is_raw(const reagent& r) {
+  return has_property(r, "raw");
+}
+
 :(before "End Primitive Recipe Implementations")
 case SUBTRACT: {
-  double result = ingredients.at(0).at(0);
+  double result = scalar_ingredient(ingredients, 0);
   for (int i = 1;  i < SIZE(ingredients);  ++i)
-    result -= ingredients.at(i).at(0);
+    result -= scalar_ingredient(ingredients, i);
   products.resize(1);
   products.at(0).push_back(result);
   break;
 }
 :(code)
-bool is_raw(const reagent& r) {
-  return has_property(r, "raw");
+double scalar_ingredient(const vector<vector<double> >& ingredients, int i) {
+  if (is_mu_address(current_instruction().ingredients.at(i)))
+    return ingredients.at(i).at(1);  // skip alloc id
+  return ingredients.at(i).at(0);
 }
 
 :(scenario subtract_literal)
diff --git a/023boolean.cc b/023boolean.cc
index 3d51fd7a..976cbff9 100644
--- a/023boolean.cc
+++ b/023boolean.cc
@@ -26,7 +26,7 @@ case AND: {
 case AND: {
   bool result = true;
   for (int i = 0;  i < SIZE(ingredients);  ++i)
-    result = result && ingredients.at(i).at(0);
+    result = result && scalar_ingredient(ingredients, i);
   products.resize(1);
   products.at(0).push_back(result);
   break;
@@ -84,7 +84,7 @@ case OR: {
 case OR: {
   bool result = false;
   for (int i = 0;  i < SIZE(ingredients);  ++i)
-    result = result || ingredients.at(i).at(0);
+    result = result || scalar_ingredient(ingredients, i);
   products.resize(1);
   products.at(0).push_back(result);
   break;
@@ -127,8 +127,8 @@ case NOT: {
     break;
   }
   for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
-    if (!is_mu_scalar(inst.ingredients.at(i))) {
-      raise << maybe(get(Recipe, r).name) << "'not' requires boolean ingredients, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+    if (!is_mu_scalar(inst.ingredients.at(i)) && !is_mu_address(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'not' requires ingredients that can be interpreted as boolean, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
       goto finish_checking_instruction;
     }
   }
@@ -145,7 +145,7 @@ case NOT: {
 case NOT: {
   products.resize(SIZE(ingredients));
   for (int i = 0;  i < SIZE(ingredients);  ++i) {
-    products.at(i).push_back(!ingredients.at(i).at(0));
+    products.at(i).push_back(!scalar_ingredient(ingredients, i));
   }
   break;
 }
diff --git a/024jump.cc b/024jump.cc
index 37011290..b7011c09 100644
--- a/024jump.cc
+++ b/024jump.cc
@@ -72,7 +72,7 @@ case JUMP_IF: {
     raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' should get exactly two ingredients\n" << end();
     break;
   }
-  if (!is_mu_scalar(inst.ingredients.at(0))) {
+  if (!is_mu_address(inst.ingredients.at(0)) && !is_mu_scalar(inst.ingredients.at(0))) {
     raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' requires a boolean for its first ingredient, but '" << inst.ingredients.at(0).name << "' has type '" << names_to_string_without_quotes(inst.ingredients.at(0).type) << "'\n" << end();
     break;
   }
@@ -90,7 +90,7 @@ case JUMP_IF: {
 :(before "End Primitive Recipe Implementations")
 case JUMP_IF: {
   assert(current_instruction().ingredients.at(1).initialized);
-  if (!ingredients.at(0).at(0)) {
+  if (!scalar_ingredient(ingredients, 0)) {
     trace(9998, "run") << "jump-if fell through" << end();
     break;
   }
@@ -109,7 +109,7 @@ def main [
 ]
 +run: jump-if {999: "literal"}, {1: "offset"}
 +run: jumping to instruction 2
--run: {1: "number"} <- copy {1: "literal"}
+-run: {123: "number"} <- copy {1: "literal"}
 -mem: storing 1 in location 123
 
 :(scenario jump_if_fallthrough)
@@ -122,6 +122,17 @@ def main [
 +run: {123: "number"} <- copy {1: "literal"}
 +mem: storing 1 in location 123
 
+:(scenario jump_if_on_address)
+def main [
+  10:&:num <- copy 999/unsafe
+  jump-if 10:&:number, 1:offset
+  123:num <- copy 1
+]
++run: jump-if {10: ("address" "number")}, {1: "offset"}
++run: jumping to instruction 3
+-run: {123: "number"} <- copy {1: "literal"}
+-mem: storing 1 in location 123
+
 :(before "End Primitive Recipe Declarations")
 JUMP_UNLESS,
 :(before "End Primitive Recipe Numbers")
@@ -132,7 +143,7 @@ case JUMP_UNLESS: {
     raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' should get exactly two ingredients\n" << end();
     break;
   }
-  if (!is_mu_scalar(inst.ingredients.at(0))) {
+  if (!is_mu_address(inst.ingredients.at(0)) && !is_mu_scalar(inst.ingredients.at(0))) {
     raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' requires a boolean for its first ingredient, but '" << inst.ingredients.at(0).name << "' has type '" << names_to_string_without_quotes(inst.ingredients.at(0).type) << "'\n" << end();
     break;
   }
@@ -150,7 +161,7 @@ case JUMP_UNLESS: {
 :(before "End Primitive Recipe Implementations")
 case JUMP_UNLESS: {
   assert(current_instruction().ingredients.at(1).initialized);
-  if (ingredients.at(0).at(0)) {
+  if (scalar_ingredient(ingredients, 0)) {
     trace(9998, "run") << "jump-unless fell through" << end();
     break;
   }
diff --git a/025compare.cc b/025compare.cc
index 92878208..c82f3578 100644
--- a/025compare.cc
+++ b/025compare.cc
@@ -29,6 +29,8 @@ case EQUAL: {
 }
 :(before "End Primitive Recipe Implementations")
 case EQUAL: {
+  // todo: keep the address exception from slowing down the common case
+  drop_alloc_ids_if_comparing_address_to_literal_0(ingredients);
   vector<double>& exemplar = ingredients.at(0);
   bool result = true;
   for (int i = /*skip exemplar*/1;  i < SIZE(ingredients);  ++i) {
@@ -41,6 +43,23 @@ case EQUAL: {
   products.at(0).push_back(result);
   break;
 }
+:(code)
+void drop_alloc_ids_if_comparing_address_to_literal_0(vector<vector<double> >& ingredients) {
+  bool any_ingredient_is_null = false;
+  bool any_ingredient_is_address = false;
+  for (int i = 0;  i < SIZE(current_instruction().ingredients);  ++i) {
+    if (current_instruction().ingredients.at(i).name == "0")
+      any_ingredient_is_null = true;
+    if (is_mu_address(current_instruction().ingredients.at(i)))
+      any_ingredient_is_address = true;
+  }
+  if (any_ingredient_is_null && any_ingredient_is_address) {
+    for (int i = 0;  i < SIZE(ingredients);  ++i) {
+      if (is_mu_address(current_instruction().ingredients.at(i)))
+        ingredients.at(i).erase(ingredients.at(i).begin());
+    }
+  }
+}
 
 :(scenario equal)
 def main [
@@ -74,6 +93,42 @@ def main [
 ]
 +mem: storing 0 in location 1
 
+:(scenario equal_address_null)
+def main [
+  1:&:num <- copy 0
+  10:bool <- equal 1:&:num, 0
+]
++mem: storing 1 in location 10
+
+:(scenario equal_address_null_2)
+def main [
+  1:&:num <- copy 0
+  10:bool <- equal 0, 1:&:num
+]
++mem: storing 1 in location 10
+
+:(scenario equal_address_null_3)
+def main [
+  1:&:num <- new num:type
+  10:bool <- equal 1:&:num, 0
+]
++mem: storing 0 in location 10
+
+:(scenario equal_address_null_multiple)
+def main [
+  1:&:num <- copy 0
+  10:bool <- equal 0, 1:&:num, 0
+]
++mem: storing 1 in location 10
+
+:(scenario equal_address_null_multiple_2)
+def main [
+  1:&:num <- copy 0
+  3:&:num <- copy 0
+  10:bool <- equal 0, 1:&:num, 0, 3:&:num
+]
++mem: storing 1 in location 10
+
 :(before "End Primitive Recipe Declarations")
 NOT_EQUAL,
 :(before "End Primitive Recipe Numbers")
@@ -101,6 +156,8 @@ case NOT_EQUAL: {
 }
 :(before "End Primitive Recipe Implementations")
 case NOT_EQUAL: {
+  // todo: keep the address exception from slowing down the common case
+  drop_alloc_ids_if_comparing_address_to_literal_0(ingredients);
   vector<double>& exemplar = ingredients.at(0);
   products.resize(1);
   bool equal_ingredients = equal(ingredients.at(1).begin(), ingredients.at(1).end(), exemplar.begin());
diff --git a/027call_ingredient.cc b/027call_ingredient.cc
index 46fafe7e..00e44ea3 100644
--- a/027call_ingredient.cc
+++ b/027call_ingredient.cc
@@ -68,6 +68,9 @@ case NEXT_INGREDIENT: {
     }
     products.push_back(
         current_call().ingredient_atoms.at(current_call().next_ingredient_to_process));
+    if (is_mu_scalar(current_call().ingredients.at(current_call().next_ingredient_to_process))
+        && is_mu_address(current_instruction().products.at(0)))
+      products.at(0).insert(products.at(0).begin(), /*alloc id*/0);
     assert(SIZE(products) == 1);  products.resize(2);  // push a new vector
     products.at(1).push_back(1);
     ++current_call().next_ingredient_to_process;
@@ -94,6 +97,18 @@ def f [
 ]
 +error: f: no ingredient to save in '11:num'
 
+:(scenario pass_null_ingredient_for_address)
+def main [
+  f 0
+]
+def f [
+  1:address:num <- next-ingredient
+]
++mem: storing 0 in location 2
+$error: 0
+
+//: another primitive: 'rewind-ingredients' to rescan ingredients from the start
+
 :(scenario rewind_ingredients)
 def main [
   f 2
@@ -124,6 +139,8 @@ case REWIND_INGREDIENTS: {
   break;
 }
 
+//: another primitive: 'ingredient' for random access
+
 :(scenario ingredient)
 def main [
   f 1, 2
diff --git a/028call_return.cc b/028call_return.cc
index c8c1bca6..aa5cb584 100644
--- a/028call_return.cc
+++ b/028call_return.cc
@@ -52,6 +52,10 @@ case RETURN: {
     trace(9998, "run") << "result " << i << " is " << to_string(ingredients.at(i)) << end();
   // make return products available to caller
   copy(ingredients.begin(), ingredients.end(), inserter(products, products.begin()));
+  for (int i = 0;  i < SIZE(current_instruction().products);  ++i) {
+    if (is_mu_address(current_instruction().products.at(i)) && scalar(ingredients.at(i)))
+      products.at(i).insert(products.at(i).begin(), /*alloc id*/0);
+  }
   // End Return
   break;  // continue to process rest of *caller* instruction
 }
diff --git a/030container.cc b/030container.cc
index f4aaafd4..b722e711 100644
--- a/030container.cc
+++ b/030container.cc
@@ -7,14 +7,16 @@ get_or_insert(Type, point);  // initialize
 get(Type, point).kind = CONTAINER;
 get(Type, point).name = "point";
 get(Type, point).elements.push_back(reagent("x:number"));
+get(Type, point).elements.back().set_value(0);
 get(Type, point).elements.push_back(reagent("y:number"));
+get(Type, point).elements.back().set_value(1);
 
 //: Containers can be copied around with a single instruction just like
 //: numbers, no matter how large they are.
 
 //: Tests in this layer often explicitly set up memory before reading it as a
-//: container. Don't do this in general. I'm tagging such cases with /unsafe;
-//: they'll be exceptions to later checks.
+//: container. Don't do this in general. I'm tagging exceptions with /unsafe to
+//: skip later checks.
 :(scenario copy_multiple_locations)
 def main [
   1:num <- copy 34
@@ -40,7 +42,9 @@ get_or_insert(Type, point_number);  // initialize
 get(Type, point_number).kind = CONTAINER;
 get(Type, point_number).name = "point-number";
 get(Type, point_number).elements.push_back(reagent("xy:point"));
+get(Type, point_number).elements.back().set_value(0);
 get(Type, point_number).elements.push_back(reagent("z:number"));
+get(Type, point_number).elements.back().set_value(1);
 
 :(scenario copy_handles_nested_container_elements)
 def main [
@@ -127,6 +131,10 @@ def main [
 //: 'get' takes a 'base' container and an 'offset' into it and returns the
 //: appropriate element of the container value.
 
+//: The offset is different from the distance (in memory locations) an element
+//: is at from the start. This is because elements can occupy multiple memory
+//: locations in the container.
+
 :(scenario get)
 def main [
   12:num <- copy 34
@@ -195,9 +203,7 @@ case GET: {
   // Update GET base_type in Run
   int offset = ingredients.at(1).at(0);
   if (offset < 0 || offset >= SIZE(get(Type, base_type->value).elements)) break;  // copied from Check above
-  int src = base_address;
-  for (int i = 0; i < offset; ++i)
-    src += size_of(element_type(base.type, i));
+  int src = element_location(base_address, offset, base.type);
   trace(9998, "run") << "address to copy is " << src << end();
   //: use base.type rather than base_type because later layers will introduce compound types
   reagent/*copy*/ element = element_type(base.type, offset);
@@ -222,6 +228,12 @@ const reagent element_type(const type_tree* type, int offset_value) {
   // End element_type Special-cases
   return element;
 }
+int element_location(int base_address, int offset, const type_tree* type) {
+  int result = base_address;
+  for (int i = 0; i < offset; ++i)
+    result += size_of(element_type(type, i));
+  return result;
+}
 
 :(scenario get_handles_nested_container_elements)
 def main [
@@ -352,14 +364,17 @@ case PUT: {
   // Update PUT base_type in Run
   int offset = ingredients.at(1).at(0);
   if (offset < 0 || offset >= SIZE(get(Type, base_type->value).elements)) break;  // copied from Check above
-  int address = base_address;
-  for (int i = 0; i < offset; ++i)
-    address += size_of(element_type(base.type, i));
+  int address = element_location(base_address, offset, base.type);
   trace(9998, "run") << "address to copy to is " << address << end();
   // optimization: directly write the element rather than updating 'product'
   // and writing the entire container
   // Write Memory in PUT in Run
   write_products = false;
+  if (is_mu_address(element_type(base.type, offset)) && is_literal(current_instruction().ingredients.at(2)) && current_instruction().ingredients.at(2).name == "0") {
+    trace("mem") << "storing 0 in location " << address << end();
+    put(Memory, address, /*alloc id*/0);
+    ++address;
+  }
   for (int i = 0;  i < SIZE(ingredients.at(2));  ++i) {
     trace("mem") << "storing " << no_scientific(ingredients.at(2).at(i)) << " in location " << address+i << end();
     put(Memory, address+i, ingredients.at(2).at(i));
@@ -377,6 +392,23 @@ def main [
 ]
 +error: main: product of 'put' must be first ingredient '1:point', but got '3:point'
 
+:(scenario put_null_address)
+container foo [
+  x:num
+  y:&:num
+  z:num
+]
+def main [
+  1:num <- copy 34
+  2:num <- copy 0  # alloc id
+  3:num <- copy 1000  # pretend address
+  4:num <- copy 36
+  put 1:foo, y:offset, 0
+]
++run: put {1: "foo"}, {y: "offset"}, {0: "literal"}
++mem: storing 0 in location 2
++mem: storing 0 in location 3
+
 //:: Allow containers to be defined in Mu code.
 
 :(scenarios load)
@@ -490,6 +522,7 @@ void insert_container(const string& command, kind_of_type kind, istream& in) {
       break;
     }
     info.elements.push_back(reagent(element));
+    info.elements.back().set_value(SIZE(info.elements)-1);
     expand_type_abbreviations(info.elements.back().type);  // todo: use abbreviation before declaration
     replace_unknown_types_with_unique_ordinals(info.elements.back().type, info);
     trace(9993, "parse") << "  element: " << to_string(info.elements.back()) << end();
diff --git a/032array.cc b/032array.cc
index 3bde42fb..d893b529 100644
--- a/032array.cc
+++ b/032array.cc
@@ -11,7 +11,7 @@ def main [
   # create an array occupying locations 1 (for the size) and 2-4 (for the elements)
   1:array:num:3 <- create-array
 ]
-+run: creating array of size 4
++run: creating array from 4 locations
 
 :(before "End Primitive Recipe Declarations")
 CREATE_ARRAY,
@@ -60,7 +60,7 @@ case CREATE_ARRAY: {
   trace("mem") << "storing " << array_length << " in location " << base_address << end();
   put(Memory, base_address, array_length);  // in array elements
   int size = size_of(product);  // in locations
-  trace(9998, "run") << "creating array of size " << size << end();
+  trace(9998, "run") << "creating array from " << size << " locations" << end();
   // initialize array
   for (int i = 1;  i <= size_of(product);  ++i)
     put(Memory, base_address+i, 0);
@@ -208,19 +208,23 @@ def main [
   2:num <- copy 14
   3:num <- copy 15
   4:num <- copy 16
-  5:num <- index 1:array:num:3, 0/index  # the index must be a non-negative whole number
+  10:num <- index 1:array:num:3, 0/index  # the index must be a non-negative whole number
 ]
-+mem: storing 14 in location 5
++mem: storing 14 in location 10
 
 :(scenario index_compound_element)
 def main [
   {1: (array (address number) 3)} <- create-array
-  2:num <- copy 14
-  3:num <- copy 15
-  4:num <- copy 16
-  5:address:num <- index {1: (array (address number) 3)}, 0
+  # skip alloc id
+  3:num <- copy 14
+  # skip alloc id
+  5:num <- copy 15
+  # skip alloc id
+  7:num <- copy 16
+  10:address:num <- index {1: (array (address number) 3)}, 0
 ]
-+mem: storing 14 in location 5
+# skip alloc id
++mem: storing 14 in location 11
 
 :(scenario index_direct_offset)
 def main [
@@ -228,10 +232,10 @@ def main [
   2:num <- copy 14
   3:num <- copy 15
   4:num <- copy 16
-  5:num <- copy 0
-  6:num <- index 1:array:num, 5:num
+  10:num <- copy 0
+  20:num <- index 1:array:num, 10:num
 ]
-+mem: storing 14 in location 6
++mem: storing 14 in location 20
 
 :(before "End Primitive Recipe Declarations")
 INDEX,
@@ -346,38 +350,38 @@ def main [
   2:num <- copy 14
   3:num <- copy 15
   4:num <- copy 16
-  5:num <- index 1:array:num:3, 1.5  # non-whole number
+  10:num <- index 1:array:num:3, 1.5  # non-whole number
 ]
 # fraction is truncated away
-+mem: storing 15 in location 5
++mem: storing 15 in location 10
 
 :(scenario index_out_of_bounds)
 % Hide_errors = true;
 def main [
-  1:array:num:3 <- create-array
-  2:num <- copy 14
-  3:num <- copy 15
-  4:num <- copy 16
-  5:num <- copy 14
-  6:num <- copy 15
-  7:num <- copy 16
-  index 1:array:num:3, 4  # less than size of array in locations, but larger than its length in elements
+  2:array:point:3 <- create-array
+  3:num <- copy 14
+  4:num <- copy 15
+  5:num <- copy 16
+  6:num <- copy 17
+  7:num <- copy 18
+  8:num <- copy 19
+  index 1:array:point:3/skip-alloc-id, 4  # less than size of array in locations, but larger than its length in elements
 ]
-+error: main: invalid index 4 in 'index 1:array:num:3, 4'
++error: main: invalid index 4 in 'index 1:array:point:3/skip-alloc-id, 4'
 
 :(scenario index_out_of_bounds_2)
 % Hide_errors = true;
 def main [
-  1:array:point:3 <- create-array
-  2:num <- copy 14
-  3:num <- copy 15
-  4:num <- copy 16
-  5:num <- copy 14
-  6:num <- copy 15
-  7:num <- copy 16
-  index 1:array:point, -1
+  2:array:point:3 <- create-array
+  3:num <- copy 14
+  4:num <- copy 15
+  5:num <- copy 16
+  6:num <- copy 14
+  7:num <- copy 15
+  8:num <- copy 16
+  index 1:array:point/skip-alloc-id, -1
 ]
-+error: main: invalid index -1 in 'index 1:array:point, -1'
++error: main: invalid index -1 in 'index 1:array:point/skip-alloc-id, -1'
 
 :(scenario index_product_type_mismatch)
 % Hide_errors = true;
diff --git a/033exclusive_container.cc b/033exclusive_container.cc
index 44161d7c..e4fa6398 100644
--- a/033exclusive_container.cc
+++ b/033exclusive_container.cc
@@ -12,12 +12,14 @@ get_or_insert(Type, tmp);  // initialize
 get(Type, tmp).kind = EXCLUSIVE_CONTAINER;
 get(Type, tmp).name = "number-or-point";
 get(Type, tmp).elements.push_back(reagent("i:number"));
+get(Type, tmp).elements.back().set_value(0);
 get(Type, tmp).elements.push_back(reagent("p:point"));
+get(Type, tmp).elements.back().set_value(1);
 }
 
-//: Tests in this layer often explicitly set up memory before reading it as a
-//: container. Don't do this in general. I'm tagging such cases with /unsafe;
-//: they'll be exceptions to later checks.
+//: Tests in this layer often explicitly set up memory before reading it as an
+//: array. Don't do this in general. I'm tagging exceptions with /raw to keep
+//: checks in future layers from flagging them.
 :(scenario copy_exclusive_container)
 # Copying exclusive containers copies all their contents and an extra location for the tag.
 def main [
diff --git a/034address.cc b/034address.cc
index bce51b2e..94c930bd 100644
--- a/034address.cc
+++ b/034address.cc
@@ -18,6 +18,37 @@
 //: write to the payload of an ingredient rather than its value, simply add
 //: the /lookup property to it. Modern computers provide efficient support for
 //: addresses and lookups, making this a realistic feature.
+//:
+//: To create addresses and allocate memory exclusively for their use, use
+//: 'new'. Memory is a finite resource so if the computer can't satisfy your
+//: request, 'new' may return a 0 (null) address.
+//:
+//: Computers these days have lots of memory so in practice we can often
+//: assume we'll never run out. If you start running out however, say in a
+//: long-running program, you'll need to switch mental gears and start
+//: husbanding our memory more carefully. The most important tool to avoid
+//: wasting memory is to 'abandon' an address when you don't need it anymore.
+//: That frees up the memory allocated to it to be reused in future calls to
+//: 'new'.
+
+//: Since memory can be reused multiple times, it can happen that you have a
+//: stale copy to an address that has since been abandoned and reused. Using
+//: the stale address is almost never safe, but it can be very hard to track
+//: down such copies because any errors caused by them may occur even millions
+//: of instructions after the copy or abandon instruction. To help track down
+//: such issues, Mu tracks an 'alloc id' for each allocation it makes. The
+//: first call to 'new' has an alloc id of 1, the second gets 2, and so on.
+//: The alloc id is never reused.
+:(before "End Globals")
+long long Next_alloc_id = 0;
+:(before "End Reset")
+Next_alloc_id = 0;
+
+//: The 'new' instruction records alloc ids both in the memory being allocated
+//: and *also* in the address. The 'abandon' instruction clears alloc ids in
+//: both places as well. Tracking alloc ids in this manner allows us to raise
+//: errors about stale addresses much earlier: 'lookup' operations always
+//: compare alloc ids between the address and its payload.
 
 //: todo: give 'new' a custodian ingredient. Following malloc/free is a temporary hack.
 
@@ -26,28 +57,30 @@
 # should get back different results
 def main [
   1:address:num/raw <- new number:type
-  2:address:num/raw <- new number:type
-  3:bool/raw <- equal 1:address:num/raw, 2:address:num/raw
+  3:address:num/raw <- new number:type
+  5:bool/raw <- equal 1:address:num/raw, 3:address:num/raw
 ]
++mem: storing 1000 in location 2
 +mem: storing 0 in location 3
 
 :(scenario new_array)
 # call 'new' with a second ingredient to allocate an array of some type rather than a single copy
 def main [
   1:address:array:num/raw <- new number:type, 5
-  2:address:num/raw <- new number:type
-  3:num/raw <- subtract 2:address:num/raw, 1:address:array:num/raw
+  3:address:num/raw <- new number:type
+  5:num/raw <- subtract 3:address:num/raw, 1:address:array:num/raw
 ]
 +run: {1: ("address" "array" "number"), "raw": ()} <- new {number: "type"}, {5: "literal"}
 +mem: array length is 5
++mem: storing 1000 in location 2
 # don't forget the extra location for array length
-+mem: storing 6 in location 3
++mem: storing 7 in location 5
 
 :(scenario dilated_reagent_with_new)
 def main [
   1:address:address:num <- new {(address number): type}
 ]
-+new: size of '(address number)' is 1
++new: size of '(address number)' is 2
 
 //: 'new' takes a weird 'type' as its first ingredient; don't error on it
 :(before "End Mu Types Initialization")
@@ -151,6 +184,13 @@ def main [
 ]
 $error: 0
 
+:(scenario equal_result_of_new_with_null)
+def main [
+  1:&:num <- new num:type
+  10:bool <- equal 1:&:num, 0
+]
++mem: storing 0 in location 10
+
 //: To implement 'new', a Mu transform turns all 'new' instructions into
 //: 'allocate' instructions that precompute the amount of memory they want to
 //: allocate.
@@ -221,15 +261,18 @@ case ALLOCATE: {
   int result = allocate(size);
   if (SIZE(current_instruction().ingredients) > 1) {
     // initialize array length
-    trace("mem") << "storing " << ingredients.at(1).at(0) << " in location " << result << end();
-    put(Memory, result, ingredients.at(1).at(0));
+    trace("mem") << "storing array length " << ingredients.at(1).at(0) << " in location " << result+/*skip alloc id*/1 << end();
+    put(Memory, result+/*skip alloc id*/1, ingredients.at(1).at(0));
   }
   products.resize(1);
+  products.at(0).push_back(0);
   products.at(0).push_back(result);
   break;
 }
 :(code)
 int allocate(int size) {
+  // include space for alloc id
+  ++size;
   trace("mem") << "allocating size " << size << end();
 //?   Total_alloc += size;
 //?   ++Num_alloc;
@@ -290,41 +333,41 @@ def main [
 :(scenario new_size)
 def main [
   11:address:num/raw <- new number:type
-  12:address:num/raw <- new number:type
-  13:num/raw <- subtract 12:address:num/raw, 11:address:num/raw
+  13:address:num/raw <- new number:type
+  15:num/raw <- subtract 13:address:num/raw, 11:address:num/raw
 ]
-# size of number
-+mem: storing 1 in location 13
+# size of number + alloc id
++mem: storing 2 in location 15
 
 :(scenario new_array_size)
 def main [
   1:address:array:num/raw <- new number:type, 5
-  2:address:num/raw <- new number:type
-  3:num/raw <- subtract 2:address:num/raw, 1:address:array:num/raw
+  3:address:num/raw <- new number:type
+  5:num/raw <- subtract 3:address:num/raw, 1:address:array:num/raw
 ]
 # 5 locations for array contents + array length
-+mem: storing 6 in location 3
++mem: storing 7 in location 5
 
 :(scenario new_empty_array)
 def main [
   1:address:array:num/raw <- new number:type, 0
-  2:address:num/raw <- new number:type
-  3:num/raw <- subtract 2:address:num/raw, 1:address:array:num/raw
+  3:address:num/raw <- new number:type
+  5:num/raw <- subtract 3:address:num/raw, 1:address:array:num/raw
 ]
 +run: {1: ("address" "array" "number"), "raw": ()} <- new {number: "type"}, {0: "literal"}
 +mem: array length is 0
 # one location for array length
-+mem: storing 1 in location 3
++mem: storing 2 in location 5
 
 //: If a routine runs out of its initial allocation, it should allocate more.
 :(scenario new_overflow)
-% Initial_memory_per_routine = 2;  // barely enough room for point allocation below
+% Initial_memory_per_routine = 3;  // barely enough room for point allocation below
 def main [
   1:address:num/raw <- new number:type
   2:address:point/raw <- new point:type  # not enough room in initial page
 ]
-+new: routine allocated memory from 1000 to 1002
-+new: routine allocated memory from 1002 to 1004
++new: routine allocated memory from 1000 to 1003
++new: routine allocated memory from 1003 to 1006
 
 :(scenario new_without_ingredient)
 % Hide_errors = true;
diff --git a/035lookup.cc b/035lookup.cc
index a2647f5d..9708ce5b 100644
--- a/035lookup.cc
+++ b/035lookup.cc
@@ -6,7 +6,8 @@
 :(scenario copy_indirect)
 def main [
   1:address:num <- copy 10/unsafe
-  10:num <- copy 34
+  # skip alloc id
+  11:num <- copy 34
   # This loads location 1 as an address and looks up *that* location.
   2:num <- copy 1:address:num/lookup
 ]
@@ -22,7 +23,7 @@ def main [
   1:address:num <- copy 10/unsafe
   1:address:num/lookup <- copy 34
 ]
-+mem: storing 34 in location 10
++mem: storing 34 in location 11
 
 :(before "End Preprocess write_memory(x, data)")
 canonize(x);
@@ -35,7 +36,7 @@ def main [
   1:address:num/lookup <- copy 34
 ]
 -mem: storing 34 in location 0
-+error: can't write to location 0 in '1:address:num/lookup <- copy 34'
++error: main: tried to lookup 0 in '1:address:num/lookup <- copy 34'
 
 //: attempts to /lookup address 0 always loudly fail
 :(scenario lookup_0_fails)
@@ -82,7 +83,7 @@ void lookup_memory(reagent& x) {
 }
 
 void lookup_memory_core(reagent& x, bool check_for_null) {
-  double address = x.value;
+  double address = x.value + /*skip alloc id in address*/1;
   double new_value = get_or_insert(Memory, address);
   trace("mem") << "location " << address << " contains " << no_scientific(new_value) << end();
   if (check_for_null && new_value == 0) {
@@ -94,12 +95,18 @@ void lookup_memory_core(reagent& x, bool check_for_null) {
       raise << "tried to lookup 0\n" << end();
     }
   }
-  x.set_value(new_value);
+  x.set_value(new_value+/*skip alloc id in payload*/1);
   drop_from_type(x, "address");
   drop_one_lookup(x);
 }
 
-:(before "End Preprocess types_strictly_match(reagent to, reagent from)")
+:(after "Begin types_coercible(reagent to, reagent from)")
+if (!canonize_type(to)) return false;
+if (!canonize_type(from)) return false;
+:(after "Begin types_match(reagent to, reagent from)")
+if (!canonize_type(to)) return false;
+if (!canonize_type(from)) return false;
+:(after "Begin types_strictly_match(reagent to, reagent from)")
 if (!canonize_type(to)) return false;
 if (!canonize_type(from)) return false;
 
@@ -157,30 +164,33 @@ void drop_one_lookup(reagent& r) {
 :(scenario get_indirect)
 def main [
   1:address:point <- copy 10/unsafe
-  10:num <- copy 34
-  11:num <- copy 35
-  2:num <- get 1:address:point/lookup, 0:offset
+  # skip alloc id
+  11:num <- copy 34
+  12:num <- copy 35
+  20:num <- get 1:address:point/lookup, 0:offset
 ]
-+mem: storing 34 in location 2
++mem: storing 34 in location 20
 
 :(scenario get_indirect2)
 def main [
   1:address:point <- copy 10/unsafe
-  10:num <- copy 34
-  11:num <- copy 35
-  2:address:num <- copy 20/unsafe
-  2:address:num/lookup <- get 1:address:point/lookup, 0:offset
+  # skip alloc id
+  11:num <- copy 94
+  12:num <- copy 95
+  20:address:num <- copy 30/unsafe
+  20:address:num/lookup <- get 1:address:point/lookup, 0:offset
 ]
-+mem: storing 34 in location 20
++mem: storing 94 in location 31
 
 :(scenario include_nonlookup_properties)
 def main [
   1:address:point <- copy 10/unsafe
-  10:num <- copy 34
-  11:num <- copy 35
-  2:num <- get 1:address:point/lookup/foo, 0:offset
+  # skip alloc id
+  11:num <- copy 34
+  12:num <- copy 35
+  20:num <- get 1:address:point/lookup/foo, 0:offset
 ]
-+mem: storing 34 in location 2
++mem: storing 34 in location 20
 
 :(after "Update GET base in Check")
 if (!canonize_type(base)) break;
@@ -192,11 +202,12 @@ canonize(base);
 :(scenario put_indirect)
 def main [
   1:address:point <- copy 10/unsafe
-  10:num <- copy 34
-  11:num <- copy 35
+  # skip alloc id
+  11:num <- copy 34
+  12:num <- copy 35
   1:address:point/lookup <- put 1:address:point/lookup, 0:offset, 36
 ]
-+mem: storing 36 in location 10
++mem: storing 36 in location 11
 
 :(after "Update PUT base in Check")
 if (!canonize_type(base)) break;
@@ -241,7 +252,7 @@ def main [
   11:num <- copy 14
   12:num <- copy 15
   13:num <- copy 16
-  1:address:array:num <- copy 10/unsafe
+  1:address:array:num <- copy 9/unsafe/skip-alloc-id
   2:array:num <- copy 1:address:array:num/lookup
 ]
 +mem: storing 3 in location 2
@@ -254,7 +265,7 @@ def main [
   1:address:array:num:3 <- copy 1000/unsafe  # pretend allocation
   1:address:array:num:3/lookup <- create-array
 ]
-+mem: storing 3 in location 1000
++mem: storing 3 in location 1001
 
 :(after "Update CREATE_ARRAY product in Check")
 if (!canonize_type(product)) break;
@@ -267,7 +278,7 @@ def main [
   11:num <- copy 14
   12:num <- copy 15
   13:num <- copy 16
-  1:address:array:num <- copy 10/unsafe
+  1:address:array:num <- copy 9/unsafe/skip-alloc-id
   2:num <- index 1:address:array:num/lookup, 1
 ]
 +mem: storing 15 in location 2
@@ -290,7 +301,7 @@ def main [
   11:num <- copy 14
   12:num <- copy 15
   13:num <- copy 16
-  1:address:array:num <- copy 10/unsafe
+  1:address:array:num <- copy 9/unsafe/skip-alloc-id
   1:address:array:num/lookup <- put-index 1:address:array:num/lookup, 1, 34
 ]
 +mem: storing 34 in location 12
@@ -301,7 +312,7 @@ def main [
   2:num <- copy 14
   3:num <- copy 15
   4:num <- copy 16
-  5:address:num <- copy 10/unsafe
+  5:address:num <- copy 9/unsafe/skip-alloc-id
   10:num <- copy 1
   1:array:num:3 <- put-index 1:array:num:3, 5:address:num/lookup, 34
 ]
@@ -314,7 +325,7 @@ def main [
   11:num <- copy 14
   12:num <- copy 15
   13:num <- copy 16
-  1:address:array:num <- copy 10/unsafe
+  1:address:array:num <- copy 9/unsafe/skip-alloc-id
   1:address:array:num <- put-index 1:address:array:num/lookup, 1, 34
 ]
 +error: main: product of 'put-index' must be first ingredient '1:address:array:num/lookup', but got '1:address:array:num'
@@ -337,7 +348,7 @@ def main [
   *5:address:num <- copy 34
   6:num <- copy *5:address:num
 ]
-+run: creating array of size 4
++run: creating array from 7 locations
 +mem: storing 34 in location 6
 
 :(before "Update PUT_INDEX base in Check")
@@ -358,7 +369,7 @@ def main [
   11:num <- copy 14
   12:num <- copy 15
   13:num <- copy 16
-  1:address:array:num <- copy 10/unsafe
+  1:address:array:num <- copy 9/unsafe/skip-alloc-id
   2:num <- length 1:address:array:num/lookup
 ]
 +mem: storing 3 in location 2
@@ -370,8 +381,8 @@ canonize(array);
 
 :(scenario maybe_convert_indirect)
 def main [
-  10:number-or-point <- merge 0/number, 34
-  1:address:number-or-point <- copy 10/unsafe
+  11:number-or-point <- merge 0/number, 34
+  1:address:number-or-point <- copy 10/unsafe/skip-alloc-id
   2:num, 3:bool <- maybe-convert 1:address:number-or-point/lookup, i:variant
 ]
 +mem: storing 1 in location 3
@@ -379,23 +390,23 @@ def main [
 
 :(scenario maybe_convert_indirect_2)
 def main [
-  10:number-or-point <- merge 0/number, 34
-  1:address:number-or-point <- copy 10/unsafe
-  2:address:num <- copy 20/unsafe
-  2:address:num/lookup, 3:bool <- maybe-convert 1:address:number-or-point/lookup, i:variant
+  11:number-or-point <- merge 0/number, 34
+  1:address:number-or-point <- copy 10/unsafe/skip-alloc-id
+  3:address:num <- copy 20/unsafe
+  3:address:num/lookup, 5:bool <- maybe-convert 1:address:number-or-point/lookup, i:variant
 ]
-+mem: storing 1 in location 3
-+mem: storing 34 in location 20
++mem: storing 1 in location 5
++mem: storing 34 in location 21
 
 :(scenario maybe_convert_indirect_3)
 def main [
-  10:number-or-point <- merge 0/number, 34
-  1:address:number-or-point <- copy 10/unsafe
-  2:address:bool <- copy 20/unsafe
-  3:num, 2:address:bool/lookup <- maybe-convert 1:address:number-or-point/lookup, i:variant
+  11:number-or-point <- merge 0/number, 34
+  1:address:number-or-point <- copy 10/unsafe/skip-alloc-id
+  3:address:bool <- copy 20/unsafe
+  5:num, 3:address:bool/lookup <- maybe-convert 1:address:number-or-point/lookup, i:variant
 ]
-+mem: storing 1 in location 20
-+mem: storing 34 in location 3
++mem: storing 1 in location 21
++mem: storing 34 in location 5
 
 :(before "Update MAYBE_CONVERT base in Check")
 if (!canonize_type(base)) break;
@@ -416,8 +427,9 @@ def main [
   1:address:number-or-point <- copy 10/unsafe
   1:address:number-or-point/lookup <- merge 0/number, 34
 ]
-+mem: storing 0 in location 10
-+mem: storing 34 in location 11
+# skip alloc id
++mem: storing 0 in location 11
++mem: storing 34 in location 12
 
 :(before "Update size_mismatch Check for MERGE(x)
 canonize(x);
@@ -427,7 +439,7 @@ canonize(x);
 :(scenario lookup_abbreviation)
 def main [
   1:address:number <- copy 10/unsafe
-  10:number <- copy 34
+  11:number <- copy 34
   3:number <- copy *1:address:number
 ]
 +parse: ingredient: {1: ("address" "number"), "lookup": ()}
diff --git a/037abandon.cc b/037abandon.cc
index 5a4adbd1..1c2bc395 100644
--- a/037abandon.cc
+++ b/037abandon.cc
@@ -3,14 +3,14 @@
 :(scenario new_reclaim)
 def main [
   1:address:num <- new number:type
-  2:num <- copy 1:address:num  # because 1 will get reset during abandon below
+  3:num <- copy 1:address:num  # because 1 will get reset during abandon below
   abandon 1:address:num
-  3:address:num <- new number:type  # must be same size as abandoned memory to reuse
-  4:num <- copy 3:address:num
-  5:bool <- equal 2:num, 4:num
+  4:address:num <- new number:type  # must be same size as abandoned memory to reuse
+  6:num <- copy 4:address:num
+  7:bool <- equal 3:num, 6:num
 ]
 # both allocations should have returned the same address
-+mem: storing 1 in location 5
++mem: storing 1 in location 7
 
 //: When abandoning addresses we'll save them to a 'free list', segregated by size.
 
@@ -39,7 +39,7 @@ case ABANDON: {
   for (int i = 0;  i < SIZE(current_instruction().ingredients);  ++i) {
     reagent/*copy*/ ingredient = current_instruction().ingredients.at(i);
     canonize(ingredient);
-    abandon(get_or_insert(Memory, ingredient.value), payload_size(ingredient));
+    abandon(get_or_insert(Memory, ingredient.value+/*skip alloc id*/1), payload_size(ingredient));
   }
   break;
 }
@@ -50,7 +50,7 @@ void abandon(int address, int payload_size) {
   for (int curr = address;  curr < address+payload_size;  ++curr)
     put(Memory, curr, 0);
   // append existing free list to address
-  trace("abandon") << "saving " << address << " in free-list of size " << payload_size << end();
+  trace("mem") << "saving " << address << " in free-list of size " << payload_size << end();
   put(Memory, address, get_or_insert(Current_routine->free_list, payload_size));
   put(Current_routine->free_list, payload_size, address);
 }
@@ -58,7 +58,7 @@ void abandon(int address, int payload_size) {
 int payload_size(reagent/*copy*/ x) {
   x.properties.push_back(pair<string, string_tree*>("lookup", NULL));
   lookup_memory_core(x, /*check_for_null*/false);
-  return size_of(x);
+  return size_of(x) + /*alloc id*/1;
 }
 
 :(after "Allocate Special-cases")
@@ -80,23 +80,23 @@ if (get_or_insert(Current_routine->free_list, size)) {
 :(scenario new_differing_size_no_reclaim)
 def main [
   1:address:num <- new number:type
-  2:num <- copy 1:address:num
+  3:num <- copy 1:address:num
   abandon 1:address:num
-  3:address:array:num <- new number:type, 2  # different size
-  4:num <- copy 3:address:array:num
-  5:bool <- equal 2:num, 4:num
+  4:address:array:num <- new number:type, 2  # different size
+  6:num <- copy 4:address:array:num
+  7:bool <- equal 3:num, 6:num
 ]
 # no reuse
-+mem: storing 0 in location 5
++mem: storing 0 in location 7
 
 :(scenario new_reclaim_array)
 def main [
   1:address:array:num <- new number:type, 2
-  2:num <- copy 1:address:array:num
+  3:num <- copy 1:address:array:num
   abandon 1:address:array:num
-  3:address:array:num <- new number:type, 2  # same size
-  4:num <- copy 3:address:array:num
-  5:bool <- equal 2:num, 4:num
+  4:address:array:num <- new number:type, 2  # same size
+  6:num <- copy 4:address:array:num
+  7:bool <- equal 3:num, 6:num
 ]
 # both calls to new returned identical addresses
-+mem: storing 1 in location 5
++mem: storing 1 in location 7
diff --git a/038new_text.cc b/038new_text.cc
index 4b666f1c..b2a5db75 100644
--- a/038new_text.cc
+++ b/038new_text.cc
@@ -4,23 +4,25 @@
 :(before "End Mu Types Initialization")
 put(Type_abbreviations, "text", new_type_tree("address:array:character"));
 
-:(scenario new_string)
+:(scenario new_text)
 def main [
   1:text <- new [abc def]
-  2:char <- index *1:text, 5
+  # location 2 is part of 1:text
+  3:char <- index *1:text, 5
 ]
 # number code for 'e'
-+mem: storing 101 in location 2
++mem: storing 101 in location 3
 
-:(scenario new_string_handles_unicode)
+:(scenario new_text_handles_unicode)
 def main [
   1:text <- new [a«c]
-  2:num <- length *1:text
-  3:char <- index *1:text, 1
+  # location 2 is part of 1:text
+  3:num <- length *1:text
+  4:char <- index *1:text, 1
 ]
-+mem: storing 3 in location 2
++mem: storing 3 in location 3
 # unicode for '«'
-+mem: storing 171 in location 3
++mem: storing 171 in location 4
 
 :(before "End NEW Check Special-cases")
 if (is_literal_text(inst.ingredients.at(0))) break;
@@ -29,6 +31,7 @@ if (inst.name == "new" && !inst.ingredients.empty() && is_literal_text(inst.ingr
 :(after "case NEW" following "Primitive Recipe Implementations")
   if (is_literal_text(current_instruction().ingredients.at(0))) {
     products.resize(1);
+    products.at(0).push_back(/*alloc id*/0);
     products.at(0).push_back(new_mu_text(current_instruction().ingredients.at(0).name));
     trace("mem") << "new string alloc: " << products.at(0).at(0) << end();
     break;
@@ -40,8 +43,9 @@ int new_mu_text(const string& contents) {
   int string_length = unicode_length(contents);
 //?   Total_alloc += string_length+1;
 //?   ++Num_alloc;
-  int result = allocate(string_length+/*array length*/1);
+  int result = allocate(/*array length*/1 + string_length);
   int curr_address = result;
+  ++curr_address;  // skip alloc id
   trace("mem") << "storing string length " << string_length << " in location " << curr_address << end();
   put(Memory, curr_address, string_length);
   ++curr_address;  // skip length
@@ -62,16 +66,16 @@ int new_mu_text(const string& contents) {
 
 //: a new kind of typo
 
-:(scenario string_literal_without_instruction)
+:(scenario literal_text_without_instruction)
 % Hide_errors = true;
 def main [
   [abc]
 ]
 +error: main: instruction '[abc]' has no recipe in '[abc]'
 
-//: stash recognizes strings
+//: stash recognizes texts
 
-:(scenario stash_string)
+:(scenario stash_text)
 def main [
   1:text <- new [abc]
   stash [foo:], 1:text
@@ -80,30 +84,29 @@ def main [
 
 :(before "End inspect Special-cases(r, data)")
 if (is_mu_text(r)) {
-  assert(scalar(data));
-  return read_mu_text(data.at(0));
+  return read_mu_text(data.at(/*skip alloc id*/1));
 }
 
 :(before "End $print Special-cases")
 else if (is_mu_text(current_instruction().ingredients.at(i))) {
-  cout << read_mu_text(ingredients.at(i).at(0));
+  cout << read_mu_text(ingredients.at(i).at(/*skip alloc id*/1));
 }
 
-:(scenario unicode_string)
+:(scenario unicode_text)
 def main [
   1:text <- new [♠]
   stash [foo:], 1:text
 ]
 +app: foo: ♠
 
-:(scenario stash_space_after_string)
+:(scenario stash_space_after_text)
 def main [
   1:text <- new [abc]
   stash 1:text, [foo]
 ]
 +app: abc foo
 
-:(scenario stash_string_as_array)
+:(scenario stash_text_as_array)
 def main [
   1:text <- new [abc]
   stash *1:text
@@ -114,15 +117,16 @@ def main [
 :(before "End Preprocess is_mu_text(reagent x)")
 if (!canonize_type(x)) return false;
 
-//: Allocate more to routine when initializing a literal string
-:(scenario new_string_overflow)
-% Initial_memory_per_routine = 2;
+//: Allocate more to routine when initializing a literal text
+:(scenario new_text_overflow)
+% Initial_memory_per_routine = 3;
 def main [
   1:address:num/raw <- new number:type
-  2:text/raw <- new [a]  # not enough room in initial page, if you take the array length into account
+  # location 2 is part of 1:address
+  3:text/raw <- new [a]  # not enough room in initial page, if you take the array length into account
 ]
-+new: routine allocated memory from 1000 to 1002
-+new: routine allocated memory from 1002 to 1004
++new: routine allocated memory from 1000 to 1003
++new: routine allocated memory from 1003 to 1006
 
 //: helpers
 :(code)
@@ -140,9 +144,9 @@ int unicode_length(const string& s) {
 
 string read_mu_text(int address) {
   if (address == 0) return "";
-  int length = get_or_insert(Memory, address);
+  int length = get_or_insert(Memory, address+/*alloc id*/1);
   if (length == 0) return "";
-  return read_mu_characters(address+1, length);
+  return read_mu_characters(address+/*alloc id*/1+/*length*/1, length);
 }
 
 string read_mu_characters(int start, int length) {
@@ -156,13 +160,21 @@ string read_mu_characters(int start, int length) {
 
 //: assert: perform sanity checks at runtime
 
-:(scenario assert)
+:(scenario assert_literal)
 % Hide_errors = true;  // '%' lines insert arbitrary C code into tests before calling 'run' with the lines below. Must be immediately after :(scenario) line.
 def main [
   assert 0, [this is an assert in Mu]
 ]
 +error: this is an assert in Mu
 
+:(scenario assert)
+% Hide_errors = true;  // '%' lines insert arbitrary C code into tests before calling 'run' with the lines below. Must be immediately after :(scenario) line.
+def main [
+  1:text <- new [this is an assert in Mu]
+  assert 0, 1:text
+]
++error: this is an assert in Mu
+
 :(before "End Primitive Recipe Declarations")
 ASSERT,
 :(before "End Primitive Recipe Numbers")
@@ -173,7 +185,7 @@ case ASSERT: {
     raise << maybe(get(Recipe, r).name) << "'assert' takes exactly two ingredients rather than '" << to_original_string(inst) << "'\n" << end();
     break;
   }
-  if (!is_mu_scalar(inst.ingredients.at(0))) {
+  if (!is_mu_address(inst.ingredients.at(0)) && !is_mu_scalar(inst.ingredients.at(0))) {
     raise << maybe(get(Recipe, r).name) << "'assert' requires a boolean for its first ingredient, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
     break;
   }
@@ -185,11 +197,11 @@ case ASSERT: {
 }
 :(before "End Primitive Recipe Implementations")
 case ASSERT: {
-  if (!ingredients.at(0).at(0)) {
+  if (!scalar_ingredient(ingredients, 0)) {
     if (is_literal_text(current_instruction().ingredients.at(1)))
       raise << current_instruction().ingredients.at(1).name << '\n' << end();
     else
-      raise << read_mu_text(ingredients.at(1).at(0)) << '\n' << end();
+      raise << read_mu_text(ingredients.at(1).at(/*skip alloc id*/1)) << '\n' << end();
     if (!Hide_errors) exit(1);
   }
   break;
diff --git a/042name.cc b/042name.cc
index f183962c..e1f35136 100644
--- a/042name.cc
+++ b/042name.cc
@@ -6,8 +6,8 @@
 def main [
   x:num <- copy 0
 ]
-+name: assign x 1
-+mem: storing 0 in location 1
++name: assign x 2
++mem: storing 0 in location 2
 
 :(scenarios transform)
 :(scenario transform_names_fails_on_use_before_define)
@@ -42,7 +42,7 @@ void transform_names(const recipe_ordinal r) {
   map<string, int>& names = Name[r];
   // store the indices 'used' so far in the map
   int& curr_idx = names[""];
-  ++curr_idx;  // avoid using index 0, benign skip in some other cases
+  curr_idx = 2;  // reserve indices 0 and 1 for the chaining slot in a later layer
   for (int i = 0;  i < SIZE(caller.steps);  ++i) {
     instruction& inst = caller.steps.at(i);
     // End transform_names(inst) Special-cases
@@ -135,13 +135,21 @@ bool is_compound_type_starting_with(const type_tree* type, const string& expecte
   return type->left->value == get(Type_ordinal, expected_name);
 }
 
-int find_element_name(const type_ordinal t, const string& name, const string& recipe_name) {
+int find_element_offset(const type_ordinal t, const string& name, const string& recipe_name) {
   const type_info& container = get(Type, t);
   for (int i = 0;  i < SIZE(container.elements);  ++i)
     if (container.elements.at(i).name == name) return i;
   raise << maybe(recipe_name) << "unknown element '" << name << "' in container '" << get(Type, t).name << "'\n" << end();
   return -1;
 }
+int find_element_location(int base_address, const string& name, const type_tree* type, const string& recipe_name) {
+  int offset = find_element_offset(get_base_type(type)->value, name, recipe_name);
+  if (offset == -1) return offset;
+  int result = base_address;
+  for (int i = 0; i < offset; ++i)
+    result += size_of(element_type(type, i));
+  return result;
+}
 
 bool is_numeric_location(const reagent& x) {
   if (is_literal(x)) return false;
@@ -170,26 +178,26 @@ def main [
   x:point <- merge 34, 35
   y:num <- copy 3
 ]
-+name: assign x 1
++name: assign x 2
 # skip location 2 because x occupies two locations
-+name: assign y 3
++name: assign y 4
 
 :(scenario transform_names_supports_static_arrays)
 def main [
   x:@:num:3 <- create-array
   y:num <- copy 3
 ]
-+name: assign x 1
++name: assign x 2
 # skip locations 2, 3, 4 because x occupies four locations
-+name: assign y 5
++name: assign y 6
 
 :(scenario transform_names_passes_dummy)
 # _ is just a dummy result that never gets consumed
 def main [
   _, x:num <- copy 0, 1
 ]
-+name: assign x 1
--name: assign _ 1
++name: assign x 2
+-name: assign _ 2
 
 //: an escape hatch to suppress name conversion that we'll use later
 :(scenarios run)
@@ -198,7 +206,7 @@ def main [
 def main [
   x:num/raw <- copy 0
 ]
--name: assign x 1
+-name: assign x 2
 +error: can't write to location 0 in 'x:num/raw <- copy 0'
 
 :(scenarios transform)
@@ -266,7 +274,7 @@ if (inst.name == "get" || inst.name == "get-location" || inst.name == "put") {
     // since first non-address in base type must be a container, we don't have to canonize
     type_ordinal base_type = skip_addresses(inst.ingredients.at(0).type);
     if (contains_key(Type, base_type)) {  // otherwise we'll raise an error elsewhere
-      inst.ingredients.at(1).set_value(find_element_name(base_type, inst.ingredients.at(1).name, get(Recipe, r).name));
+      inst.ingredients.at(1).set_value(find_element_offset(base_type, inst.ingredients.at(1).name, get(Recipe, r).name));
       trace(9993, "name") << "element " << inst.ingredients.at(1).name << " of type " << get(Type, base_type).name << " is at offset " << no_scientific(inst.ingredients.at(1).value) << end();
     }
   }
@@ -286,8 +294,8 @@ def main [
   a:point <- copy 0/unsafe
   b:num <- copy 0/unsafe
 ]
-+name: assign a 1
-+name: assign b 3
++name: assign a 2
++name: assign b 4
 
 //:: Support variant names for exclusive containers in 'maybe-convert'.
 
@@ -316,7 +324,7 @@ if (inst.name == "maybe-convert") {
     // since first non-address in base type must be an exclusive container, we don't have to canonize
     type_ordinal base_type = skip_addresses(inst.ingredients.at(0).type);
     if (contains_key(Type, base_type)) {  // otherwise we'll raise an error elsewhere
-      inst.ingredients.at(1).set_value(find_element_name(base_type, inst.ingredients.at(1).name, get(Recipe, r).name));
+      inst.ingredients.at(1).set_value(find_element_offset(base_type, inst.ingredients.at(1).name, get(Recipe, r).name));
       trace(9993, "name") << "variant " << inst.ingredients.at(1).name << " of type " << get(Type, base_type).name << " has tag " << no_scientific(inst.ingredients.at(1).value) << end();
     }
   }
diff --git a/043space.cc b/043space.cc
index f290a0b9..e62e9285 100644
--- a/043space.cc
+++ b/043space.cc
@@ -13,25 +13,31 @@
 put(Type_abbreviations, "space", new_type_tree("address:array:location"));
 
 :(scenario set_default_space)
-# if default-space is 10, and if an array of 5 locals lies from location 12 to 16 (inclusive),
-# then local 0 is really location 12, local 1 is really location 13, and so on.
+# if default-space is 10, then:
+#   10: alloc id
+#   11: array size
+#   12: local 0 (space for the chaining slot; described later; often unused)
+#   13: local 0 (space for the chaining slot; described later; often unused)
+#   14: local 2 (assuming it is a scalar)
+#   15: local 3
+#   ..and so on
 def main [
   # pretend address:array:location; in practice we'll use 'new'
-  10:num <- copy 5  # length
-  default-space:space <- copy 10/unsafe
-  1:num <- copy 23
+  11:num <- copy 5  # length
+  default-space:space <- copy 10/unsafe/skip-alloc-id
+  2:num <- copy 23
 ]
-+mem: storing 23 in location 12
++mem: storing 23 in location 14
 
 :(scenario lookup_sidesteps_default_space)
 def main [
   # pretend pointer from outside
-  2000:num <- copy 34
+  2001:num <- copy 34
   # pretend address:array:location; in practice we'll use 'new'
-  1000:num <- copy 5  # length
+  1001:num <- copy 5  # length
   # actual start of this recipe
-  default-space:space <- copy 1000/unsafe
-  1:&:num <- copy 2000/unsafe  # even local variables always contain raw addresses
+  default-space:space <- copy 1000/unsafe/skip-alloc-id
+  1:&:num <- copy 2000/unsafe/skip-alloc-id  # even local variables always contain raw addresses
   8:num/raw <- copy *1:&:num
 ]
 +mem: storing 34 in location 8
@@ -43,8 +49,9 @@ def main [
 def main [
   default-space:num, x:num <- copy 0, 1
 ]
-+name: assign x 1
++name: assign x 2
 -name: assign default-space 1
+-name: assign default-space 2
 
 :(before "End is_disqualified Special-cases")
 if (x.name == "default-space")
@@ -74,7 +81,7 @@ void absolutize(reagent& x) {
 
 //: hook replaced in a later layer
 int space_base(const reagent& x) {
-  return current_call().default_space ? current_call().default_space : 0;
+  return current_call().default_space ? (current_call().default_space + /*skip alloc id*/1) : 0;
 }
 
 int address(int offset, int base) {
@@ -95,9 +102,13 @@ int address(int offset, int base) {
 
 :(after "Begin Preprocess write_memory(x, data)")
 if (x.name == "default-space") {
-  if (!scalar(data) || !is_mu_space(x))
+  if (!is_mu_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;
+  }
+  double space_location = data.at(/*skip alloc id*/1);
+  trace("mem") << "storing " << no_scientific(space_location) << " to default_space" << end();
+  current_call().default_space = space_location;
   return;
 }
 :(code)
@@ -115,11 +126,13 @@ def main [
   default-space:space <- copy 10/unsafe
   1:space/raw <- copy default-space:space
 ]
-+mem: storing 10 in location 1
+# skip alloc id
++mem: storing 10 in location 2
 
 :(after "Begin Preprocess read_memory(x)")
 if (x.name == "default-space") {
   vector<double> result;
+  result.push_back(/*alloc id*/0);
   result.push_back(current_call().default_space);
   return result;
 }
@@ -129,13 +142,13 @@ if (x.name == "default-space") {
 :(scenario lookup_sidesteps_default_space_in_get)
 def main [
   # pretend pointer to container from outside
-  2000:num <- copy 34
-  2001:num <- copy 35
+  2001:num <- copy 34
+  2002:num <- copy 35
   # pretend address:array:location; in practice we'll use 'new'
-  1000:num <- copy 5  # length
+  1001:num <- copy 5  # length
   # actual start of this recipe
-  default-space:space <- copy 1000/unsafe
-  1:&:point <- copy 2000/unsafe
+  default-space:space <- copy 1000/unsafe/skip-alloc-id
+  1:&:point <- copy 2000/unsafe/skip-alloc-id
   9:num/raw <- get *1:&:point, 1:offset
 ]
 +mem: storing 35 in location 9
@@ -148,14 +161,14 @@ element.properties.push_back(pair<string, string_tree*>("raw", NULL));
 :(scenario lookup_sidesteps_default_space_in_index)
 def main [
   # pretend pointer to array from outside
-  2000:num <- copy 2  # length
-  2001:num <- copy 34
-  2002:num <- copy 35
+  2001:num <- copy 2  # length
+  2002:num <- copy 34
+  2003:num <- copy 35
   # pretend address:array:location; in practice we'll use 'new'
-  1000:num <- copy 5  # length
+  1001:num <- copy 5  # length
   # actual start of this recipe
-  default-space:space <- copy 1000/unsafe
-  1:&:@:num <- copy 2000/unsafe
+  default-space:space <- copy 1000/unsafe/skip-alloc-id
+  1:&:@:num <- copy 2000/unsafe/skip-alloc-id
   9:num/raw <- index *1:&:@:num, 1
 ]
 +mem: storing 35 in location 9
@@ -172,8 +185,8 @@ def main [
   x:num <- copy 0
   y:num <- copy 3
 ]
-# allocate space for x and y, as well as the chaining slot at 0
-+mem: array length is 3
+# allocate space for x and y, as well as the chaining slot at indices 0 and 1
++mem: array length is 4
 
 :(before "End is_disqualified Special-cases")
 if (x.name == "number-of-locals")
diff --git a/044space_surround.cc b/044space_surround.cc
index 310672be..8efda681 100644
--- a/044space_surround.cc
+++ b/044space_surround.cc
@@ -8,24 +8,24 @@
 # location 1 in space 1 refers to the space surrounding the default space, here 20.
 def main [
   # pretend address:array:location; in practice we'll use 'new'
-  10:num <- copy 5  # length
+  11:num <- copy 5  # length
   # pretend address:array:location; in practice we'll use 'new"
-  20:num <- copy 5  # length
+  21:num <- copy 5  # length
   # actual start of this recipe
-  default-space:space <- copy 10/unsafe
+  default-space:space <- copy 10/unsafe/skip-alloc-id
   #: later layers will explain the /names: property
-  0:space/names:dummy <- copy 20/unsafe
-  1:num <- copy 32
-  1:num/space:1 <- copy 33
+  0:space/names:dummy <- copy 20/unsafe/skip-alloc-id
+  2:num <- copy 32
+  2:num/space:1 <- copy 33
 ]
 def dummy [  # just for the /names: property above
 ]
-# chain space: 10 + (length) 1
-+mem: storing 20 in location 11
-# store to default space: 10 + (skip length) 1 + (index) 1
-+mem: storing 32 in location 12
-# store to chained space: (contents of location 12) 20 + (length) 1 + (index) 1
-+mem: storing 33 in location 22
+# write chained space: 10 + (alloc id for default-space) 1 + (length) 1 + (alloc id for chained space) 1
++mem: storing 20 in location 13
+# store to inside default space: 10 + (alloc id) 1 + (length) 1 + (index) 2
++mem: storing 32 in location 14
+# store to inside chained space: (contents of location 12) 20 + (alloc id) 1 + (length) 1 + (index) 2
++mem: storing 33 in location 24
 
 //: If you think of a space as a collection of variables with a common
 //: lifetime, surrounding allows managing shorter lifetimes inside a longer
@@ -33,14 +33,17 @@ def dummy [  # just for the /names: property above
 
 :(replace{} "int space_base(const reagent& x)")
 int space_base(const reagent& x) {
-  int base = current_call().default_space ? current_call().default_space : 0;
+  int base = current_call().default_space ? (current_call().default_space+/*skip alloc id*/1) : 0;
   return space_base(x, space_index(x), base);
 }
 
 int space_base(const reagent& x, int space_index, int base) {
+  trace("space") << "default_space is at location " << base << " with " << space_index << " chained spaces to go" << end();
   if (space_index == 0)
     return base;
-  return space_base(x, space_index-1, get_or_insert(Memory, base+/*skip length*/1));
+  double chained_space_address = base+/*skip length*/1+/*skip alloc id of chaining slot*/1;
+  double chained_space_base = get_or_insert(Memory, chained_space_address) + /*skip alloc id of chained space*/1;
+  return space_base(x, space_index-1, chained_space_base);
 }
 
 int space_index(const reagent& x) {
diff --git a/045closure_name.cc b/045closure_name.cc
index e478337d..f5c8d2aa 100644
--- a/045closure_name.cc
+++ b/045closure_name.cc
@@ -9,9 +9,9 @@
 :(scenario closure)
 def main [
   default-space:space <- new location:type, 30
-  1:space/names:new-counter <- new-counter
-  2:num/raw <- increment-counter 1:space/names:new-counter
-  3:num/raw <- increment-counter 1:space/names:new-counter
+  2:space/names:new-counter <- new-counter
+  10:num/raw <- increment-counter 2:space/names:new-counter
+  11:num/raw <- increment-counter 2:space/names:new-counter
 ]
 def new-counter [
   default-space:space <- new location:type, 30
@@ -27,7 +27,7 @@ def increment-counter [
   return y:num/space:1
 ]
 +name: lexically surrounding space for recipe increment-counter comes from new-counter
-+mem: storing 5 in location 3
++mem: storing 5 in location 11
 
 //: To make this work, compute the recipe that provides names for the
 //: surrounding space of each recipe.
diff --git a/046check_type_by_name.cc b/046check_type_by_name.cc
index df5a2a6d..8a023024 100644
--- a/046check_type_by_name.cc
+++ b/046check_type_by_name.cc
@@ -87,27 +87,27 @@ void check_type(set<reagent>& known, const reagent& x, const recipe& caller) {
 
 :(scenario transform_fills_in_missing_types)
 def main [
-  x:num <- copy 1
+  x:num <- copy 10
   y:num <- add x, 1
 ]
-# x is in location 1, y in location 2
-+mem: storing 2 in location 2
+# x is in location 2, y in location 3
++mem: storing 11 in location 3
 
 :(scenario transform_fills_in_missing_types_in_product)
 def main [
-  x:num <- copy 1
-  x <- copy 2
+  x:num <- copy 10
+  x <- copy 11
 ]
-# x is in location 1
-+mem: storing 2 in location 1
+# x is in location 2
++mem: storing 11 in location 2
 
 :(scenario transform_fills_in_missing_types_in_product_and_ingredient)
 def main [
-  x:num <- copy 1
+  x:num <- copy 10
   x <- add x, 1
 ]
 # x is in location 1
-+mem: storing 2 in location 1
++mem: storing 11 in location 2
 
 :(scenario transform_fills_in_missing_label_type)
 def main [
diff --git a/050scenario.cc b/050scenario.cc
index e84a18e9..c4f2541b 100644
--- a/050scenario.cc
+++ b/050scenario.cc
@@ -79,6 +79,7 @@ Scenario_names = Scenario_names_snapshot;
 :(before "End Command Handlers")
 else if (command == "scenario") {
   scenario result = parse_scenario(in);
+//?   result.name.clear();  // disable running scenarios
   if (!result.name.empty())
     Scenarios.push_back(result);
 }
diff --git a/053recipe_header.cc b/053recipe_header.cc
index 057234f9..b948ce61 100644
--- a/053recipe_header.cc
+++ b/053recipe_header.cc
@@ -207,6 +207,9 @@ case NEXT_INGREDIENT_WITHOUT_TYPECHECKING: {
   if (current_call().next_ingredient_to_process < SIZE(current_call().ingredient_atoms)) {
     products.push_back(
         current_call().ingredient_atoms.at(current_call().next_ingredient_to_process));
+    if (is_mu_scalar(current_call().ingredients.at(current_call().next_ingredient_to_process))
+        && is_mu_address(current_instruction().products.at(0)))
+      products.at(0).insert(products.at(0).begin(), /*alloc id*/0);
     assert(SIZE(products) == 1);  products.resize(2);  // push a new vector
     products.at(1).push_back(1);
     ++current_call().next_ingredient_to_process;
diff --git a/055shape_shifting_container.cc b/055shape_shifting_container.cc
index 0e7409d8..3bf1fe80 100644
--- a/055shape_shifting_container.cc
+++ b/055shape_shifting_container.cc
@@ -295,12 +295,12 @@ container foo:_a:_b [
   y:_b
 ]
 def main [
-  1:text <- new [abc]
-  {2: (foo number (address array character))} <- merge 34/x, 1:text/y
-  3:text <- get {2: (foo number (address array character))}, y:offset
-  4:bool <- equal 1:text, 3:text
+  10:text <- new [abc]
+  {20: (foo number (address array character))} <- merge 34/x, 10:text/y
+  30:text <- get {20: (foo number (address array character))}, y:offset
+  40:bool <- equal 10:text, 30:text
 ]
-+mem: storing 1 in location 4
++mem: storing 1 in location 40
 
 :(before "End element_type Special-cases")
 replace_type_ingredients(element, type, info, " while computing element type of container");
@@ -346,8 +346,8 @@ exclusive-container foo:_a [
 ]
 def main [
   1:text <- new [abc]
-  2:foo:point <- merge 0/variant, 34/xx, 35/xy
-  10:point, 20:bool <- maybe-convert 2:foo:point, 0/variant
+  3:foo:point <- merge 0/variant, 34/xx, 35/xy
+  10:point, 20:bool <- maybe-convert 3:foo:point, 0/variant
 ]
 +mem: storing 1 in location 20
 +mem: storing 35 in location 11
diff --git a/058to_text.cc b/058to_text.cc
index 8c86e36c..9cb14e14 100644
--- a/058to_text.cc
+++ b/058to_text.cc
@@ -18,6 +18,7 @@ case TO_TEXT: {
 :(before "End Primitive Recipe Implementations")
 case TO_TEXT: {
   products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
   products.at(0).push_back(new_mu_text(inspect(current_instruction().ingredients.at(0), ingredients.at(0))));
   break;
 }
diff --git a/060rewrite_literal_string.cc b/060rewrite_literal_string.cc
index f4ed9b4c..95e38924 100644
--- a/060rewrite_literal_string.cc
+++ b/060rewrite_literal_string.cc
@@ -19,6 +19,7 @@ Transform.push_back(rewrite_literal_string_to_text);  // idempotent
 set<string> recipes_taking_literal_strings;
 :(code)
 void initialize_transform_rewrite_literal_string_to_text() {
+  recipes_taking_literal_strings.insert("assert");
   recipes_taking_literal_strings.insert("$print");
   recipes_taking_literal_strings.insert("$dump-trace");
   recipes_taking_literal_strings.insert("$system");
diff --git a/065duplex_list.mu b/065duplex_list.mu
index 037cb923..b299fda7 100644
--- a/065duplex_list.mu
+++ b/065duplex_list.mu
@@ -393,12 +393,17 @@ def remove-between start:&:duplex-list:_elem, end:&:duplex-list:_elem/contained-
   # start->next = end
   *next <- put *next, prev:offset, 0
   *start <- put *start, next:offset, end
-  return-unless end
+  {
+    break-if end
+    stash [spliced:] next
+    return
+  }
   # end->prev->next = 0
   # end->prev = start
   prev:&:duplex-list:_elem <- get *end, prev:offset
   assert prev, [malformed duplex list - 2]
   *prev <- put *prev, next:offset, 0
+  stash [spliced:] next
   *end <- put *end, prev:offset, start
 ]
 
@@ -431,6 +436,9 @@ scenario remove-range [
     12 <- 15
     20 <- 0
   ]
+  trace-should-contain [
+    app: spliced: 16 <-> 17 <-> 18
+  ]
 ]
 
 scenario remove-range-to-final [
@@ -466,6 +474,49 @@ scenario remove-range-to-final [
     12 <- 18
     20 <- 0  # no more elements
   ]
+  trace-should-contain [
+    app: spliced: 15 <-> 16 <-> 17
+  ]
+]
+
+scenario remove-range-to-penultimate [
+  local-scope
+  # construct a duplex list with six elements [13, 14, 15, 16, 17, 18]
+  list:&:duplex-list:num <- push 18, 0
+  list <- push 17, list
+  list <- push 16, list
+  list <- push 15, list
+  list <- push 14, list
+  list <- push 13, list
+  run [
+    # delete 15 and 16
+    # start pointer: to the second element
+    list2:&:duplex-list:num <- next list
+    # end pointer: to the last (sixth) element
+    end:&:duplex-list:num <- next list2
+    end <- next end
+    end <- next end
+    remove-between list2, end
+    # now check the list
+    10:num/raw <- get *list, value:offset
+    list <- next list
+    11:num/raw <- get *list, value:offset
+    list <- next list
+    12:num/raw <- get *list, value:offset
+    list <- next list
+    13:num/raw <- get *list, value:offset
+    20:&:duplex-list:num/raw <- next list
+  ]
+  memory-should-contain [
+    10 <- 13
+    11 <- 14
+    12 <- 17
+    13 <- 18
+    20 <- 0  # no more elements
+  ]
+  trace-should-contain [
+    app: spliced: 15 <-> 16
+  ]
 ]
 
 scenario remove-range-empty [
diff --git a/069hash.cc b/069hash.cc
index 4400c1e8..1d2f706e 100644
--- a/069hash.cc
+++ b/069hash.cc
@@ -62,7 +62,7 @@ size_t hash_mu_address(size_t h, reagent& r) {
 }
 
 size_t hash_mu_text(size_t h, const reagent& r) {
-  string input = read_mu_text(get_or_insert(Memory, r.value));
+  string input = read_mu_text(get_or_insert(Memory, r.value+/*skip alloc id*/1));
   for (int i = 0;  i < SIZE(input);  ++i) {
     h = hash_iter(h, static_cast<size_t>(input.at(i)));
 //?     cerr << i << ": " << h << '\n';
@@ -319,11 +319,11 @@ def main [
 :(scenario hash_matches_old_version)
 def main [
   1:text <- new [abc]
-  2:num <- hash 1:text
-  3:num <- hash_old 1:text
-  4:bool <- equal 2:num, 3:num
+  3:num <- hash 1:text
+  4:num <- hash_old 1:text
+  5:bool <- equal 3:num, 4:num
 ]
-+mem: storing 1 in location 4
++mem: storing 1 in location 5
 
 :(before "End Primitive Recipe Declarations")
 HASH_OLD,
@@ -343,7 +343,7 @@ case HASH_OLD: {
 }
 :(before "End Primitive Recipe Implementations")
 case HASH_OLD: {
-  string input = read_mu_text(ingredients.at(0).at(0));
+  string input = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
   size_t h = 0 ;
 
   for (int i = 0;  i < SIZE(input);  ++i) {
diff --git a/074wait.cc b/074wait.cc
index eb17c8aa..47bbb0cc 100644
--- a/074wait.cc
+++ b/074wait.cc
@@ -262,22 +262,23 @@ def main [
 :(scenario get_location_indirect)
 # 'get-location' can read from container address
 def main [
-  1:num <- copy 10
-  10:num <- copy 34
-  11:num <- copy 35
+  1:&:point <- copy 10/unsafe
+  # skip alloc id
+  11:num <- copy 34
+  12:num <- copy 35
   4:location <- get-location 1:&:point/lookup, 0:offset
 ]
-+mem: storing 10 in location 4
++mem: storing 11 in location 4
 
 :(scenario get_location_indirect_2)
 def main [
-  1:num <- copy 10
-  10:num <- copy 34
-  11:num <- copy 35
+  1:&:point <- copy 10/unsafe
+  11:num <- copy 34
+  12:num <- copy 35
   4:&:num <- copy 20/unsafe
   4:&:location/lookup <- get-location 1:&:point/lookup, 0:offset
 ]
-+mem: storing 10 in location 20
++mem: storing 11 in location 21
 
 //: allow waiting on a routine to complete
 
diff --git a/082scenario_screen.cc b/082scenario_screen.cc
index 31cbfcc9..dc015fed 100644
--- a/082scenario_screen.cc
+++ b/082scenario_screen.cc
@@ -145,8 +145,14 @@ assert(Next_predefined_global_for_scenarios < Reserved_for_tests);
 
 :(before "End Globals")
 // Scenario Globals.
-extern const int SCREEN = Next_predefined_global_for_scenarios++;
+extern const int SCREEN = next_predefined_global_for_scenarios(/*size_of(address:screen)*/2);
 // End Scenario Globals.
+:(code)
+int next_predefined_global_for_scenarios(int size) {
+  int result = Next_predefined_global_for_scenarios;
+  Next_predefined_global_for_scenarios += size;
+  return result;
+}
 
 //: give 'screen' a fixed location in scenarios
 :(before "End Special Scenario Variable Names(r)")
@@ -250,19 +256,27 @@ struct raw_string_stream {
 
 :(code)
 void check_screen(const string& expected_contents, const int color) {
-  int screen_location = get_or_insert(Memory, SCREEN);
-  int data_offset = find_element_name(get(Type_ordinal, "screen"), "data", "");
-  assert(data_offset >= 0);
-  int screen_data_location = screen_location+data_offset;  // type: address:array:character
-  int screen_data_start = get_or_insert(Memory, screen_data_location);  // type: array:character
-  int width_offset = find_element_name(get(Type_ordinal, "screen"), "num-columns", "");
-  int screen_width = get_or_insert(Memory, screen_location+width_offset);
-  int height_offset = find_element_name(get(Type_ordinal, "screen"), "num-rows", "");
-  int screen_height = get_or_insert(Memory, screen_location+height_offset);
+  int screen_location = get_or_insert(Memory, SCREEN+/*skip address alloc id*/1) + /*skip payload alloc id*/1;
+  reagent screen("x:screen");  // just to ensure screen.type is reclaimed
+  int screen_data_location = find_element_location(screen_location, "data", screen.type, "check_screen");  // type: address:array:character
+  assert(screen_data_location >= 0);
+//?   cerr << "screen data is at location " << screen_data_location << '\n';
+  int screen_data_start = get_or_insert(Memory, screen_data_location+/*skip address alloc id*/1) + /*skip payload alloc id*/1;  // type: array:character
+//?   cerr << "screen data start is at " << screen_data_start << '\n';
+  int screen_width_location = find_element_location(screen_location, "num-columns", screen.type, "check_screen");
+//?   cerr << "screen width is at location " << screen_width_location << '\n';
+  int screen_width = get_or_insert(Memory, screen_width_location);
+//?   cerr << "screen width: " << screen_width << '\n';
+  int screen_height_location = find_element_location(screen_location, "num-rows", screen.type, "check_screen");
+//?   cerr << "screen height is at location " << screen_height_location << '\n';
+  int screen_height = get_or_insert(Memory, screen_height_location);
+//?   cerr << "screen height: " << screen_height << '\n';
+  int top_index_location= find_element_location(screen_location, "top-idx", screen.type, "check_screen");
+//?   cerr << "top of screen is at location " << top_index_location << '\n';
+  int top_index = get_or_insert(Memory, top_index_location);
+//?   cerr << "top of screen is index " << top_index << '\n';
   raw_string_stream cursor(expected_contents);
   // todo: too-long expected_contents should fail
-  int top_index_offset = find_element_name(get(Type_ordinal, "screen"), "top-idx", "");
-  int top_index = get_or_insert(Memory, screen_location+top_index_offset);
   for (int i=0, row=top_index/screen_width;  i < screen_height;  ++i, row=(row+1)%screen_height) {
     cursor.skip_whitespace_and_comments();
     if (cursor.at_end()) break;
@@ -385,18 +399,25 @@ case _DUMP_SCREEN: {
 
 :(code)
 void dump_screen() {
-  int screen_location = get_or_insert(Memory, SCREEN);
-  int width_offset = find_element_name(get(Type_ordinal, "screen"), "num-columns", "");
-  int screen_width = get_or_insert(Memory, screen_location+width_offset);
-  int height_offset = find_element_name(get(Type_ordinal, "screen"), "num-rows", "");
-  int screen_height = get_or_insert(Memory, screen_location+height_offset);
-  int data_offset = find_element_name(get(Type_ordinal, "screen"), "data", "");
-  assert(data_offset >= 0);
-  int screen_data_location = screen_location+data_offset;  // type: address:array:character
-  int screen_data_start = get_or_insert(Memory, screen_data_location);  // type: array:character
-  assert(get_or_insert(Memory, screen_data_start) == screen_width*screen_height);
-  int top_index_offset = find_element_name(get(Type_ordinal, "screen"), "top-idx", "");
-  int top_index = get_or_insert(Memory, screen_location+top_index_offset);
+  int screen_location = get_or_insert(Memory, SCREEN+/*skip address alloc id*/1) + /*skip payload alloc id*/1;
+  reagent screen("x:screen");  // just to ensure screen.type is reclaimed
+  int screen_data_location = find_element_location(screen_location, "data", screen.type, "check_screen");  // type: address:array:character
+  assert(screen_data_location >= 0);
+//?   cerr << "screen data is at location " << screen_data_location << '\n';
+  int screen_data_start = get_or_insert(Memory, screen_data_location+/*skip address alloc id*/1) + /*skip payload alloc id*/1;  // type: array:character
+//?   cerr << "screen data start is at " << screen_data_start << '\n';
+  int screen_width_location = find_element_location(screen_location, "num-columns", screen.type, "check_screen");
+//?   cerr << "screen width is at location " << screen_width_location << '\n';
+  int screen_width = get_or_insert(Memory, screen_width_location);
+//?   cerr << "screen width: " << screen_width << '\n';
+  int screen_height_location = find_element_location(screen_location, "num-rows", screen.type, "check_screen");
+//?   cerr << "screen height is at location " << screen_height_location << '\n';
+  int screen_height = get_or_insert(Memory, screen_height_location);
+//?   cerr << "screen height: " << screen_height << '\n';
+  int top_index_location= find_element_location(screen_location, "top-idx", screen.type, "check_screen");
+//?   cerr << "top of screen is at location " << top_index_location << '\n';
+  int top_index = get_or_insert(Memory, top_index_location);
+//?   cerr << "top of screen is index " << top_index << '\n';
   for (int i=0, row=top_index/screen_width;  i < screen_height;  ++i, row=(row+1)%screen_height) {
     cerr << '.';
     int curr = screen_data_start+/*length*/1+row*screen_width* /*size of screen-cell*/2;
diff --git a/085scenario_console.cc b/085scenario_console.cc
index 2c3ab4bc..31aa4fe7 100644
--- a/085scenario_console.cc
+++ b/085scenario_console.cc
@@ -34,7 +34,7 @@ scenario keyboard-in-scenario [
 ]
 
 :(before "End Scenario Globals")
-extern const int CONSOLE = Next_predefined_global_for_scenarios++;
+extern const int CONSOLE = next_predefined_global_for_scenarios(/*size_of(address:console)*/2);
 //: give 'console' a fixed location in scenarios
 :(before "End Special Scenario Variable Names(r)")
 Name[r]["console"] = CONSOLE;
@@ -61,8 +61,8 @@ case ASSUME_CONSOLE: {
   int size = /*length*/1 + num_events*size_of_event();
   int event_data_address = allocate(size);
   // store length
-  put(Memory, event_data_address, num_events);
-  int curr_address = event_data_address + /*skip length*/1;
+  put(Memory, event_data_address+/*skip alloc id*/1, num_events);
+  int curr_address = event_data_address + /*skip alloc id*/1 + /*skip length*/1;
   for (int i = 0;  i < SIZE(r.steps);  ++i) {
     const instruction& inst = r.steps.at(i);
     if (inst.name == "left-click") {
@@ -113,13 +113,13 @@ case ASSUME_CONSOLE: {
       }
     }
   }
-  assert(curr_address == event_data_address+size);
+  assert(curr_address == event_data_address+/*skip alloc id*/1+size);
   // wrap the array of events in a console object
   int console_address = allocate(size_of_console());
   trace("mem") << "storing console in " << console_address << end();
-  put(Memory, CONSOLE, console_address);
+  put(Memory, CONSOLE+/*skip alloc id*/1, console_address);
   trace("mem") << "storing console data in " << console_address+/*offset of 'data' in container 'events'*/1 << end();
-  put(Memory, console_address+/*offset of 'data' in container 'events'*/1, event_data_address);
+  put(Memory, console_address+/*skip alloc id*/1+/*offset of 'data' in container 'events'*/1+/*skip alloc id of 'data'*/1, event_data_address);
   break;
 }
 
diff --git a/087file.cc b/087file.cc
index 44da9b02..9fd056db 100644
--- a/087file.cc
+++ b/087file.cc
@@ -35,7 +35,7 @@ case _OPEN_FILE_FOR_READING: {
 }
 :(before "End Primitive Recipe Implementations")
 case _OPEN_FILE_FOR_READING: {
-  string filename = read_mu_text(ingredients.at(0).at(0));
+  string filename = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
   assert(sizeof(long long int) >= sizeof(FILE*));
   FILE* f = fopen(filename.c_str(), "r");
   long long int result = reinterpret_cast<long long int>(f);
@@ -70,7 +70,7 @@ case _OPEN_FILE_FOR_WRITING: {
 }
 :(before "End Primitive Recipe Implementations")
 case _OPEN_FILE_FOR_WRITING: {
-  string filename = read_mu_text(ingredients.at(0).at(0));
+  string filename = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
   assert(sizeof(long long int) >= sizeof(FILE*));
   long long int result = reinterpret_cast<long long int>(fopen(filename.c_str(), "w"));
   products.resize(1);
diff --git a/089scenario_filesystem.cc b/089scenario_filesystem.cc
index f14534ac..bacb61be 100644
--- a/089scenario_filesystem.cc
+++ b/089scenario_filesystem.cc
@@ -71,7 +71,7 @@ scenario escaping-file-contents [
 ]
 
 :(before "End Globals")
-extern const int RESOURCES = Next_predefined_global_for_scenarios++;
+extern const int RESOURCES = next_predefined_global_for_scenarios(/*size_of(address:resources)*/2);
 //: give 'resources' a fixed location in scenarios
 :(before "End Special Scenario Variable Names(r)")
 Name[r]["resources"] = RESOURCES;
@@ -203,26 +203,28 @@ string munge_resources_contents(const string& data, const string& filename, cons
 }
 
 void construct_resources_object(const map<string, string>& contents) {
-  int resources_data_address = allocate(SIZE(contents)*2 + /*array length*/1);
-  int curr = resources_data_address + /*skip length*/1;
+  int resources_data_address = allocate(SIZE(contents) * /*size of resource*/4 + /*array length*/1);
+  int curr = resources_data_address + /*skip alloc id*/1 + /*skip array length*/1;
   for (map<string, string>::const_iterator p = contents.begin();  p != contents.end();  ++p) {
+    ++curr;  // skip alloc id of resource.name
     put(Memory, curr, new_mu_text(p->first));
     trace("mem") << "storing file name " << get(Memory, curr) << " in location " << curr << end();
     ++curr;
+    ++curr;  // skip alloc id of resource.contents
     put(Memory, curr, new_mu_text(p->second));
     trace("mem") << "storing file contents " << get(Memory, curr) << " in location " << curr << end();
     ++curr;
   }
-  curr = resources_data_address;
-  put(Memory, curr, SIZE(contents));  // size of array
+  curr = resources_data_address + /*skip alloc id of resources.data*/1;
+  put(Memory, curr, SIZE(contents));  // array length
   trace("mem") << "storing resources size " << get(Memory, curr) << " in location " << curr << end();
   // wrap the resources data in a 'resources' object
   int resources_address = allocate(size_of_resources());
-  curr = resources_address+/*offset of 'data' element*/1;
+  curr = resources_address+/*alloc id*/1+/*offset of 'data' element*/1+/*skip alloc id of 'data' element*/1;
   put(Memory, curr, resources_data_address);
   trace("mem") << "storing resources data address " << resources_data_address << " in location " << curr << end();
   // save in product
-  put(Memory, RESOURCES, resources_address);
+  put(Memory, RESOURCES+/*skip alloc id*/1, resources_address);
   trace("mem") << "storing resources address " << resources_address << " in location " << RESOURCES << end();
 }
 
diff --git a/091socket.cc b/091socket.cc
index 7b6ca5b1..a0f3b948 100644
--- a/091socket.cc
+++ b/091socket.cc
@@ -40,7 +40,7 @@ case _OPEN_CLIENT_SOCKET: {
 }
 :(before "End Primitive Recipe Implementations")
 case _OPEN_CLIENT_SOCKET: {
-  string host = read_mu_text(ingredients.at(0).at(0));
+  string host = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
   int port = ingredients.at(1).at(0);
   socket_t* client = client_socket(host, port);
   products.resize(1);
diff --git a/101run_sandboxed.cc b/101run_sandboxed.cc
index a0b827e9..6d866b60 100644
--- a/101run_sandboxed.cc
+++ b/101run_sandboxed.cc
@@ -3,20 +3,23 @@
 
 :(scenario run_interactive_code)
 def main [
-  1:num <- copy 0
-  2:text <- new [1:num/raw <- copy 34]
-  run-sandboxed 2:text
-  3:num <- copy 1:num
+  1:num <- copy 0  # reserve space for the sandbox
+  10:text <- new [1:num/raw <- copy 34]
+#?   $print 10:num [|] 11:num [: ] 1000:num [|] *10:text [ (] 10:text [)] 10/newline
+  run-sandboxed 10:text
+  20:num <- copy 1:num
 ]
-+mem: storing 34 in location 3
+#? ?
++mem: storing 34 in location 20
 
 :(scenario run_interactive_empty)
 def main [
-  1:text <- copy 0/unsafe
-  2:text <- run-sandboxed 1:text
+  10:text <- copy 0/unsafe
+  20:text <- run-sandboxed 10:text
 ]
 # result is null
-+mem: storing 0 in location 2
++mem: storing 0 in location 20
++mem: storing 0 in location 21
 
 //: As the name suggests, 'run-sandboxed' will prevent certain operations that
 //: regular Mu code can perform.
@@ -52,12 +55,16 @@ case RUN_SANDBOXED: {
 }
 :(before "End Primitive Recipe Implementations")
 case RUN_SANDBOXED: {
-  bool new_code_pushed_to_stack = run_interactive(ingredients.at(0).at(0));
+  bool new_code_pushed_to_stack = run_interactive(ingredients.at(0).at(/*skip alloc id*/1));
   if (!new_code_pushed_to_stack) {
     products.resize(5);
+    products.at(0).push_back(/*alloc id*/0);
     products.at(0).push_back(0);
+    products.at(1).push_back(/*alloc id*/0);
     products.at(1).push_back(trace_error_contents());
+    products.at(2).push_back(/*alloc id*/0);
     products.at(2).push_back(0);
+    products.at(3).push_back(/*alloc id*/0);
     products.at(3).push_back(trace_app_contents());
     products.at(4).push_back(1);  // completed
     run_code_end();
@@ -90,6 +97,7 @@ string Save_trace_file;
 // all errors.
 // returns true if successfully called (no errors found during load and transform)
 bool run_interactive(int address) {
+//?   cerr << "run_interactive: " << address << '\n';
   assert(contains_key(Recipe_ordinal, "interactive") && get(Recipe_ordinal, "interactive") != 0);
   // try to sandbox the run as best you can
   // todo: test this
@@ -98,6 +106,7 @@ bool run_interactive(int address) {
       Memory.erase(i);
   }
   string command = trim(strip_comments(read_mu_text(address)));
+//?   cerr << "command: " << command << '\n';
   Name[get(Recipe_ordinal, "interactive")].clear();
   run_code_begin(/*should_stash_snapshots*/true);
   if (command.empty()) return false;
@@ -213,15 +222,20 @@ load(string(
 "]\n" +
 "recipe sandbox [\n" +
   "local-scope\n" +
+//?   "$print [aaa] 10/newline\n" +
   "screen:&:screen <- new-fake-screen 30, 5\n" +
   "routine-id:num <- start-running interactive, screen\n" +
   "limit-time routine-id, 100000/instructions\n" +
   "wait-for-routine routine-id\n" +
+//?   "$print [bbb] 10/newline\n" +
   "instructions-run:num <- number-of-instructions routine-id\n" +
   "stash instructions-run [instructions run]\n" +
   "sandbox-state:num <- routine-state routine-id\n" +
   "completed?:bool <- equal sandbox-state, 1/completed\n" +
+//?   "$print [completed: ] completed? 10/newline\n" +
   "output:text <- $most-recent-products\n" +
+//?   "$print [zzz] 10/newline\n" +
+//?   "$print output\n" +
   "errors:text <- save-errors\n" +
   "stashes:text <- save-app-trace\n" +
   "$cleanup-run-sandboxed\n" +
@@ -281,6 +295,7 @@ case _MOST_RECENT_PRODUCTS: {
 :(before "End Primitive Recipe Implementations")
 case _MOST_RECENT_PRODUCTS: {
   products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
   products.at(0).push_back(new_mu_text(Most_recent_products));
   break;
 }
@@ -296,6 +311,7 @@ case SAVE_ERRORS: {
 :(before "End Primitive Recipe Implementations")
 case SAVE_ERRORS: {
   products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
   products.at(0).push_back(trace_error_contents());
   break;
 }
@@ -311,6 +327,7 @@ case SAVE_APP_TRACE: {
 :(before "End Primitive Recipe Implementations")
 case SAVE_APP_TRACE: {
   products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
   products.at(0).push_back(trace_app_contents());
   break;
 }
@@ -332,64 +349,64 @@ case _CLEANUP_RUN_SANDBOXED: {
 :(scenario "run_interactive_converts_result_to_text")
 def main [
   # try to interactively add 2 and 2
-  1:text <- new [add 2, 2]
-  2:text <- run-sandboxed 1:text
-  10:@:char <- copy *2:text
+  10:text <- new [add 2, 2]
+  20:text <- run-sandboxed 10:text
+  30:@:char <- copy *20:text
 ]
 # first letter in the output should be '4' in unicode
-+mem: storing 52 in location 11
++mem: storing 52 in location 31
 
 :(scenario "run_interactive_ignores_products_in_nested_functions")
 def main [
-  1:text <- new [foo]
-  2:text <- run-sandboxed 1:text
-  10:@:char <- copy *2:text
+  10:text <- new [foo]
+  20:text <- run-sandboxed 10:text
+  30:@:char <- copy *20:text
 ]
 def foo [
-  20:num <- copy 1234
+  40:num <- copy 1234
   {
     break
     reply 5678
   }
 ]
 # no product should have been tracked
-+mem: storing 0 in location 10
++mem: storing 0 in location 30
 
 :(scenario "run_interactive_ignores_products_in_previous_instructions")
 def main [
-  1:text <- new [
+  10:text <- new [
     add 1, 1  # generates a product
     foo]  # no products
-  2:text <- run-sandboxed 1:text
-  10:@:char <- copy *2:text
+  20:text <- run-sandboxed 10:text
+  30:@:char <- copy *20:text
 ]
 def foo [
-  20:num <- copy 1234
+  40:num <- copy 1234
   {
     break
     reply 5678
   }
 ]
 # no product should have been tracked
-+mem: storing 0 in location 10
++mem: storing 0 in location 30
 
 :(scenario "run_interactive_remembers_products_before_final_label")
 def main [
-  1:text <- new [
+  10:text <- new [
     add 1, 1  # generates a product
     +foo]  # no products
-  2:text <- run-sandboxed 1:text
-  10:@:char <- copy *2:text
+  20:text <- run-sandboxed 10:text
+  30:@:char <- copy *20:text
 ]
 def foo [
-  20:num <- copy 1234
+  40:num <- copy 1234
   {
     break
     reply 5678
   }
 ]
 # product tracked
-+mem: storing 50 in location 11
++mem: storing 50 in location 31
 
 :(scenario "run_interactive_returns_text")
 def main [
@@ -399,38 +416,42 @@ def main [
     y:text <- new [b]
     z:text <- append x:text, y:text
   ]
-  2:text <- run-sandboxed 1:text
-  10:@:char <- copy *2:text
+  10:text <- run-sandboxed 1:text
+#?   $print 10:text 10/newline
+  20:@:char <- copy *10:text
 ]
 # output contains "ab"
-+mem: storing 97 in location 11
-+mem: storing 98 in location 12
+#? ?
++mem: storing 97 in location 21
++mem: storing 98 in location 22
 
 :(scenario "run_interactive_returns_errors")
 def main [
   # run a command that generates an error
-  1:text <- new [x:num <- copy 34
+  10:text <- new [x:num <- copy 34
 get x:num, foo:offset]
-  2:text, 3:text <- run-sandboxed 1:text
-  10:@:char <- copy *3:text
+  20:text, 30:text <- run-sandboxed 10:text
+  40:@:char <- copy *30:text
 ]
 # error should be "unknown element foo in container number"
-+mem: storing 117 in location 11
-+mem: storing 110 in location 12
-+mem: storing 107 in location 13
-+mem: storing 110 in location 14
++mem: storing 117 in location 41
++mem: storing 110 in location 42
++mem: storing 107 in location 43
++mem: storing 110 in location 44
 # ...
 
 :(scenario run_interactive_with_comment)
 def main [
   # 2 instructions, with a comment after the first
-  1:&:@:num <- new [a:num <- copy 0  # abc
+  10:text <- new [a:num <- copy 0  # abc
 b:num <- copy 0
 ]
-  2:text, 3:text <- run-sandboxed 1:text
+  20:text, 30:text <- run-sandboxed 10:text
 ]
 # no errors
-+mem: storing 0 in location 3
+# skip alloc id
++mem: storing 0 in location 30
++mem: storing 0 in location 31
 
 :(after "Running One Instruction")
 if (Track_most_recent_products && SIZE(Current_routine->calls) == Call_depth_to_track_most_recent_products_at
@@ -441,6 +462,7 @@ if (Track_most_recent_products && SIZE(Current_routine->calls) == Call_depth_to_
 :(before "End Running One Instruction")
 if (Track_most_recent_products && SIZE(Current_routine->calls) == Call_depth_to_track_most_recent_products_at) {
   Most_recent_products = track_most_recent_products(current_instruction(), products);
+//?   cerr << "most recent products: " << Most_recent_products << '\n';
 }
 :(code)
 string track_most_recent_products(const instruction& instruction, const vector<vector<double> >& products) {
@@ -458,8 +480,8 @@ string track_most_recent_products(const instruction& instruction, const vector<v
     //    => abc
     if (i < SIZE(instruction.products)) {
       if (is_mu_text(instruction.products.at(i))) {
-        if (!scalar(products.at(i))) continue;  // error handled elsewhere
-        out << read_mu_text(products.at(i).at(0)) << '\n';
+        if (SIZE(products.at(i)) != 2) continue;  // weak silent check for address
+        out << read_mu_text(products.at(i).at(/*skip alloc id*/1)) << '\n';
         continue;
       }
     }
@@ -562,6 +584,7 @@ case RELOAD: {
   Sandbox_mode = false;
   Current_routine = save_current_routine;
   products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
   products.at(0).push_back(trace_error_contents());
   run_code_end();  // wait until we're done with the trace contents
   break;
diff --git a/edit/001-editor.mu b/edit/001-editor.mu
index 8855395a..036ef07a 100644
--- a/edit/001-editor.mu
+++ b/edit/001-editor.mu
@@ -81,18 +81,20 @@ scenario editor-initializes-without-data [
   assume-screen 5/width, 3/height
   run [
     e:&:editor <- new-editor 0/data, 2/left, 5/right
-    2:editor/raw <- copy *e
+    1:editor/raw <- copy *e
   ]
   memory-should-contain [
-    # 2 (data) <- just the § sentinel
-    # 3 (top of screen) <- the § sentinel
-    4 <- 0  # bottom-of-screen; null since text fits on screen
-    # 5 (before cursor) <- the § sentinel
-    6 <- 2  # left
-    7 <- 4  # right  (inclusive)
-    8 <- 0  # bottom (not set until render)
-    9 <- 1  # cursor row
-    10 <- 2  # cursor column
+    # 1,2 (data) <- just the § sentinel
+    # 3,4 (top of screen) <- the § sentinel
+    # 5 (bottom of screen) <- null since text fits on screen
+    5 <- 0
+    6 <- 0
+    # 7,8 (before cursor) <- the § sentinel
+    9 <- 2  # left
+    10 <- 4  # right  (inclusive)
+    11 <- 0  # bottom (not set until render)
+    12 <- 1  # cursor row
+    13 <- 2  # cursor column
   ]
   screen-should-contain [
     .     .
diff --git a/edit/002-typing.mu b/edit/002-typing.mu
index 47885c4f..67fe76a0 100644
--- a/edit/002-typing.mu
+++ b/edit/002-typing.mu
@@ -280,7 +280,11 @@ scenario editor-handles-empty-event-queue [
   assume-screen 10/width, 5/height
   e:&:editor <- new-editor [abc], 0/left, 10/right
   editor-render screen, e
+#?   x:num <- get *screen, num-rows:offset
+#?   $print [a: ] x 10/newline
   assume-console []
+#?   x:num <- get *screen, num-rows:offset
+#?   $print [z: ] x 10/newline
   run [
     editor-event-loop screen, console, e
   ]
diff --git a/edit/003-shortcuts.mu b/edit/003-shortcuts.mu
index 02ea77d0..78c6e49f 100644
--- a/edit/003-shortcuts.mu
+++ b/edit/003-shortcuts.mu
@@ -2006,7 +2006,13 @@ after <handle-special-character> [
     delete-to-start-of-line?:bool <- equal c, 21/ctrl-u
     break-unless delete-to-start-of-line?
     <begin-delete-to-start-of-line>
+    $print [before: ] cursor-row [ ] cursor-column 10/newline
     deleted-cells:&:duplex-list:char <- delete-to-start-of-line editor
+    x:text <- to-text deleted-cells
+    $print x 10/newline
+    cursor-row <- get *editor, cursor-row:offset
+    cursor-column <- get *editor, cursor-column:offset
+    $print [after: ] cursor-row [ ] cursor-column 10/newline
     <end-delete-to-start-of-line>
     go-render?:bool <- minimal-render-for-ctrl-u screen, editor, deleted-cells
     return
@@ -2016,6 +2022,7 @@ after <handle-special-character> [
 def minimal-render-for-ctrl-u screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
   local-scope
   load-inputs
+  $print [minimal render for ctrl-u] 10/newline
   curr-column:num <- get *editor, cursor-column:offset
   # accumulate the current line as text and render it
   buf:&:buffer:char <- new-buffer 30  # accumulator for the text we need to render
@@ -2025,6 +2032,7 @@ def minimal-render-for-ctrl-u screen:&:screen, editor:&:editor, deleted-cells:&:
   {
     # if we have a wrapped line, give up and render the whole screen
     wrap?:bool <- greater-or-equal i, right
+    $print [wrap? ] wrap? 10/newline
     return-if wrap?, 1/go-render
     curr <- next curr
     break-unless curr
@@ -2061,6 +2069,7 @@ def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor
   {
     at-start-of-text?:bool <- equal start, init
     break-if at-start-of-text?
+    $print [0] 10/newline
     curr:char <- get *start, value:offset
     at-start-of-line?:bool <- equal curr, 10/newline
     break-if at-start-of-line?
@@ -2071,14 +2080,23 @@ def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor
     assert start, [delete-to-start-of-line tried to move before start of text]
     loop
   }
+  $print [1] 10/newline
   # snip it out
   result:&:duplex-list:char <- next start
+  x:text <- to-text start
+  $print [start: ] x 10/newline
+  x:text <- to-text end
+  $print [end: ] x 10/newline
   remove-between start, end
+  x:text <- to-text result
+  $print [snip: ] x 10/newline
   # update top-of-screen if it's just been invalidated
   {
     break-unless update-top-of-screen?
+    $print [2] 10/newline
     put *editor, top-of-screen:offset, start
   }
+  $print [3] 10/newline
   # adjust cursor
   before-cursor <- copy start
   *editor <- put *editor, before-cursor:offset, before-cursor
@@ -2089,17 +2107,22 @@ def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor
   width:num <- subtract right, left
   num-deleted:num <- length result
   cursor-row-adjustment:num <- divide-with-remainder num-deleted, width
+  $print [adj ] num-deleted [/] width [=] cursor-row-adjustment 10/newline
   return-unless cursor-row-adjustment
+  $print [4] 10/newline
   cursor-row:num <- get *editor, cursor-row:offset
   cursor-row-in-editor:num <- subtract cursor-row, 1  # ignore menubar
   at-top?:bool <- lesser-or-equal cursor-row-in-editor, cursor-row-adjustment
   {
     break-unless at-top?
+    $print [5] 10/newline
     cursor-row <- copy 1  # top of editor, below menubar
   }
   {
     break-if at-top?
+    $print [6] 10/newline
     cursor-row <- subtract cursor-row, cursor-row-adjustment
+    $print cursor-row 10/newline
   }
   put *editor, cursor-row:offset, cursor-row
 ]
diff --git a/index.html b/index.html
index 88e9b14a..dde93753 100644
--- a/index.html
+++ b/index.html
@@ -158,18 +158,16 @@ for gradually constructing long strings in a piecemeal fashion.
 space at run-time as pointers or <em>addresses</em>. All Mu instructions can
 dereference or <a href='html/035lookup.cc.html'><em>lookup</em></a> addresses
 of values in addition to operating on regular values. These addresses are
-manually managed like C. However, all allocations are transparently
-reference-counted or <a href='html/036refcount.cc.html'><em>refcounted</em></a>,
-with every copy of a pointer updating refcounts appropriately. When the
-refcount of an allocation drops to zero it is transparently <a href='html/037abandon.cc.html'>reclaimed</a>
-and made available to future allocations. By construction it is impossible to
-reclaim memory prematurely, while some other part of a program is still
-pointing to it. This eliminates a whole class of undefined behavior and
-security vulnerabilities that plague C. Compared to Rust, Mu pays some
-additional runtime cost in exchange for C-like flexibility (you can copy
-addresses around all you like, and write from any copy of an address) and
-simpler implementation (no static analysis). Mu by convention abbreviates type
-<tt>address</tt> to <tt>&amp;</tt>.
+manually managed like C, and can be reclaimed using the <a href='html/037abandon.cc.html'><tt>abandon</tt></a>
+instruction. To ensure that stale addresses aren't used after being
+abandoned/reused, each allocation gets a unique <em>alloc id</em> that is also
+stored in the address returned. The lookup operation ensures that the alloc id
+of an address matches that of its payload. This eliminates a whole class of
+undefined behavior and security vulnerabilities that plague C. Compared to
+Rust, Mu pays some additional runtime cost in exchange for C-like flexibility
+(you can copy addresses around all you like, and write from any copy of an
+address) and simpler implementation (no static analysis). Mu by convention
+abbreviates type <tt>address</tt> to <tt>&amp;</tt>.
 
 <p/>Support for higher-order recipes that can pass <a href='html/072recipe.cc.html'>recipes</a>
 around like any other value.