about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--browse_trace/browse_trace.cc1
-rw-r--r--subx/010---vm.cc7
-rw-r--r--subx/013direct_addressing.cc1
-rw-r--r--subx/014indirect_addressing.cc9
-rw-r--r--subx/017jump_disp8.cc50
-rw-r--r--subx/018jump_disp32.cc56
-rw-r--r--subx/021byte_addressing.cc2
-rw-r--r--subx/031check_operands.cc7
-rw-r--r--subx/039debug.cc18
-rw-r--r--subx/054string-equal.subx94
-rw-r--r--subx/055stream.subx2
-rw-r--r--subx/056trace.subx832
-rw-r--r--subx/058stream-equal.subx13
-rw-r--r--subx/059stop.subx7
-rw-r--r--subx/060read.subx4
-rw-r--r--subx/061read-byte.subx8
-rw-r--r--subx/065hex.subx205
-rw-r--r--subx/066write-buffered.subx10
-rw-r--r--subx/067print-int.subx24
-rw-r--r--subx/071read-line.subx7
-rw-r--r--subx/072slice.subx303
-rw-r--r--subx/073next-token.subx23
-rw-r--r--subx/074print-int-decimal.subx10
-rw-r--r--subx/075array-equal.subx629
-rw-r--r--subx/076zero-out.subx84
-rw-r--r--subx/Readme.md18
-rwxr-xr-xsubx/apps/assortbin22514 -> 33376 bytes
-rw-r--r--subx/apps/assort.subx596
-rwxr-xr-xsubx/apps/crenshaw2-1bin19682 -> 23839 bytes
-rw-r--r--subx/apps/crenshaw2-1.subx13
-rwxr-xr-xsubx/apps/crenshaw2-1bbin20241 -> 24398 bytes
-rw-r--r--subx/apps/crenshaw2-1b.subx13
-rwxr-xr-xsubx/apps/dquotesbin27193 -> 39729 bytes
-rw-r--r--subx/apps/dquotes.subx173
-rwxr-xr-xsubx/apps/factorialbin18598 -> 22755 bytes
-rw-r--r--subx/apps/factorial.subx13
-rwxr-xr-xsubx/apps/handlebin19478 -> 23614 bytes
-rwxr-xr-xsubx/apps/hexbin22691 -> 26848 bytes
-rw-r--r--subx/apps/hex.subx14
-rwxr-xr-xsubx/apps/packbin37323 -> 45742 bytes
-rw-r--r--subx/apps/pack.subx1499
-rw-r--r--subx/apps/subx-common.subx2889
-rwxr-xr-xsubx/apps/surveybin0 -> 40444 bytes
-rw-r--r--subx/apps/survey.subx3993
-rwxr-xr-xsubx/dgen28
-rwxr-xr-xsubx/drun22
-rwxr-xr-xsubx/gen28
-rwxr-xr-xsubx/run22
-rwxr-xr-xsubx/run_one_test.sh22
-rw-r--r--subx/run_one_test.subx30
-rwxr-xr-xsubx/test_apps10
-rw-r--r--[l---------]subx/vimrc.vim96
-rw-r--r--vimrc.vim47
53 files changed, 9228 insertions, 2704 deletions
diff --git a/browse_trace/browse_trace.cc b/browse_trace/browse_trace.cc
index abfd6daa..33463ebc 100644
--- a/browse_trace/browse_trace.cc
+++ b/browse_trace/browse_trace.cc
@@ -273,6 +273,7 @@ int main(int argc, char* argv[]) {
         search(Current_search_pattern, opposite(Current_search_direction));
     }
   }
+  tb_clear();
   tb_shutdown();
   return 0;
 }
diff --git a/subx/010---vm.cc b/subx/010---vm.cc
index 18f69035..de8d51b1 100644
--- a/subx/010---vm.cc
+++ b/subx/010---vm.cc
@@ -203,6 +203,7 @@ inline uint8_t* mem_addr_u8(uint32_t addr) {
   if (result == NULL) {
     if (Trace_file) Trace_file.flush();
     raise << "Tried to access uninitialized memory at address 0x" << HEXWORD << addr << '\n' << end();
+    exit(1);
   }
   return result;
 }
@@ -293,7 +294,6 @@ void run_one_instruction() {
     // End Two-Byte Opcodes Starting With 0f
     default:
       cerr << "unrecognized second opcode after 0f: " << HEXBYTE << NUM(op2) << '\n';
-      DUMP("");
       exit(1);
     }
     break;
@@ -305,13 +305,11 @@ void run_one_instruction() {
       // End Three-Byte Opcodes Starting With f2 0f
       default:
         cerr << "unrecognized third opcode after f2 0f: " << HEXBYTE << NUM(op3) << '\n';
-        DUMP("");
         exit(1);
       }
       break;
     default:
       cerr << "unrecognized second opcode after f2: " << HEXBYTE << NUM(op2) << '\n';
-      DUMP("");
       exit(1);
     }
     break;
@@ -323,19 +321,16 @@ void run_one_instruction() {
       // End Three-Byte Opcodes Starting With f3 0f
       default:
         cerr << "unrecognized third opcode after f3 0f: " << HEXBYTE << NUM(op3) << '\n';
-        DUMP("");
         exit(1);
       }
       break;
     default:
       cerr << "unrecognized second opcode after f3: " << HEXBYTE << NUM(op2) << '\n';
-      DUMP("");
       exit(1);
     }
     break;
   default:
     cerr << "unrecognized opcode: " << HEXBYTE << NUM(op) << '\n';
-    DUMP("");
     exit(1);
   }
 }
diff --git a/subx/013direct_addressing.cc b/subx/013direct_addressing.cc
index ca76acfb..513cb61b 100644
--- a/subx/013direct_addressing.cc
+++ b/subx/013direct_addressing.cc
@@ -1103,7 +1103,6 @@ case 0xff: {
     }
     default:
       cerr << "unrecognized subop for ff: " << HEXBYTE << NUM(subop) << '\n';
-      DUMP("");
       exit(1);
     // End Op ff Subops
   }
diff --git a/subx/014indirect_addressing.cc b/subx/014indirect_addressing.cc
index dd00c614..3767c46d 100644
--- a/subx/014indirect_addressing.cc
+++ b/subx/014indirect_addressing.cc
@@ -874,7 +874,7 @@ void test_add_r32_to_mem_at_r32_plus_disp8() {
 }
 
 :(before "End Mod Special-cases(addr)")
-case 1:  // indirect + disp8 addressing
+case 1: {  // indirect + disp8 addressing
   switch (rm) {
   default:
     addr = Reg[rm].u;
@@ -882,11 +882,16 @@ case 1:  // indirect + disp8 addressing
     break;
   // End Mod 1 Special-cases(addr)
   }
+  int8_t displacement = static_cast<int8_t>(next());
   if (addr > 0) {
-    addr += static_cast<int8_t>(next());
+    addr += displacement;
     trace(Callstack_depth+1, "run") << "effective address is 0x" << HEXWORD << addr << " (after adding disp8)" << end();
   }
+  else {
+    trace(Callstack_depth+1, "run") << "null address; skipping displacement" << end();
+  }
   break;
+}
 
 :(code)
 void test_add_r32_to_mem_at_r32_plus_negative_disp8() {
diff --git a/subx/017jump_disp8.cc b/subx/017jump_disp8.cc
index 086765c0..d7558401 100644
--- a/subx/017jump_disp8.cc
+++ b/subx/017jump_disp8.cc
@@ -6,7 +6,7 @@
 put_new(Name, "eb", "jump disp8 bytes away (jmp)");
 
 :(code)
-void test_jump_rel8() {
+void test_jump_disp8() {
   run(
       "== code 0x1\n"
       // op     ModR/M  SIB   displacement  immediate
@@ -23,7 +23,7 @@ void test_jump_rel8() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0xeb: {  // jump rel8
+case 0xeb: {  // jump disp8
   int8_t offset = static_cast<int>(next());
   trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
   EIP += offset;
@@ -36,7 +36,7 @@ case 0xeb: {  // jump rel8
 put_new(Name, "74", "jump disp8 bytes away if equal, if ZF is set (jcc/jz/je)");
 
 :(code)
-void test_je_rel8_success() {
+void test_je_disp8_success() {
   ZF = true;
   run(
       "== code 0x1\n"
@@ -54,7 +54,7 @@ void test_je_rel8_success() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x74: {  // jump rel8 if ZF
+case 0x74: {  // jump disp8 if ZF
   const int8_t offset = static_cast<int>(next());
   if (ZF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -64,7 +64,7 @@ case 0x74: {  // jump rel8 if ZF
 }
 
 :(code)
-void test_je_rel8_fail() {
+void test_je_disp8_fail() {
   ZF = false;
   run(
       "== code 0x1\n"
@@ -87,7 +87,7 @@ void test_je_rel8_fail() {
 put_new(Name, "75", "jump disp8 bytes away if not equal, if ZF is not set (jcc/jnz/jne)");
 
 :(code)
-void test_jne_rel8_success() {
+void test_jne_disp8_success() {
   ZF = false;
   run(
       "== code 0x1\n"
@@ -105,7 +105,7 @@ void test_jne_rel8_success() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x75: {  // jump rel8 unless ZF
+case 0x75: {  // jump disp8 unless ZF
   const int8_t offset = static_cast<int>(next());
   if (!ZF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -115,7 +115,7 @@ case 0x75: {  // jump rel8 unless ZF
 }
 
 :(code)
-void test_jne_rel8_fail() {
+void test_jne_disp8_fail() {
   ZF = true;
   run(
       "== code 0x1\n"
@@ -139,7 +139,7 @@ put_new(Name, "7f", "jump disp8 bytes away if greater (signed), if ZF is unset a
 put_new(Name, "77", "jump disp8 bytes away if greater (unsigned), if ZF is unset and CF is unset (jcc/ja/jnbe)");
 
 :(code)
-void test_jg_rel8_success() {
+void test_jg_disp8_success() {
   ZF = false;
   SF = false;
   OF = false;
@@ -159,7 +159,7 @@ void test_jg_rel8_success() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x7f: {  // jump rel8 if SF == OF and !ZF
+case 0x7f: {  // jump disp8 if SF == OF and !ZF
   const int8_t offset = static_cast<int>(next());
   if (SF == OF && !ZF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -167,7 +167,7 @@ case 0x7f: {  // jump rel8 if SF == OF and !ZF
   }
   break;
 }
-case 0x77: {  // jump rel8 if !CF and !ZF
+case 0x77: {  // jump disp8 if !CF and !ZF
   const int8_t offset = static_cast<int>(next());
   if (!CF && !ZF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -177,7 +177,7 @@ case 0x77: {  // jump rel8 if !CF and !ZF
 }
 
 :(code)
-void test_jg_rel8_fail() {
+void test_jg_disp8_fail() {
   ZF = false;
   SF = true;
   OF = false;
@@ -203,7 +203,7 @@ put_new(Name, "7d", "jump disp8 bytes away if greater or equal (signed), if SF =
 put_new(Name, "73", "jump disp8 bytes away if greater or equal (unsigned), if CF is unset (jcc/jae/jnb)");
 
 :(code)
-void test_jge_rel8_success() {
+void test_jge_disp8_success() {
   SF = false;
   OF = false;
   run(
@@ -222,7 +222,7 @@ void test_jge_rel8_success() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x7d: {  // jump rel8 if SF == OF
+case 0x7d: {  // jump disp8 if SF == OF
   const int8_t offset = static_cast<int>(next());
   if (SF == OF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -230,7 +230,7 @@ case 0x7d: {  // jump rel8 if SF == OF
   }
   break;
 }
-case 0x73: {  // jump rel8 if !CF
+case 0x73: {  // jump disp8 if !CF
   const int8_t offset = static_cast<int>(next());
   if (!CF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -240,7 +240,7 @@ case 0x73: {  // jump rel8 if !CF
 }
 
 :(code)
-void test_jge_rel8_fail() {
+void test_jge_disp8_fail() {
   SF = true;
   OF = false;
   run(
@@ -265,7 +265,7 @@ put_new(Name, "7c", "jump disp8 bytes away if lesser (signed), if SF != OF (jcc/
 put_new(Name, "72", "jump disp8 bytes away if lesser (unsigned), if CF is set (jcc/jb/jnae)");
 
 :(code)
-void test_jl_rel8_success() {
+void test_jl_disp8_success() {
   ZF = false;
   SF = true;
   OF = false;
@@ -285,7 +285,7 @@ void test_jl_rel8_success() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x7c: {  // jump rel8 if SF != OF
+case 0x7c: {  // jump disp8 if SF != OF
   const int8_t offset = static_cast<int>(next());
   if (SF != OF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -293,7 +293,7 @@ case 0x7c: {  // jump rel8 if SF != OF
   }
   break;
 }
-case 0x72: {  // jump rel8 if CF
+case 0x72: {  // jump disp8 if CF
   const int8_t offset = static_cast<int>(next());
   if (CF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -303,7 +303,7 @@ case 0x72: {  // jump rel8 if CF
 }
 
 :(code)
-void test_jl_rel8_fail() {
+void test_jl_disp8_fail() {
   ZF = false;
   SF = false;
   OF = false;
@@ -329,7 +329,7 @@ put_new(Name, "7e", "jump disp8 bytes away if lesser or equal (signed), if ZF is
 put_new(Name, "76", "jump disp8 bytes away if lesser or equal (unsigned), if ZF is set or CF is set (jcc/jbe/jna)");
 
 :(code)
-void test_jle_rel8_equal() {
+void test_jle_disp8_equal() {
   ZF = true;
   SF = false;
   OF = false;
@@ -349,7 +349,7 @@ void test_jle_rel8_equal() {
 }
 
 :(code)
-void test_jle_rel8_lesser() {
+void test_jle_disp8_lesser() {
   ZF = false;
   SF = true;
   OF = false;
@@ -369,7 +369,7 @@ void test_jle_rel8_lesser() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x7e: {  // jump rel8 if ZF or SF != OF
+case 0x7e: {  // jump disp8 if ZF or SF != OF
   const int8_t offset = static_cast<int>(next());
   if (ZF || SF != OF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -377,7 +377,7 @@ case 0x7e: {  // jump rel8 if ZF or SF != OF
   }
   break;
 }
-case 0x76: {  // jump rel8 if ZF or CF
+case 0x76: {  // jump disp8 if ZF or CF
   const int8_t offset = static_cast<int>(next());
   if (ZF || CF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -387,7 +387,7 @@ case 0x76: {  // jump rel8 if ZF or CF
 }
 
 :(code)
-void test_jle_rel8_greater() {
+void test_jle_disp8_greater() {
   ZF = false;
   SF = false;
   OF = false;
diff --git a/subx/018jump_disp32.cc b/subx/018jump_disp32.cc
index 524ef5f1..86e06e9f 100644
--- a/subx/018jump_disp32.cc
+++ b/subx/018jump_disp32.cc
@@ -57,7 +57,7 @@ void test_je_disp32_success() {
 case 0x84: {  // jump disp32 if ZF
   const int32_t offset = next32();
   if (ZF) {
-    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
     EIP += offset;
   }
   break;
@@ -108,7 +108,7 @@ void test_jne_disp32_success() {
 case 0x85: {  // jump disp32 unless ZF
   const int32_t offset = next32();
   if (!ZF) {
-    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
     EIP += offset;
   }
   break;
@@ -135,7 +135,8 @@ void test_jne_disp32_fail() {
 //:: jump if greater
 
 :(before "End Initialize Op Names")
-put_new(Name_0f, "8f", "jump disp32 bytes away if greater, if ZF is unset and SF == OF (jcc/jg/jnle)");
+put_new(Name_0f, "8f", "jump disp32 bytes away if greater (signed), if ZF is unset and SF == OF (jcc/jg/jnle)");
+put_new(Name_0f, "87", "jump disp32 bytes away if greater (unsigned), if ZF is unset and CF is unset (jcc/ja/jnbe)");
 
 :(code)
 void test_jg_disp32_success() {
@@ -161,7 +162,15 @@ void test_jg_disp32_success() {
 case 0x8f: {  // jump disp32 if !SF and !ZF
   const int32_t offset = next32();
   if (!ZF && SF == OF) {
-    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
+    EIP += offset;
+  }
+  break;
+}
+case 0x87: {  // jump disp32 if !CF and !ZF
+  const int32_t offset = next();
+  if (!CF && !ZF) {
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
     EIP += offset;
   }
   break;
@@ -190,7 +199,8 @@ void test_jg_disp32_fail() {
 //:: jump if greater or equal
 
 :(before "End Initialize Op Names")
-put_new(Name_0f, "8d", "jump disp32 bytes away if greater or equal, if SF == OF (jcc/jge/jnl)");
+put_new(Name_0f, "8d", "jump disp32 bytes away if greater or equal (signed), if SF == OF (jcc/jge/jnl)");
+put_new(Name_0f, "83", "jump disp32 bytes away if greater or equal (unsigned), if CF is unset (jcc/jae/jnb)");
 
 :(code)
 void test_jge_disp32_success() {
@@ -215,7 +225,15 @@ void test_jge_disp32_success() {
 case 0x8d: {  // jump disp32 if !SF
   const int32_t offset = next32();
   if (SF == OF) {
-    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
+    EIP += offset;
+  }
+  break;
+}
+case 0x83: {  // jump disp32 if !CF
+  const int32_t offset = next32();
+  if (!CF) {
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
     EIP += offset;
   }
   break;
@@ -243,7 +261,8 @@ void test_jge_disp32_fail() {
 //:: jump if lesser
 
 :(before "End Initialize Op Names")
-put_new(Name_0f, "8c", "jump disp32 bytes away if lesser, if SF != OF (jcc/jl/jnge)");
+put_new(Name_0f, "8c", "jump disp32 bytes away if lesser (signed), if SF != OF (jcc/jl/jnge)");
+put_new(Name_0f, "82", "jump disp32 bytes away if lesser (unsigned), if CF is set (jcc/jb/jnae)");
 
 :(code)
 void test_jl_disp32_success() {
@@ -269,7 +288,15 @@ void test_jl_disp32_success() {
 case 0x8c: {  // jump disp32 if SF and !ZF
   const int32_t offset = next32();
   if (SF != OF) {
-    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
+    EIP += offset;
+  }
+  break;
+}
+case 0x72: {  // jump disp32 if CF
+  const int32_t offset = next32();
+  if (CF) {
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
     EIP += offset;
   }
   break;
@@ -298,7 +325,8 @@ void test_jl_disp32_fail() {
 //:: jump if lesser or equal
 
 :(before "End Initialize Op Names")
-put_new(Name_0f, "8e", "jump disp32 bytes away if lesser or equal, if ZF is set or SF != OF (jcc/jle/jng)");
+put_new(Name_0f, "8e", "jump disp32 bytes away if lesser or equal (signed), if ZF is set or SF != OF (jcc/jle/jng)");
+put_new(Name_0f, "86", "jump disp8 bytes away if lesser or equal (unsigned), if ZF is set or CF is set (jcc/jbe/jna)");
 
 :(code)
 void test_jle_disp32_equal() {
@@ -344,7 +372,15 @@ void test_jle_disp32_lesser() {
 case 0x8e: {  // jump disp32 if SF or ZF
   const int32_t offset = next32();
   if (ZF || SF != OF) {
-    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
+    EIP += offset;
+  }
+  break;
+}
+case 0x86: {  // jump disp32 if ZF or CF
+  const int32_t offset = next32();
+  if (ZF || CF) {
+    trace(Callstack_depth+1, "run") << "jump " << offset << end();
     EIP += offset;
   }
   break;
diff --git a/subx/021byte_addressing.cc b/subx/021byte_addressing.cc
index 6510e2a6..a0b36776 100644
--- a/subx/021byte_addressing.cc
+++ b/subx/021byte_addressing.cc
@@ -162,7 +162,7 @@ case 0xc6: {  // copy imm8 to r/m8
   const uint8_t modrm = next();
   const uint8_t src = next();
   trace(Callstack_depth+1, "run") << "copy imm8 to r8/m8-at-r32" << end();
-  trace(Callstack_depth+1, "run") << "imm8 is 0x" << HEXWORD << src << end();
+  trace(Callstack_depth+1, "run") << "imm8 is 0x" << HEXWORD << NUM(src) << end();
   const uint8_t subop = (modrm>>3)&0x7;  // middle 3 'reg opcode' bits
   if (subop != 0) {
     cerr << "unrecognized subop for opcode c6: " << NUM(subop) << " (only 0/copy currently implemented)\n";
diff --git a/subx/031check_operands.cc b/subx/031check_operands.cc
index 45d9e7e1..bf5d3719 100644
--- a/subx/031check_operands.cc
+++ b/subx/031check_operands.cc
@@ -225,12 +225,11 @@ void init_permitted_operands() {
   put(Permitted_operands, "87", 0x01);
   // copy address (lea)
   put(Permitted_operands, "8d", 0x01);
-  // pop
-  put(Permitted_operands, "8f", 0x01);
 
   //// Class N: op, ModR/M and subop (not r32)
   //  imm32 imm8  disp32 |disp16  disp8 subop modrm
   //  0     0     0      |0       0     1     1
+  put(Permitted_operands, "8f", 0x03);  // pop
   put(Permitted_operands, "d3", 0x03);  // shift
   put(Permitted_operands, "f7", 0x03);  // test/not/mul/div
   put(Permitted_operands, "ff", 0x03);  // jump/push/call
@@ -627,8 +626,12 @@ map</*op*/string, /*bitvector*/uint8_t> Permitted_operands_0f;
 //// Class D: just op and disp32
 //  imm32 imm8  disp32 |disp16  disp8 subop modrm
 //  0     0     1      |0       0     0     0
+put_new(Permitted_operands_0f, "82", 0x10);
+put_new(Permitted_operands_0f, "83", 0x10);
 put_new(Permitted_operands_0f, "84", 0x10);
 put_new(Permitted_operands_0f, "85", 0x10);
+put_new(Permitted_operands_0f, "86", 0x10);
+put_new(Permitted_operands_0f, "87", 0x10);
 put_new(Permitted_operands_0f, "8c", 0x10);
 put_new(Permitted_operands_0f, "8d", 0x10);
 put_new(Permitted_operands_0f, "8e", 0x10);
diff --git a/subx/039debug.cc b/subx/039debug.cc
index 8aa40558..b308a7d4 100644
--- a/subx/039debug.cc
+++ b/subx/039debug.cc
@@ -74,9 +74,9 @@ Watch_points.clear();
 :(code)
 void dump_watch_points() {
   if (Watch_points.empty()) return;
-  dbg << "watch points:" << end();
+  trace(Callstack_depth, "dbg") << "watch points:" << end();
   for (map<string, uint32_t>::iterator p = Watch_points.begin();  p != Watch_points.end();  ++p)
-    dbg << "  " << p->first << ": " << HEXWORD << p->second << " -> " << HEXWORD << read_mem_u32(p->second) << end();
+    trace(Callstack_depth, "dbg") << "  " << p->first << ": " << HEXWORD << p->second << " -> " << HEXWORD << read_mem_u32(p->second) << end();
 }
 
 :(before "End Globals")
@@ -91,6 +91,20 @@ if (!Watch_this_effective_address.empty()) {
   put(Watch_points, Watch_this_effective_address, addr);
 }
 
+//: Special label that dumps regions of memory.
+//: Not a general mechanism; by the time you get here you're willing to hack
+//: on the emulator.
+:(after "Run One Instruction")
+if (contains_key(Symbol_name, EIP) && get(Symbol_name, EIP) == "$dump-stream-at-EAX")
+  dump_stream_at(Reg[EAX].u);
+:(code)
+void dump_stream_at(uint32_t stream_start) {
+  int32_t stream_length = read_mem_i32(stream_start + 8);
+  dbg << "stream length: " << std::dec << stream_length << end();
+  for (int i = 0;  i < stream_length + 12;  ++i)
+    dbg << "0x" << HEXWORD << (stream_start+i) << ": " << HEXBYTE << NUM(read_mem_u8(stream_start+i)) << end();
+}
+
 //: helpers
 
 :(code)
diff --git a/subx/054string-equal.subx b/subx/054string-equal.subx
index 6c23fccb..9784e2ad 100644
--- a/subx/054string-equal.subx
+++ b/subx/054string-equal.subx
@@ -15,22 +15,20 @@ Entry:  # run all tests
 
 string-equal?:  # s : (address string), benchmark : (address string) -> EAX : boolean
     # pseudocode:
-    #   lens = s->length
-    #   if (lens != benchmark->length) return false
-    #   i = 0
+    #   if (s->length != benchmark->length) return false
     #   currs = s->data
     #   currb = benchmark->data
-    #   while i < s->length
+    #   maxs = s->data + s->length
+    #   while currs < maxs
     #     c1 = *currs
     #     c2 = *currb
     #     if (c1 != c2) return false
-    #     ++i, ++currs, ++currb
+    #     ++currs, ++currb
     #   return true
     #
     # registers:
-    #   i: ECX
-    #   lens: EDX
     #   currs: ESI
+    #   maxs: ECX
     #   currb: EDI
     #   c1: EAX
     #   c2: EBX
@@ -41,40 +39,38 @@ string-equal?:  # s : (address string), benchmark : (address string) -> EAX : bo
     # . save registers
     51/push-ECX
     52/push-EDX
-    53/push-EBX
     56/push-ESI
     57/push-EDI
     # ESI = s
     8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
     # EDI = benchmark
     8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   0xc/disp8       .                 # copy *(EBP+12) to EDI
-    # lens/EDX = s->length
-    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
+    # ECX = s->length
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
 $string-equal?:lengths:
-    # if (lens != benchmark->length) return false
-    39/compare                      0/mod/indirect  7/rm32/EDI    .           .             .           2/r32/EDX   .               .                 # compare *EDI and EDX
+    # if (ECX != benchmark->length) return false
+    39/compare                      0/mod/indirect  7/rm32/EDI    .           .             .           1/r32/ECX   .               .                 # compare *EDI and ECX
     75/jump-if-not-equal  $string-equal?:false/disp8
     # currs/ESI = s->data
     81          0/subop/add         3/mod/direct    6/rm32/ESI    .           .             .           .           .               4/imm32           # add to ESI
+    # maxs/ECX = s->data + s->length
+    01/add                          3/mod/direct    1/rm32/ECX    .           .             .           6/r32/ESI   .               .                 # add ESI to ECX
     # currb/EDI = benchmark->data
     81          0/subop/add         3/mod/direct    7/rm32/EDI    .           .             .           .           .               4/imm32           # add to EDI
-    # i/ECX = c1/EAX = c2/EBX = 0
-    31/xor                          3/mod/direct    1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # clear ECX
+    # c1/EAX = c2/EDX = 0
     31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
-    31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
+    31/xor                          3/mod/direct    2/rm32/EDX    .           .             .           2/r32/EDX   .               .                 # clear EDX
 $string-equal?:loop:
-    # if (i >= lens) return true
-    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $string-equal?:true/disp8
+    # if (currs >= maxs) return true
+    39/compare                      3/mod/direct    6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # compare ESI with ECX
+    73/jump-if-greater-or-equal-unsigned  $string-equal?:true/disp8
     # c1 = *currs
     8a/copy-byte                    0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/AL    .               .                 # copy byte at *ESI to AL
     # c2 = *currb
-    8a/copy-byte                    0/mod/indirect  7/rm32/EDI    .           .             .           3/r32/BL    .               .                 # copy byte at *EDI to BL
+    8a/copy-byte                    0/mod/indirect  7/rm32/EDI    .           .             .           2/r32/DL    .               .                 # copy byte at *EDI to DL
     # if (c1 != c2) return false
-    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # compare EAX and EBX
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # compare EAX and EDX
     75/jump-if-not-equal  $string-equal?:false/disp8
-    # ++i
-    41/increment-ECX
     # ++currs
     46/increment-ESI
     # ++currb
@@ -89,7 +85,6 @@ $string-equal?:end:
     # . restore registers
     5f/pop-to-EDI
     5e/pop-to-ESI
-    5b/pop-to-EBX
     5a/pop-to-EDX
     59/pop-to-ECX
     # . epilog
@@ -179,4 +174,57 @@ test-compare-inequal-strings-equal-lengths:
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
     c3/return
 
+# helper for later tests
+check-string-equal:  # s : (address string), expected : (address string), msg : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    # EAX = string-equal?(s, expected)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  string-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    68/push  1/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$check-string-equal:end:
+    # . restore registers
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# test the helper
+test-check-string-equal:
+    # check-string-equal?("Abc", "Abc")
+    # . . push args
+    68/push  "Abc"/imm32
+    68/push  "Abc"/imm32
+    # . . call
+    e8/call  check-string-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-check-string-equal"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    c3/return
+
 # . . vim:nowrap:textwidth=0
diff --git a/subx/055stream.subx b/subx/055stream.subx
index 92049634..f2969861 100644
--- a/subx/055stream.subx
+++ b/subx/055stream.subx
@@ -37,7 +37,7 @@ clear-stream:  # f : (address stream) -> <void>
 $clear-stream:loop:
     # if (EAX >= ECX) break
     39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare EAX with ECX
-    7d/jump-if-greater-or-equal  $clear-stream:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $clear-stream:end/disp8
     # *EAX = 0
     c6          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm8            # copy byte to *EAX
     # ++EAX
diff --git a/subx/056trace.subx b/subx/056trace.subx
index 5203f914..cc245e3f 100644
--- a/subx/056trace.subx
+++ b/subx/056trace.subx
@@ -4,19 +4,13 @@
 #   write : int  # index at which writes go
 #   read : int  # index that we've read until
 #   data : (array byte)  # prefixed by length as usual
-# In a real trace the data will be in a special segment set aside for the purpose.
+# Usually the trace stream will be in a separate segment set aside for the purpose.
 #
-# primitives for operating on traces:
-#   - initialize-trace-stream (update global variable)
-#   - trace: stream, string
-#   - die: stream (exit(1) if using real trace)
-#   - check-trace-contains: stream, string/line, string/message (scans only from stream's read pointer, prints message to stderr on failure, updates stream's read pointer)
-#   - scan-to-next-line: stream (advance read pointer past next newline)
-#
-# Traces are very fundamental, so many of the helpers we create here won't be
-# used elsewhere; we'll switch to more bounds-checked variants. But here we get
-# bounds-checking for free; we allocate a completely disjoint segment for trace
-# data, and overflowing it will generate a page fault.
+# primitives for operating on traces (arguments in quotes):
+#   - initialize-trace-stream: populates Trace-stream with a new segment of the given 'size'
+#   - trace: adds a 'line' to Trace-stream
+#   - check-trace-contains: scans from Trace-stream's start for a matching 'line', prints a 'message' to stderr on failure
+#   - check-trace-scans-to: scans from Trace-stream's read pointer for a matching 'line', prints a 'message' to stderr on failure
 
 == data
 
@@ -24,6 +18,10 @@
 Trace-stream:
     0/imm32
 
+Trace-segment:
+    0/imm32/curr
+    0/imm32/limit
+
 # Fake trace-stream for tests.
 # Also illustrates the layout of the real trace-stream (segment).
 _test-trace-stream:
@@ -43,23 +41,45 @@ _test-trace-stream:
 
 # Allocate a new segment for the trace stream, initialize its length, and save its address to Trace-stream.
 # The Trace-stream segment will consist of variable-length lines separated by newlines (0x0a)
-initialize-trace-stream:
-    # EAX = new-segment(0x1000)
+initialize-trace-stream:  # n : int -> <void>
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    # ECX = n
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # Trace-segment = new-segment(n)
     # . . push args
-    68/push  0x1000/imm32/N
+    68/push  Trace-segment/imm32
+    51/push-ECX
     # . . call
     e8/call  new-segment/disp32
     # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # copy EAX to *Trace-stream
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # copy Trace-segment->curr to *Trace-stream
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-segment/disp32              # copy *Trace-segment to EAX
+    # watch point to catch Trace-stream leaks
+#? $watch-1:
     89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy EAX to *Trace-stream
-    # Trace-stream->length = 0x1000/N - 12
-    c7          0/subop/copy        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           8/disp8         0xff4/imm32       # copy 0xff4 to *(EAX+8)
+    # Trace-stream->length = n - 12
+    # . ECX -= 12
+    81          5/subop/subtract    3/mod/direct    1/rm32/ECX    .           .             .           .           .               0xc/imm32         # subtract from ECX
+    # . Trace-stream->length = ECX
+    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           1/r32/ECX   8/disp8         .                 # copy ECX to *(EAX+8)
+$initialize-trace-stream:end:
+    # . restore registers
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
     c3/return
 
 # Append a string to the given trace stream.
 # Silently give up if it's already full. Or truncate the string if there isn't enough room.
-trace:  # t : (address trace-stream), line : string
+trace:  # line : (address string)
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -70,10 +90,10 @@ trace:  # t : (address trace-stream), line : string
     53/push-EBX
     56/push-ESI
     57/push-EDI
-    # EDI = t
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         7/r32/EDI   8/disp8         .                 # copy *(EBP+8) to EDI
+    # EDI = *Trace-stream
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           7/r32/EDI   Trace-stream/disp32               # copy *Trace-stream to EDI
     # ESI = line
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
     # ECX = t->write
     8b/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           1/r32/ECX   .               .                 # copy *EDI to ECX
     # EDX = t->length
@@ -126,61 +146,21 @@ $trace:end:
     5d/pop-to-EBP
     c3/return
 
-clear-trace-stream:  # t : (address trace-stream)
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # . save registers
-    50/push-EAX
-    51/push-ECX
-    # EAX = t
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         0/r32/EAX   8/disp8         .                 # copy *(EBP+8) to EAX
-    # ECX = t->length
-    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           1/r32/ECX   8/disp8         .                 # copy *(EAX+8) to ECX
-    # ECX = &t->data[t->length]
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   0xc/disp8       .                 # copy EAX+ECX+12 to ECX
-    # t->write = 0
-    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # copy to *EAX
-    # t->read = 0
-    c7          0/subop/copy        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         0/imm32           # copy to *(EAX+4)
-    # EAX = t->data
-    81          0/subop/add         3/mod/direct    0/rm32/EAX    .           .             .           .           .               0xc/imm32         # add to EAX
-$clear-trace-stream:loop:
-    # if (EAX >= ECX) break
-    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare EAX with ECX
-    7d/jump-if-greater-or-equal  $clear-trace-stream:end/disp8
-    # *EAX = 0
-    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # copy to *EAX
-    # EAX += 4
-    81          0/subop/add         3/mod/direct    0/rm32/EAX    .           .             .           .           .               4/imm32           # add to EAX
-    eb/jump  $clear-trace-stream:loop/disp8
-$clear-trace-stream:end:
-    # . restore registers
-    59/pop-to-ECX
-    58/pop-to-EAX
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-# - tests
-
 test-trace-single:
-    # clear-trace-stream(_test-trace-stream)
-    # . . push args
-    68/push  _test-trace-stream/imm32
-    # . . call
+    # push *Trace-stream
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # *Trace-stream = _test-trace-stream
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy EAX to *Trace-stream
+    # clear-trace-stream()
     e8/call  clear-trace-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # trace(_test-trace-stream, "Ab")
+    # trace("Ab")
     # . . push args
     68/push  "Ab"/imm32
-    68/push  _test-trace-stream/imm32
     # . . call
     e8/call  trace/disp32
     # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # check-ints-equal(*_test-trace-stream->data, 41/A 62/b 0a/newline 00, msg)
     # . . push args
     68/push  "F - test-trace-single"/imm32
@@ -192,33 +172,33 @@ test-trace-single:
     e8/call  check-ints-equal/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # pop into *Trace-stream
+    8f          0/subop/pop         0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # pop into *Trace-stream
     # end
     c3/return
 
 test-trace-appends:
-    # clear-trace-stream(_test-trace-stream)
-    # . . push args
-    68/push  _test-trace-stream/imm32
-    # . . call
+    # push *Trace-stream
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # *Trace-stream = _test-trace-stream
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy EAX to *Trace-stream
+    # clear-trace-stream()
     e8/call  clear-trace-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # trace(_test-trace-stream, "C")
+    # trace("C")
     # . . push args
     68/push  "C"/imm32
-    68/push  _test-trace-stream/imm32
     # . . call
     e8/call  trace/disp32
     # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # trace(_test-trace-stream, "D")
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # trace("D")
     # . . push args
     68/push  "D"/imm32
-    68/push  _test-trace-stream/imm32
     # . . call
     e8/call  trace/disp32
     # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # check-ints-equal(*_test-trace-stream->data, 43/C 0a/newline 44/D 0a/newline, msg)
     # . . push args
     68/push  "F - test-trace-appends"/imm32
@@ -230,25 +210,26 @@ test-trace-appends:
     e8/call  check-ints-equal/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # pop into *Trace-stream
+    8f          0/subop/pop         0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # pop into *Trace-stream
     # end
     c3/return
 
 test-trace-empty-line:
-    # clear-trace-stream(_test-trace-stream)
-    # . . push args
-    68/push  _test-trace-stream/imm32
-    # . . call
+    # push *Trace-stream
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # *Trace-stream = _test-trace-stream
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy EAX to *Trace-stream
+    # clear-trace-stream()
     e8/call  clear-trace-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # trace(_test-trace-stream, "")
+    # trace("")
     # . . push args
     68/push  ""/imm32
-    68/push  _test-trace-stream/imm32
     # . . call
     e8/call  trace/disp32
     # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # check-ints-equal(*_test-trace-stream->data, 0, msg)
     # . . push args
     68/push  "F - test-trace-empty-line"/imm32
@@ -260,9 +241,640 @@ test-trace-empty-line:
     e8/call  check-ints-equal/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # pop into *Trace-stream
+    8f          0/subop/pop         0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # pop into *Trace-stream
     # end
     c3/return
 
+check-trace-contains:  # line : (address string), msg : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # rewind-stream(*Trace-stream)
+    # . . push args
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  rewind-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-trace-scans-to(line, msg)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  check-trace-scans-to/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+$check-trace-contains:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+check-trace-scans-to:  # line : (address string), msg : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    # EAX = trace-scan(line)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  trace-scan/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    68/push  1/imm32
+    50/push-EAX
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$check-trace-scans-to:end:
+    # . restore registers
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# Start scanning from Trace-stream->read for 'line'. If found, update Trace-stream->read and return true.
+trace-scan:  # line : (address string) -> result/EAX : boolean
+    # pseudocode:
+    #   push Trace-stream->read
+    #   while true:
+    #     if Trace-stream->read >= Trace-stream->write
+    #       break
+    #     if next-line-matches?(Trace-stream, line)
+    #       skip-next-line(Trace-stream)
+    #       dump saved copy of Trace-stream->read
+    #       return true
+    #     skip-next-line(Trace-stream)
+    #   pop saved copy of Trace-stream->read
+    #   return false
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    56/push-ESI
+    # ESI = *Trace-stream
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           6/r32/ESI   Trace-stream/disp32               # copy *Trace-stream to ESI
+    # ECX = Trace-stream->write
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX                   .                 # copy *ESI to ECX
+    # push Trace-stream->read
+    ff          6/subop/push        1/mod/*+disp8   6/rm32/ESI    .           .             .           .           4/disp8         .                 # push *(ESI+4)
+$trace-scan:loop:
+    # if (Trace-stream->read >= Trace-stream->write) return false
+    39/compare                      1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # compare ECX with *(ESI+4)
+    7d/jump-if-greater-or-equal  $trace-scan:false/disp8
+    # EAX = next-line-matches?(Trace-stream, line)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    56/push-ESI
+    # . . call
+    e8/call  next-line-matches?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # if (EAX == 0) continue
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $trace-scan:continue/disp8
+$trace-scan:true:
+    # skip-next-line(Trace-stream)
+    # . . push args
+    56/push-ESI
+    # . . call
+    e8/call  skip-next-line/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # dump saved copy of Trace-stream->read
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # return true
+    b8/copy-to-EAX  1/imm32/true
+    eb/jump  $trace-scan:end/disp8
+$trace-scan:continue:
+    # skip-next-line(Trace-stream)
+    # . . push args
+    56/push-ESI
+    # . . call
+    e8/call  skip-next-line/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    eb/jump  $trace-scan:loop/disp8
+$trace-scan:false:
+    # restore saved copy of Trace-stream->read
+    8f          0/subop/pop         1/mod/*+disp8   6/rm32/ESI    .           .             .           .           4/disp8         .                 # pop to *(ESI+4)
+    # return false
+    b8/copy-to-EAX  0/imm32/false
+$trace-scan:end:
+    # . restore registers
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-trace-scan-first:
+    # push *Trace-stream
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # setup
+    # . *Trace-stream = _test-trace-stream
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy EAX to *Trace-stream
+    # . clear-trace-stream()
+    e8/call  clear-trace-stream/disp32
+    # . trace("Ab")
+    # . . push args
+    68/push  "Ab"/imm32
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # EAX = trace-scan("Ab")
+    # . . push args
+    68/push  "Ab"/imm32
+    # . . call
+    e8/call  trace-scan/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-trace-scan-first"/imm32
+    68/push  1/imm32
+    50/push-EAX
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # pop into *Trace-stream
+    8f          0/subop/pop         0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # pop into *Trace-stream
+    # . end
+    c3/return
+
+test-trace-scan-skips-lines-until-found:
+    # push *Trace-stream
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # setup
+    # . *Trace-stream = _test-trace-stream
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy EAX to *Trace-stream
+    # . clear-trace-stream()
+    e8/call  clear-trace-stream/disp32
+    # . trace("Ab")
+    # . . push args
+    68/push  "Ab"/imm32
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . trace("cd")
+    # . . push args
+    68/push  "cd"/imm32
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # EAX = trace-scan("cd")
+    # . . push args
+    68/push  "cd"/imm32
+    # . . call
+    e8/call  trace-scan/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-trace-scan-skips-lines-until-found"/imm32
+    68/push  1/imm32
+    50/push-EAX
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # pop into *Trace-stream
+    8f          0/subop/pop         0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # pop into *Trace-stream
+    # . end
+    c3/return
+
+test-trace-second-scan-starts-where-first-left-off:
+    # push *Trace-stream
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # setup
+    # . *Trace-stream = _test-trace-stream
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy EAX to *Trace-stream
+    # . clear-trace-stream()
+    e8/call  clear-trace-stream/disp32
+    # . trace("Ab")
+    # . . push args
+    68/push  "Ab"/imm32
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . EAX = trace-scan("Ab")
+    # . . push args
+    68/push  "Ab"/imm32
+    # . . call
+    e8/call  trace-scan/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # second scan fails
+    # . EAX = trace-scan("Ab")
+    # . . push args
+    68/push  "Ab"/imm32
+    # . . call
+    e8/call  trace-scan/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-trace-second-scan-starts-where-first-left-off"/imm32
+    68/push  0/imm32
+    50/push-EAX
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # pop into *Trace-stream
+    8f          0/subop/pop         0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # pop into *Trace-stream
+    # . end
+    c3/return
+
+test-trace-scan-failure-leaves-read-index-untouched:
+    # push *Trace-stream
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # setup
+    # . *Trace-stream = _test-trace-stream
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy EAX to *Trace-stream
+    # . clear-trace-stream()
+    e8/call  clear-trace-stream/disp32
+    # . trace("Ab")
+    # . . push args
+    68/push  "Ab"/imm32
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . check-ints-equal(_test-trace-stream->read, 0, msg)
+    # . . push args
+    68/push  "F - test-trace-second-scan-starts-where-first-left-off/precondition-failure"/imm32
+    68/push  0/imm32
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # perform a failing scan
+    # . EAX = trace-scan("Ax")
+    # . . push args
+    68/push  "Ax"/imm32
+    # . . call
+    e8/call  trace-scan/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # no change in read index
+    # . check-ints-equal(_test-trace-stream->read, 0, msg)
+    # . . push args
+    68/push  "F - test-trace-second-scan-starts-where-first-left-off"/imm32
+    68/push  0/imm32
+    b8/copy-to-EAX  _test-trace-stream/imm32
+    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
+    # . . call
+    e8/call check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # pop into *Trace-stream
+    8f          0/subop/pop         0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # pop into *Trace-stream
+    # . end
+    c3/return
+
+next-line-matches?:  # t : (address stream), line : (address string) -> result/EAX : boolean
+    # pseudocode:
+    #   while true:
+    #     if (currl >= maxl) break
+    #     if (currt >= maxt) return false
+    #     if (*currt != *currl) return false
+    #     ++currt
+    #     ++currl
+    #   return *currt == '\n'
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    56/push-ESI
+    57/push-EDI
+    # EDX = line
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         2/r32/EDX   0xc/disp8       .                 # copy *(EBP+12) to EDX
+    # currl/ESI = line->data
+    # . ESI = line/EDX->data
+    8d/copy-address                 1/mod/*+disp8   2/rm32/EDX    .           .             .           6/r32/ESI   4/disp8         .                 # copy EDX+4 to ESI
+    # maxl/ECX = line->data + line->size
+    # . EAX = line/EDX->size
+    8b/copy                         0/mod/indirect  2/rm32/EDX    .           .                         0/r32/EAX   .               .                 # copy *EDX to EAX
+    # . maxl/ECX = line->data/ESI + line->size/EAX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    6/base/ESI  0/index/EAX   .           1/r32/ECX   .               .                 # copy EDX+EAX to ECX
+    # EDI = t
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         7/r32/EDI   8/disp8         .                 # copy *(EBP+8) to EDI
+    # EBX = t->data
+    8d/copy-address                 1/mod/*+disp8   7/rm32/EDI    .           .             .           3/r32/EBX   0xc/disp8       .                 # copy EDI+12 to EBX
+    # maxt/EDX = t->data + t->write
+    # . EAX = t->write
+    8b/copy                         0/mod/indirect  7/rm32/EDI    .           .                         0/r32/EAX   .               .                 # copy *EDI to EAX
+    # . maxt/EDX = t->data/EBX + t->write/EAX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    3/base/EBX  0/index/EAX   .           2/r32/EDX   .               .                 # copy EBX+EAX to EDX
+    # currt/EDI = t->data + t->read
+    # . EAX = t/EDI->read
+    8b/copy                         1/mod/*+disp8   7/rm32/EDI    .           .                         0/r32/EAX   4/disp8         .                 # copy *(EDI+4) to EAX
+    # . currt/EDI = t->data/EBX + t->read/EAX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    3/base/EBX  0/index/EAX   .           7/r32/EDI   .               .                 # copy EBX+EAX to EDI
+$next-line-matches?:loop:
+    # if (currl/ESI >= maxl/ECX) break
+    39/compare                      3/mod/direct    6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # compare ESI and ECX
+    73/jump-if-greater-or-equal-unsigned  $next-line-matches?:break/disp8
+    # if (currt/EDI >= maxt/EDX) return false
+    # . EAX = false
+    b8/copy-to-EAX  0/imm32/false
+    39/compare                      3/mod/direct    7/rm32/EDI    .           .             .           2/r32/EDX   .               .                 # compare EDI and EDX
+    73/jump-if-greater-or-equal-unsigned  $next-line-matches?:end/disp8
+    # if (*currt/EDI != *currl/ESI) return false
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    31/xor                          3/mod/direct    3/rm32/EAX    .           .             .           3/r32/EAX   .               .                 # clear EBX
+    # . EAX = (char) *currt/EDI
+    8a/copy-byte                    0/mod/indirect  7/rm32/EDI    .           .                         0/r32/EAX   .               .                 # copy *EDI to EAX
+    # . EBX = (char) *currl/ESI
+    8a/copy-byte                    0/mod/indirect  6/rm32/ESI    .           .                         3/r32/EBX   .               .                 # copy *ESI to EBX
+    # . EAX >= EBX
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # compare EAX and EBX
+    # . EAX = false
+    b8/copy-to-EAX  0/imm32/false
+    75/jump-if-not-equal  $next-line-matches?:end/disp8
+    # ++currt/EDI
+    47/increment-EDI
+    # ++currl/ESI
+    46/increment-ESI
+    eb/jump  $next-line-matches?:loop/disp8
+$next-line-matches?:break:
+    # return *currt == '\n'
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    # . EAX = (char) *currt
+    8a/copy-byte                    0/mod/indirect  7/rm32/EDI    .           .                         0/r32/EAX   .               .                 # copy *EDI to EAX
+    3d/compare-EAX-and  0xa/imm32/newline
+    # . EAX = false
+    b8/copy-to-EAX  1/imm32/true
+    74/jump-if-equal  $next-line-matches?:end/disp8
+    b8/copy-to-EAX  0/imm32/true
+$next-line-matches?:end:
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-next-line-matches?-no-match-1:
+    # next line of "ABABA" does not match "blah blah"
+    # . EAX = next-line-matches?(_test-stream-line-ABABA, "blah blah")
+    # . . push args
+    68/push  "blah blah"/imm32
+    68/push  _test-stream-line-ABABA/imm32
+    # . . call
+    e8/call  next-line-matches?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-next-line-matches?-no-match-1"/imm32
+    68/push  0/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    c3/return
+
+test-next-line-matches?-no-match-2:
+    # next line of "ABABA" does not match ""
+    # . EAX = next-line-matches?(_test-stream-line-ABABA, "")
+    # . . push args
+    68/push  ""/imm32
+    68/push  _test-stream-line-ABABA/imm32
+    # . . call
+    e8/call  next-line-matches?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-next-line-matches?-no-match-2"/imm32
+    68/push  0/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    c3/return
+
+test-next-line-matches?-no-match-3:
+    # next line of "ABABA" does not match  "AA"
+    # . EAX = next-line-matches?(_test-stream-line-ABABA, "AA")
+    # . . push args
+    68/push  "AA"/imm32
+    68/push  _test-stream-line-ABABA/imm32
+    # . . call
+    e8/call  next-line-matches?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-next-line-matches?-no-match-3"/imm32
+    68/push  0/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    c3/return
+
+test-next-line-matches?-match:
+    # next line of "ABABA" matches "ABABA"
+    # . EAX = next-line-matches?(_test-stream-line-ABABA, "ABABA")
+    # . . push args
+    68/push  "ABABA"/imm32
+    68/push  _test-stream-line-ABABA/imm32
+    # . . call
+    e8/call  next-line-matches?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-next-line-matches?-match"/imm32
+    68/push  1/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    c3/return
+
+# move t->read to _after_ next newline
+skip-next-line:  # t : (address stream)
+    # pseudocode:
+    #   max = t->data + t->write
+    #   i = t->read
+    #   curr = t->data + t->read
+    #   while true
+    #     if (curr >= max) break
+    #     ++i
+    #     if (*curr == '\n') break
+    #     ++curr
+    #   t->read = i
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    # ECX = t
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # EDX = t/ECX->data
+    8d/copy-address                 1/mod/*+disp8   1/rm32/ECX    .           .             .           2/r32/EDX   0xc/disp8       .                 # copy ECX+12 to EDX
+    # EAX = t/ECX->write
+    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
+    # max/EBX = t->data/EDX + t->write/EAX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    2/base/EDX  0/index/EAX   .           3/r32/EBX   .               .                 # copy EDX+EAX to EBX
+    # EAX = t/ECX->read
+    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EDX
+    # curr/ECX = t->data/EDX + t->read/EAX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    2/base/EDX  0/index/EAX   .           1/r32/ECX   .               .                 # copy EDX+EAX to ECX
+    # i/EDX = EAX
+    8b/copy                         3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # copy EAX to EDX
+$skip-next-line:loop:
+    # if (curr/ECX >= max/EBX) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           3/r32/EBX   .               .                 # compare ECX and EBX
+    73/jump-if-greater-or-equal-unsigned  $skip-next-line:end/disp8
+    # ++i/EDX
+    42/increment-EDX
+    # if (*curr/ECX == '\n') break
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
+    3d/compare-EAX-and  0a/imm32/newline
+    74/jump-if-equal  $skip-next-line:end/disp8
+    # ++curr/ECX
+    41/increment-ECX
+    # loop
+    eb/jump  $skip-next-line:loop/disp8
+$skip-next-line:end:
+    # ECX = t
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # t/ECX->read = i/EDX
+    89/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           2/r32/EDX   4/disp8         .                 # copy EDX to *(ECX+4)
+    # . restore registers
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-skip-next-line-empty:
+    # skipping next line in empty stream leaves read pointer at 0
+    # . skip-next-line(_test-stream-empty)
+    # . . push args
+    68/push  _test-stream-empty/imm32
+    # . . call
+    e8/call  skip-next-line/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . check-ints-equal(_test-stream-empty->read, 0, msg)
+    # . . push args
+    68/push  "F - test-skip-next-line-empty"/imm32
+    68/push  0/imm32
+    b8/copy-to-EAX  _test-stream-empty/imm32
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(EAX+4) to EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    c3/return
+
+test-skip-next-line-filled:
+    # skipping next line increments read pointer by length of line + 1 (for newline)
+    # . skip-next-line(_test-stream-filled)
+    # . . push args
+    68/push  _test-stream-filled/imm32
+    # . . call
+    e8/call  skip-next-line/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . check-ints-equal(_test-stream-filled->read, 5, msg)
+    # . . push args
+    68/push  "F - test-skip-next-line-filled"/imm32
+    68/push  5/imm32
+    b8/copy-to-EAX  _test-stream-filled/imm32
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(EAX+4) to EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    c3/return
+
+clear-trace-stream:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    # EAX = *Trace-stream
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy *Trace-stream to EAX
+    # ECX = t->length
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           1/r32/ECX   8/disp8         .                 # copy *(EAX+8) to ECX
+    # ECX = &t->data[t->length]
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   0xc/disp8       .                 # copy EAX+ECX+12 to ECX
+    # t->write = 0
+    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # copy to *EAX
+    # t->read = 0
+    c7          0/subop/copy        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         0/imm32           # copy to *(EAX+4)
+    # EAX = t->data
+    81          0/subop/add         3/mod/direct    0/rm32/EAX    .           .             .           .           .               0xc/imm32         # add to EAX
+$clear-trace-stream:loop:
+    # if (EAX >= ECX) break
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare EAX with ECX
+    73/jump-if-greater-or-equal-unsigned  $clear-trace-stream:end/disp8
+    # *EAX = 0
+    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # copy to *EAX
+    # EAX += 4
+    81          0/subop/add         3/mod/direct    0/rm32/EAX    .           .             .           .           .               4/imm32           # add to EAX
+    eb/jump  $clear-trace-stream:loop/disp8
+$clear-trace-stream:end:
+    # . restore registers
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
 # - helpers
 
 # 3-argument variant of _append
@@ -321,10 +933,10 @@ _append-4:  # out : address, outend : address, in : address, inend : address ->
 $_append-4:loop:
     # if (in >= inend) break
     39/compare                      3/mod/direct    6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # compare ESI with ECX
-    7d/jump-if-greater-or-equal  $_append-4:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $_append-4:end/disp8
     # if (out >= outend) abort  # just to catch test failures fast
     39/compare                      3/mod/direct    7/rm32/EDI    .           .             .           2/r32/EDX   .               .                 # compare EDI with EDX
-    7d/jump-if-greater-or-equal  $_append-4:abort/disp8
+    73/jump-if-greater-or-equal-unsigned  $_append-4:abort/disp8
     # *out = *in
     8a/copy-byte                    0/mod/indirect  6/rm32/ESI    .           .             .           3/r32/BL    .               .                 # copy byte at *ESI to BL
     88/copy-byte                    0/mod/indirect  7/rm32/EDI    .           .             .           3/r32/BL    .               .                 # copy byte at BL to *EDI
@@ -362,4 +974,36 @@ $_append-4:abort:
     cd/syscall  0x80/imm8
     # never gets here
 
+== data
+
+_test-stream-line-ABABA:
+    # write
+    8/imm32
+    # read
+    0/imm32
+    # length
+    8/imm32
+    # data
+    41 42 41 42 41 0A 00 00  # 8 bytes
+
+_test-stream-empty:
+    # write
+    0/imm32
+    # read
+    0/imm32
+    # length
+    8/imm32
+    # data
+    00 00 00 00 00 00 00 00  # 8 bytes
+
+_test-stream-filled:
+    # write
+    8/imm32
+    # read
+    0/imm32
+    # length
+    8/imm32
+    # data
+    41 41 41 41 0A 41 41 41  # 8 bytes
+
 # . . vim:nowrap:textwidth=0
diff --git a/subx/058stream-equal.subx b/subx/058stream-equal.subx
index 64ea7d4c..68296212 100644
--- a/subx/058stream-equal.subx
+++ b/subx/058stream-equal.subx
@@ -5,13 +5,6 @@
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#?     e8/call test-next-stream-line-equal-stops-at-newline/disp32
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 # compare all the data in a stream (ignoring the read pointer)
 stream-data-equal?:  # f : (address stream), s : (address string) -> EAX : boolean
     # . prolog
@@ -26,7 +19,7 @@ stream-data-equal?:  # f : (address stream), s : (address string) -> EAX : boole
     8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
     # EAX = f->write
     8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
-    # max/EDX = f->data + f->write
+    # maxf/EDX = f->data + f->write
     8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  0/index/EAX   .           2/r32/EDX   0xc/disp8       .                 # copy ESI+EAX+12 to EDX
     # currf/ESI = f->data
     81          0/subop/add         3/mod/direct    6/rm32/ESI    .           .             .           .           .               0xc/imm32         # add to ESI
@@ -42,9 +35,9 @@ $stream-data-equal?:compare-lengths:
     31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
     31/xor                          3/mod/direct    1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # clear ECX
 $stream-data-equal?:loop:
-    # if (curr >= max) return true
+    # if (currf >= maxf) return true
     39/compare                      3/mod/direct    6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # compare ESI with EDX
-    7d/jump-if-greater-or-equal  $stream-data-equal?:true/disp8
+    73/jump-if-greater-or-equal-unsigned  $stream-data-equal?:true/disp8
     # AL = *currs
     8a/copy-byte                    0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/AL    .               .                 # copy byte at *ESI to AL
     # CL = *curr
diff --git a/subx/059stop.subx b/subx/059stop.subx
index 0b842e5d..dbe8a663 100644
--- a/subx/059stop.subx
+++ b/subx/059stop.subx
@@ -37,13 +37,6 @@
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#?     e8/call test-stop-skips-returns-on-exit/disp32
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 # Configure an exit-descriptor for a call pushing 'nbytes' bytes of args to
 # the stack.
 # Ugly that we need to know the size of args, but so it goes.
diff --git a/subx/060read.subx b/subx/060read.subx
index d377a1ad..61ee75b0 100644
--- a/subx/060read.subx
+++ b/subx/060read.subx
@@ -165,10 +165,10 @@ _buffer-4:  # out : address, outend : address, in : address, inend : address ->
 $_buffer-4:loop:
     # if (in >= inend) break
     39/compare                      3/mod/direct    6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # compare ESI with ECX
-    7d/jump-if-greater-or-equal  $_buffer-4:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $_buffer-4:end/disp8
     # if (out >= outend) break  # for now silently ignore filled up buffer
     39/compare                      3/mod/direct    7/rm32/EDI    .           .             .           2/r32/EDX   .               .                 # compare EDI with EDX
-    7d/jump-if-greater-or-equal  $_buffer-4:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $_buffer-4:end/disp8
     # *out = *in
     8a/copy-byte                    0/mod/indirect  6/rm32/ESI    .           .             .           3/r32/BL    .               .                 # copy byte at *ESI to BL
     88/copy-byte                    0/mod/indirect  7/rm32/EDI    .           .             .           3/r32/BL    .               .                 # copy byte at BL to *EDI
diff --git a/subx/061read-byte.subx b/subx/061read-byte.subx
index 81080393..99e2babe 100644
--- a/subx/061read-byte.subx
+++ b/subx/061read-byte.subx
@@ -31,14 +31,6 @@ Stdin:
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#?     e8/call test-read-byte-buffered-multiple/disp32
-#?     e8/call test-read-byte-buffered-refills-buffer/disp32
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 # return next byte value in EAX, with top 3 bytes cleared.
 # On reaching end of file, return 0xffffffff (Eof).
 read-byte-buffered:  # f : (address buffered-file) -> byte-or-Eof/EAX
diff --git a/subx/065hex.subx b/subx/065hex.subx
index 8ea76ba5..d188d4d3 100644
--- a/subx/065hex.subx
+++ b/subx/065hex.subx
@@ -23,7 +23,7 @@ is-hex-int?:  # in : (address slice) -> EAX : boolean
     # if s is empty return false
     b8/copy-to-EAX  0/imm32/false
     39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $is-hex-int?:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $is-hex-int?:end/disp8
     # skip past leading '-'
     # . if (*curr == '-') ++curr
     31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
@@ -44,7 +44,7 @@ $is-hex-int?:initial-0:
 $is-hex-int?:initial-0x:
     # . if (curr >= in->end) return true
     39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $is-hex-int?:true/disp8
+    73/jump-if-greater-or-equal-unsigned  $is-hex-int?:true/disp8
     # . if (*curr != 'x') jump to loop  # the previous '0' is still valid so doesn't need to be checked again
     31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
     8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           3/r32/BL    .               .                 # copy byte at *ECX to BL
@@ -55,7 +55,7 @@ $is-hex-int?:initial-0x:
 $is-hex-int?:loop:
     # if (curr >= in->end) return true
     39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $is-hex-int?:true/disp8
+    73/jump-if-greater-or-equal-unsigned  $is-hex-int?:true/disp8
     # EAX = is-hex-digit?(*curr)
     # . . push args
     8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
@@ -88,9 +88,14 @@ test-is-hex-int:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "34"
-    68/push  _test-slice-hex-int-end/imm32
-    68/push  _test-slice-hex-int/imm32
+    # (EAX..ECX) = "34"
+    b8/copy-to-EAX  "34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = is-hex-int?(slice)
     # . . push args
@@ -117,9 +122,14 @@ test-is-hex-int-handles-letters:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "34a"
-    68/push  _test-slice-hex-int-letters-end/imm32
-    68/push  _test-slice-hex-int-letters/imm32
+    # (EAX..ECX) = "34a"
+    b8/copy-to-EAX  "34a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = is-hex-int?(slice)
     # . . push args
@@ -146,9 +156,14 @@ test-is-hex-int-with-trailing-char:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "34q"
-    68/push  _test-slice-digits-and-char-end/imm32
-    68/push  _test-slice-digits-and-char/imm32
+    # (EAX..ECX) = "34q"
+    b8/copy-to-EAX  "34q"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = is-hex-int?(slice)
     # . . push args
@@ -175,9 +190,14 @@ test-is-hex-int-with-leading-char:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "q34"
-    68/push  _test-slice-char-and-digits-end/imm32
-    68/push  _test-slice-char-and-digits/imm32
+    # (EAX..ECX) = "q34"
+    b8/copy-to-EAX  "q34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = is-hex-int?(slice)
     # . . push args
@@ -205,8 +225,8 @@ test-is-hex-int-empty:
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
     # var slice/ECX = ""
-    68/push  _test-slice-empty-end/imm32
-    68/push  _test-slice-empty/imm32
+    68/push  0/imm32
+    68/push  0/imm32
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = is-hex-int?(slice)
     # . . push args
@@ -233,9 +253,14 @@ test-is-hex-int-handles-0x-prefix:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "0x3a"
-    68/push  _test-slice-hex-int-with-0x-prefix-end/imm32
-    68/push  _test-slice-hex-int-with-0x-prefix/imm32
+    # (EAX..ECX) = "0x3a"
+    b8/copy-to-EAX  "0x3a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = is-hex-int?(slice)
     # . . push args
@@ -262,9 +287,14 @@ test-is-hex-int-handles-negative:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "-34a"
-    68/push  _test-slice-hex-int-letters-end/imm32
-    68/push  _test-slice-hex-int-letters-negative/imm32
+    # (EAX..ECX) = "-34a"
+    b8/copy-to-EAX  "-34a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = is-hex-int?(slice)
     # . . push args
@@ -291,9 +321,14 @@ test-is-hex-int-handles-negative-0x-prefix:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "-0x3a"
-    68/push  _test-slice-hex-int-with-0x-prefix-end/imm32
-    68/push  _test-slice-hex-int-with-0x-prefix-negative/imm32
+    # (EAX..ECX) = "-0x3a"
+    b8/copy-to-EAX  "-0x3a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = is-hex-int?(slice)
     # . . push args
@@ -356,7 +391,7 @@ $parse-hex-int:initial-0:
 $parse-hex-int:initial-0x:
     # . if (curr >= in->end) return result
     39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $parse-hex-int:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $parse-hex-int:end/disp8
     # . if (*curr != 'x') jump to loop  # the previous '0' is still valid so doesn't need to be checked again
     31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
     8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
@@ -367,7 +402,7 @@ $parse-hex-int:initial-0x:
 $parse-hex-int:loop:
     # if (curr >= in->end) break
     39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $parse-hex-int:negate/disp8
+    73/jump-if-greater-or-equal-unsigned  $parse-hex-int:negate/disp8
     # EAX = from-hex-char(*curr)
     # . . copy arg to EAX
     8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
@@ -400,9 +435,14 @@ test-parse-hex-int-single-digit:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "a"
-    68/push  _test-slice-hex-int-single-letter-end/imm32
-    68/push  _test-slice-hex-int-single-letter/imm32
+    # (EAX..ECX) = "a"
+    b8/copy-to-EAX  "a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = parse-hex-int(slice)
     # . . push args
@@ -429,9 +469,14 @@ test-parse-hex-int-multi-digit:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "34a"
-    68/push  _test-slice-hex-int-letters-end/imm32
-    68/push  _test-slice-hex-int-letters/imm32
+    # (EAX..ECX) = "34a"
+    b8/copy-to-EAX  "34a"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = parse-hex-int(slice)
     # . . push args
@@ -458,9 +503,14 @@ test-parse-hex-int-0x-prefix:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "0x34"
-    68/push  _test-slice-hex-int-with-0x-prefix-end/imm32
-    68/push  _test-slice-hex-int-with-0x-prefix/imm32
+    # (EAX..ECX) = "0x34"
+    b8/copy-to-EAX  "0x34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = parse-hex-int(slice)
     # . . push args
@@ -487,9 +537,14 @@ test-parse-hex-int-zero:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "0"
-    68/push  _test-slice-hex-int-zero-end/imm32
-    68/push  _test-slice-hex-int-zero/imm32
+    # (EAX..ECX) = "0"
+    b8/copy-to-EAX  "0"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = parse-hex-int(slice)
     # . . push args
@@ -516,9 +571,14 @@ test-parse-hex-int-0-prefix:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "03"
-    68/push  _test-slice-hex-int-with-0-prefix-end/imm32
-    68/push  _test-slice-hex-int-with-0-prefix/imm32
+    # (EAX..ECX) = "03"
+    b8/copy-to-EAX  "03"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = parse-hex-int(slice)
     # . . push args
@@ -545,9 +605,14 @@ test-parse-hex-int-negative:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "-03"
-    68/push  _test-slice-hex-int-negative-with-0-prefix-end/imm32
-    68/push  _test-slice-hex-int-negative-with-0-prefix/imm32
+    # (EAX..ECX) = "-03"
+    b8/copy-to-EAX  "-03"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = parse-hex-int(slice)
     # . . push args
@@ -729,7 +794,7 @@ test-hex-above-f:
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
     c3/return
 
-from-hex-char:  # in/EAX : byte -> out/EAX : num
+from-hex-char:  # in/EAX : byte -> out/EAX : nibble
     # no error checking; accepts argument in EAX
     # if (EAX <= '9') return EAX - '0'
     3d/compare-EAX-with  0x39/imm32/9
@@ -753,50 +818,4 @@ $to-hex-char:else:
     05/add-to-EAX  0x57/imm32/a-10
     c3/return
 
-== data
-
-_test-slice-empty:
-  # nothing
-_test-slice-empty-end:
-
-_test-slice-hex-int:
-  33/3 34/4
-_test-slice-hex-int-end:
-
-_test-slice-hex-int-letters-negative:
-  2d/-
-_test-slice-hex-int-letters:
-  33/3 34/4 61/a
-_test-slice-hex-int-letters-end:
-
-_test-slice-hex-int-single-letter:
-  61/a
-_test-slice-hex-int-single-letter-end:
-
-_test-slice-char-and-digits:
-  71/q 33/3 34/4
-_test-slice-char-and-digits-end:
-
-_test-slice-digits-and-char:
-  33/3 34/4 71/q
-_test-slice-digits-and-char-end:
-
-_test-slice-hex-int-with-0x-prefix-negative:
-  2d/-
-_test-slice-hex-int-with-0x-prefix:
-  30/0 78/x 33/3 34/4
-_test-slice-hex-int-with-0x-prefix-end:
-
-_test-slice-hex-int-zero:
-  30/0
-_test-slice-hex-int-zero-end:
-
-_test-slice-hex-int-with-0-prefix:
-  30/0 33/3
-_test-slice-hex-int-with-0-prefix-end:
-
-_test-slice-hex-int-negative-with-0-prefix:
-  2d/- 30/0 33/3
-_test-slice-hex-int-negative-with-0-prefix-end:
-
 # . . vim:nowrap:textwidth=0
diff --git a/subx/066write-buffered.subx b/subx/066write-buffered.subx
index 343aa364..35f75199 100644
--- a/subx/066write-buffered.subx
+++ b/subx/066write-buffered.subx
@@ -5,14 +5,6 @@
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#?     e8/call test-write-buffered/disp32
-#?     e8/call test-write-buffered-with-intermediate-flush/disp32
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 write-buffered:  # f : (address buffered-file), msg : (address array byte) -> <void>
     # pseudocode:
     #   in = msg->data
@@ -60,7 +52,7 @@ write-buffered:  # f : (address buffered-file), msg : (address array byte) -> <v
 $write-buffered:loop:
     # if (in >= inend) break
     39/compare                      3/mod/direct    6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # compare ESI with ECX
-    7d/jump-if-greater-or-equal  $write-buffered:loop-end/disp8
+    73/jump-if-greater-or-equal-unsigned  $write-buffered:loop-end/disp8
     # if (f->write >= f->length) flush and clear f's stream
     39/compare                      3/mod/direct    3/rm32/EBX    .           .             .           2/r32/EDX   .               .                 # compare EBX with EDX
     7c/jump-if-lesser  $write-buffered:to-stream/disp8
diff --git a/subx/067print-int.subx b/subx/067print-int.subx
index 1280f348..6ce50cc3 100644
--- a/subx/067print-int.subx
+++ b/subx/067print-int.subx
@@ -5,13 +5,6 @@
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#?     e8/call test-print-int32/disp32
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 append-byte-hex:  # f : (address stream), n : int -> <void>
     # . prolog
     55/push-EBP
@@ -259,6 +252,23 @@ test-print-int32:
     # . end
     c3/return
 
+# TODO: append to string
+check-ints-equal2: # (a : int, b : int, msg : (address array byte))
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32           # add to ESP
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
 print-int32-buffered:  # f : (address buffered-file), n : int -> <void>
     # pseudocode:
     #  write-buffered(f, "0x")
diff --git a/subx/071read-line.subx b/subx/071read-line.subx
index f33fd035..9189773c 100644
--- a/subx/071read-line.subx
+++ b/subx/071read-line.subx
@@ -3,13 +3,6 @@
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#?     e8/call test-read-line-buffered/disp32
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 # read bytes from 'f' until (and including) a newline and store them into 's'
 # 's' fails to grow if and only if no data found
 # just abort if 's' is too small
diff --git a/subx/072slice.subx b/subx/072slice.subx
index 6d8e174b..e568fbae 100644
--- a/subx/072slice.subx
+++ b/subx/072slice.subx
@@ -6,13 +6,6 @@
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#?     e8/call test-slice-to-string/disp32
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 slice-empty?:  # s : (address slice) -> EAX : boolean
     # . prolog
     55/push-EBP
@@ -154,7 +147,7 @@ $slice-equal?:nonnull-string:
 $slice-equal?:loop:
     # if (currs >= maxs) return true
     39/compare                      3/mod/direct    2/rm32/EDX    .           .             .           6/r32/ESI   .               .                 # compare EDX with ESI
-    7d/jump-if-greater-or-equal  $slice-equal?:true/disp8
+    73/jump-if-greater-or-equal-unsigned  $slice-equal?:true/disp8
     # AL = *currp
     8a/copy-byte                    0/mod/indirect  3/rm32/EBX    .           .             .           0/r32/AL    .               .                 # copy byte at *EBX to AL
     # CL = *currs
@@ -188,9 +181,14 @@ test-slice-equal:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-equal?(ECX, "Abc")
     # . . push args
@@ -219,9 +217,14 @@ test-slice-equal-false:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-4/imm32/end
-    68/push  _test-slice-data-1/imm32/start
+    # (EAX..ECX) = "bcd"
+    b8/copy-to-EAX  "bcd"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-equal?(ECX, "Abc")
     # . . push args
@@ -250,9 +253,14 @@ test-slice-equal-too-long:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-4/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abcd"
+    b8/copy-to-EAX  "Abcd"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-equal?(ECX, "Abc")
     # . . push args
@@ -281,9 +289,14 @@ test-slice-equal-too-short:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-1/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "A"
+    b8/copy-to-EAX  "A"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-equal?(ECX, "Abc")
     # . . push args
@@ -313,8 +326,8 @@ test-slice-equal-empty:
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
     # var slice/ECX
-    68/push  _test-slice-data-0/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    68/push  0/imm32/end
+    68/push  0/imm32/start
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-equal?(ECX, "Abc")
     # . . push args
@@ -343,9 +356,14 @@ test-slice-equal-with-empty:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-2/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Ab"
+    b8/copy-to-EAX  "Ab"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-equal?(ECX, "")
     # . . push args
@@ -375,8 +393,8 @@ test-slice-equal-empty-with-empty:
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
     # var slice/ECX
-    68/push  _test-slice-data-0/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    68/push  0/imm32/end
+    68/push  0/imm32/start
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-equal?(ECX, "")
     # . . push args
@@ -405,9 +423,14 @@ test-slice-equal-with-null:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-2/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Ab"
+    b8/copy-to-EAX  "Ab"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-equal?(ECX, 0)
     # . . push args
@@ -523,9 +546,14 @@ test-slice-starts-with-single-character:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-starts-with?(ECX, "A")
     # . . push args
@@ -554,9 +582,14 @@ test-slice-starts-with-empty-string:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-starts-with?(ECX, "")
     # . . push args
@@ -585,9 +618,14 @@ test-slice-starts-with-multiple-characters:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-starts-with?(ECX, "Ab")
     # . . push args
@@ -616,9 +654,14 @@ test-slice-starts-with-entire-string:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-starts-with?(ECX, "Abc")
     # . . push args
@@ -647,9 +690,14 @@ test-slice-starts-with-fails:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-starts-with?(ECX, "Abd")
     # . . push args
@@ -678,9 +726,14 @@ test-slice-starts-with-fails-2:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-starts-with?(ECX, "Ac")
     # . . push args
@@ -704,6 +757,124 @@ test-slice-starts-with-fails-2:
     5d/pop-to-EBP
     c3/return
 
+# write a slice to a stream
+# abort if the stream doesn't have enough space
+write-slice:  # out : (address stream), s : (address slice)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    56/push-ESI
+    57/push-EDI
+    # ESI = s
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
+    # curr/ECX = s->start
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
+    # max/ESI = s->end
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           6/r32/ESI   4/disp8         .                 # copy *(ESI+4) to ESI
+    # EDI = out
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         7/r32/EDI   8/disp8         .                 # copy *(EBP+8) to EDI
+    # EDX = out->length
+    8b/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           2/r32/EDX   8/disp8         .                 # copy *(EDI+8) to EDX
+    # EBX = out->write
+    8b/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           3/r32/EBX   .               .                 # copy *EDI to EBX
+$write-slice:loop:
+    # if (curr >= max) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           6/r32/ESI   .               .                 # compare ECX with ESI
+    73/jump-if-greater-or-equal-unsigned  $write-slice:loop-end/disp8
+    # if (out->write >= out->length) abort
+    39/compare                      3/mod/direct    3/rm32/EBX    .           .             .           2/r32/EDX   .               .                 # compare EBX with EDX
+    7d/jump-if-greater-or-equal  $write-slice:abort/disp8
+    # out->data[out->write] = *in
+    # . AL = *in
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
+    # . out->data[out->write] = AL
+    88/copy-byte                    1/mod/*+disp8   4/rm32/sib    7/base/EDI  3/index/EBX   .           0/r32/AL    0xc/disp8       .                 # copy AL to *(EDI+EBX+12)
+    # ++out->write
+    43/increment-EBX
+    # ++in
+    41/increment-ECX
+    eb/jump  $write-slice:loop/disp8
+$write-slice:loop-end:
+    # persist out->write
+    89/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           3/r32/EBX   .               .                 # copy EBX to *EDI
+$write-slice:end:
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+$write-slice:abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "write-slice: out of space"/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+test-write-slice:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # write-slice(_test-stream, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-stream-equal(_test-stream, "Abc", msg)
+    # . . push args
+    68/push  "F - test-write-slice"/imm32
+    68/push  "Abc"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# write a slice to a buffered-file
 write-slice-buffered:  # out : (address buffered-file), s : (address slice)
     # . prolog
     55/push-EBP
@@ -730,7 +901,7 @@ write-slice-buffered:  # out : (address buffered-file), s : (address slice)
 $write-slice-buffered:loop:
     # if (curr >= max) break
     39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           6/r32/ESI   .               .                 # compare ECX with ESI
-    7d/jump-if-greater-or-equal  $write-slice-buffered:loop-end/disp8
+    73/jump-if-greater-or-equal-unsigned  $write-slice-buffered:loop-end/disp8
     # if (out->write >= out->length) flush and clear out's stream
     39/compare                      3/mod/direct    3/rm32/EBX    .           .             .           2/r32/EDX   .               .                 # compare EBX with EDX
     7c/jump-if-lesser  $write-slice-buffered:to-stream/disp8
@@ -802,9 +973,14 @@ test-write-slice-buffered:
     e8/call  clear-stream/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "Abc"
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # write-slice-buffered(_test-buffered-file, slice)
     # . . push args
@@ -929,9 +1105,14 @@ test-slice-to-string:
     e8/call  new-segment/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # var slice/ECX = "Abc"
-    68/push  _test-slice-data-3/imm32/end
-    68/push  _test-slice-data-0/imm32/start
+    # (EAX..ECX) = "Abc"
+    b8/copy-to-EAX  "Abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # EAX = slice-to-string(heap, slice)
     # . . push args
@@ -989,16 +1170,4 @@ test-slice-to-string:
     5d/pop-to-EBP
     c3/return
 
-== data
-
-_test-slice-data-0:
-    41/A
-_test-slice-data-1:
-    62/b
-_test-slice-data-2:
-    63/c
-_test-slice-data-3:
-    64/d
-_test-slice-data-4:
-
 # . . vim:nowrap:textwidth=0
diff --git a/subx/073next-token.subx b/subx/073next-token.subx
index a7a894e3..942d9878 100644
--- a/subx/073next-token.subx
+++ b/subx/073next-token.subx
@@ -1,15 +1,10 @@
+# Some tokenization primitives.
+
 == code
 #   instruction                     effective address                                                   register    displacement    immediate
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#? e8/call test-next-token-from-slice/disp32
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 # extract the next run of characters that are different from a given 'delimiter' (skipping multiple delimiters if necessary)
 # on reaching end of file, return an empty interval
 next-token:  # in : (address stream), delimiter : byte, out : (address slice)
@@ -698,7 +693,7 @@ skip-chars-matching-in-slice:  # curr : (address byte), end : (address byte), de
 $skip-chars-matching-in-slice:loop:
     # if (curr >= end) break
     39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare EAX with ECX
-    7d/jump-if-greater-or-equal  $skip-chars-matching-in-slice:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $skip-chars-matching-in-slice:end/disp8
     # if (*curr != delimiter) break
     8a/copy-byte                    0/mod/indirect  0/rm32/EAX    .           .             .           3/r32/BL    .               .                 # copy byte at *EAX to BL
     39/compare                      3/mod/direct    3/rm32/EBX    .           .             .           2/r32/EDX   .               .                 # compare EBX and EDX
@@ -724,7 +719,7 @@ test-skip-chars-matching-in-slice:
     05/add-to-EAX  4/imm32
     # EAX = skip-chars-matching-in-slice(EAX, ECX, 0x20/space)
     # . . push args
-    68/push  0x20/imm32
+    68/push  0x20/imm32/space
     51/push-ECX
     50/push-EAX
     # . . call
@@ -753,7 +748,7 @@ test-skip-chars-matching-in-slice-none:
     05/add-to-EAX  4/imm32
     # EAX = skip-chars-matching-in-slice(EAX, ECX, 0x20/space)
     # . . push args
-    68/push  0x20/imm32
+    68/push  0x20/imm32/space
     51/push-ECX
     50/push-EAX
     # . . call
@@ -794,7 +789,7 @@ skip-chars-not-matching-in-slice:  # curr : (address byte), end : (address byte)
 $skip-chars-not-matching-in-slice:loop:
     # if (curr >= end) break
     39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare EAX with ECX
-    7d/jump-if-greater-or-equal  $skip-chars-not-matching-in-slice:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $skip-chars-not-matching-in-slice:end/disp8
     # if (*curr == delimiter) break
     8a/copy-byte                    0/mod/indirect  0/rm32/EAX    .           .             .           3/r32/BL    .               .                 # copy byte at *EAX to BL
     39/compare                      3/mod/direct    3/rm32/EBX    .           .             .           2/r32/EDX   .               .                 # compare EBX and EDX
@@ -820,7 +815,7 @@ test-skip-chars-not-matching-in-slice:
     05/add-to-EAX  4/imm32
     # EAX = skip-chars-not-matching-in-slice(EAX, ECX, 0x20/space)
     # . . push args
-    68/push  0x20/imm32
+    68/push  0x20/imm32/space
     51/push-ECX
     50/push-EAX
     # . . call
@@ -849,7 +844,7 @@ test-skip-chars-not-matching-in-slice-none:
     05/add-to-EAX  4/imm32
     # EAX = skip-chars-not-matching-in-slice(EAX, ECX, 0x20/space)
     # . . push args
-    68/push  0x20/imm32
+    68/push  0x20/imm32/space
     51/push-ECX
     50/push-EAX
     # . . call
@@ -878,7 +873,7 @@ test-skip-chars-not-matching-in-slice-all:
     05/add-to-EAX  4/imm32
     # EAX = skip-chars-not-matching-in-slice(EAX, ECX, 0x20/space)
     # . . push args
-    68/push  0x20/imm32
+    68/push  0x20/imm32/space
     51/push-ECX
     50/push-EAX
     # . . call
diff --git a/subx/074print-int-decimal.subx b/subx/074print-int-decimal.subx
index 3b4f4830..f6ea490f 100644
--- a/subx/074print-int-decimal.subx
+++ b/subx/074print-int-decimal.subx
@@ -5,14 +5,6 @@
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
-#? Entry:  # run a single test, while debugging
-#?     e8/call test-print-int32-decimal-negative/disp32
-#?
-#?     # syscall(exit, Num-test-failures)
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     b8/copy-to-EAX  1/imm32/exit
-#?     cd/syscall  0x80/imm8
-
 print-int32-decimal:  # out : (address stream), n : int32
     # works by generating characters from lowest to highest and pushing them
     # to the stack, before popping them one by one into the stream
@@ -97,7 +89,7 @@ $print-int32-decimal:write-loop:
     74/jump-if-equal  $print-int32-decimal:write-break/disp8
     # if (curr >= max) abort
     39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           3/r32/EBX   .               .                 # compare ECX with EBX
-    7d/jump-if-greater-or-equal  $print-int32-decimal:abort/disp8
+    73/jump-if-greater-or-equal-unsigned  $print-int32-decimal:abort/disp8
 $print-int32-decimal:write-char:
     # *curr = AL
     88/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy AL to byte at *ECX
diff --git a/subx/075array-equal.subx b/subx/075array-equal.subx
new file mode 100644
index 00000000..60694829
--- /dev/null
+++ b/subx/075array-equal.subx
@@ -0,0 +1,629 @@
+# Comparing arrays of numbers.
+
+== code
+#   instruction                     effective address                                                   register    displacement    immediate
+# . op          subop               mod             rm32          base        index         scale       r32
+# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
+
+Entry:
+    # initialize heap
+    # . Heap = new-segment(64KB)
+    # . . push args
+    68/push  Heap/imm32
+    68/push  0x10000/imm32/64KB
+    # . . call
+    e8/call  new-segment/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+
+    e8/call  run-tests/disp32  # 'run-tests' is a function created automatically by SubX. It calls all functions that start with 'test-'.
+$array-equal-main:end:
+    # syscall(exit, Num-test-failures)
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+
+array-equal?:  # a : (address array int), b : (address array int) -> EAX : boolean
+    # pseudocode:
+    #   lena = a->length
+    #   if (lena != b->length) return false
+    #   i = 0
+    #   curra = a->data
+    #   currb = b->data
+    #   while i < lena
+    #     i1 = *curra
+    #     i2 = *currb
+    #     if (c1 != c2) return false
+    #     i+=4, curra+=4, currb+=4
+    #   return true
+    #
+    # registers:
+    #   i: ECX
+    #   lena: EDX
+    #   curra: ESI
+    #   currb: EDI
+    #   i1: EAX
+    #   i2: EBX
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    56/push-ESI
+    57/push-EDI
+    # ESI = a
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # EDI = b
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   0xc/disp8       .                 # copy *(EBP+12) to EDI
+    # lena/EDX = a->length
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
+$array-equal?:lengths:
+    # if (lena != b->length) return false
+    39/compare                      0/mod/indirect  7/rm32/EDI    .           .             .           2/r32/EDX   .               .                 # compare *EDI and EDX
+    75/jump-if-not-equal  $array-equal?:false/disp8
+    # curra/ESI = a->data
+    81          0/subop/add         3/mod/direct    6/rm32/ESI    .           .             .           .           .               4/imm32           # add to ESI
+    # currb/EDI = b->data
+    81          0/subop/add         3/mod/direct    7/rm32/EDI    .           .             .           .           .               4/imm32           # add to EDI
+    # i/ECX = i1/EAX = i2/EBX = 0
+    31/xor                          3/mod/direct    1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # clear ECX
+$array-equal?:loop:
+    # if (i >= lena) return true
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    7d/jump-if-greater-or-equal  $array-equal?:true/disp8
+    # i1 = *curra
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
+    # i2 = *currb
+    8b/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           3/r32/EBX   .               .                 # copy *EDI to EBX
+    # if (i1 != i2) return false
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # compare EAX and EBX
+    75/jump-if-not-equal  $array-equal?:false/disp8
+    # i += 4
+    81          0/subop/add         3/mod/direct    1/rm32/ECX    .           .             .           .           .               4/imm32           # add to ECX
+    # currs += 4
+    81          0/subop/add         3/mod/direct    6/rm32/ESI    .           .             .           .           .               4/imm32           # add to ESI
+    # currb += 4
+    81          0/subop/add         3/mod/direct    7/rm32/EDI    .           .             .           .           .               4/imm32           # add to EDI
+    eb/jump  $array-equal?:loop/disp8
+$array-equal?:true:
+    b8/copy-to-EAX  1/imm32
+    eb/jump  $array-equal?:end/disp8
+$array-equal?:false:
+    b8/copy-to-EAX  0/imm32
+$array-equal?:end:
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-compare-empty-with-empty-array:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var ECX = []
+    68/push  0/imm32/size
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # var EDX = []
+    68/push  0/imm32/size
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # EAX = array-equal?(ECX, EDX)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  array-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-compare-empty-with-empty-array"/imm32
+    68/push  1/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-compare-empty-with-non-empty-array:  # also checks length-mismatch code path
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var ECX = [1]
+    68/push  1/imm32
+    68/push  4/imm32/size
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # var EDX = []
+    68/push  0/imm32/size
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # EAX = array-equal?(ECX, EDX)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  array-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-compare-empty-with-non-empty-array"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-compare-equal-arrays:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var ECX = [1, 2, 3]
+    68/push  3/imm32
+    68/push  2/imm32
+    68/push  1/imm32
+    68/push  0xc/imm32/size
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # var EDX = [1, 2, 3]
+    68/push  3/imm32
+    68/push  2/imm32
+    68/push  1/imm32
+    68/push  0xc/imm32/size
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # EAX = array-equal?(ECX, EDX)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  array-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-compare-equal-arrays"/imm32
+    68/push  1/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-compare-inequal-arrays-equal-lengths:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var ECX = [1, 4, 3]
+    68/push  3/imm32
+    68/push  4/imm32
+    68/push  1/imm32
+    68/push  0xc/imm32/size
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # var EDX = [1, 2, 3]
+    68/push  3/imm32
+    68/push  2/imm32
+    68/push  1/imm32
+    68/push  0xc/imm32/size
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # EAX = array-equal?(ECX, EDX)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  array-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-compare-inequal-arrays-equal-lengths"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+parse-array-of-ints:  # ad : (address allocation-descriptor), s : (address string) -> result/EAX : (address array int)
+    # pseudocode
+    #   end = s->data + s->length
+    #   curr = s->data
+    #   size = 0
+    #   while true
+    #     if (curr >= end) break
+    #     curr = skip-chars-matching-in-slice(curr, end, ' ')
+    #     if (curr >= end) break
+    #     curr = skip-chars-not-matching-in-slice(curr, end, ' ')
+    #     ++size
+    #   result = allocate(ad, (size+1)*4)
+    #   result->size = (size+1)*4
+    #   var slice = {s->data, 0}
+    #   out = result->data
+    #   while true
+    #     if (slice->start >= end) break
+    #     slice->start = skip-chars-matching-in-slice(slice->start, end, ' ')
+    #     if (slice->start >= end) break
+    #     slice->end = skip-chars-not-matching-in-slice(slice->start, end, ' ')
+    #     *out = parse-hex-int(slice)
+    #     out += 4
+    #     slice->start = slice->end
+    #   return result
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    56/push-ESI
+    57/push-EDI
+    # ESI = s
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
+    # curr/ECX = s->data
+    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy ESI+4 to ECX
+    # end/EDX = s->data + s->length
+    # . EDX = s->length
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
+    # . EDX += curr
+    01/add                          3/mod/direct    2/rm32/EDX    .           .             .           1/r32/ECX   .               .                 # add ECX to EDX
+    # size/EBX = 0
+    31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
+$parse-array-of-ints:loop1:
+    # if (curr >= end) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $parse-array-of-ints:break1/disp8
+    # curr = skip-chars-matching-in-slice(curr, end, ' ')
+    # . EAX = skip-chars-matching-in-slice(curr, end, ' ')
+    # . . push args
+    68/push  0x20/imm32/space
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  skip-chars-matching-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . ECX = EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to ECX
+    # if (curr >= end) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $parse-array-of-ints:break1/disp8
+    # curr = skip-chars-not-matching-in-slice(curr, end, ' ')
+    # . EAX = skip-chars-not-matching-in-slice(curr, end, ' ')
+    # . . push args
+    68/push  0x20/imm32/space
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  skip-chars-not-matching-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . ECX = EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to ECX
+    # size += 4
+    81          0/subop/add         3/mod/direct    3/rm32/EBX    .           .             .           .           .               4/imm32           # add to EBX
+    eb/jump  $parse-array-of-ints:loop1/disp8
+$parse-array-of-ints:break1:
+    # result/EDI = allocate(ad, size+4)
+    # . EAX = allocate(ad, size+4)
+    # . . push args
+    89/copy                         3/mod/direct    0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # copy EBX to EAX
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  allocate/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . EDI = EAX
+    89/copy                         3/mod/direct    7/rm32/EDI    .           .             .           0/r32/EAX   .               .                 # copy EAX to EDI
+    # result->size = size
+    89/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # copy EBX to *EAX
+$parse-array-of-ints:pass2:
+    # var slice/ECX = {s->data, 0}
+    # . push 0
+    68/push  0/imm32/end
+    # . push s->data
+    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy ESI+4 to ECX
+    51/push-ECX
+    # . bookmark
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # out/EBX = result->data
+    8d/copy-address                 1/mod/*+disp8   0/rm32/EAX    .           .             .           3/r32/EBX   4/disp8         .                 # copy EAX+4 to EBX
+$parse-array-of-ints:loop2:
+    # if (slice->start >= end) break
+    39/compare                      0/mod/indirect  1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare *ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $parse-array-of-ints:end/disp8
+    # slice->start = skip-chars-matching-in-slice(slice->start, end, ' ')
+    # . EAX = skip-chars-matching-in-slice(slice->start, end, ' ')
+    # . . push args
+    68/push  0x20/imm32/space
+    52/push-EDX
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    # . . call
+    e8/call  skip-chars-matching-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . slice->start = EAX
+    89/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to *ECX
+    # if (slice->start >= end) break
+    39/compare                      0/mod/indirect  1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare *ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $parse-array-of-ints:end/disp8
+    # slice->end = skip-chars-not-matching-in-slice(slice->start, end, ' ')
+    # . EAX = skip-chars-not-matching-in-slice(curr, end, ' ')
+    # . . push args
+    68/push  0x20/imm32/space
+    52/push-EDX
+    50/push-EAX
+    # . . call
+    e8/call  skip-chars-not-matching-in-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . slice->end = EAX
+    89/copy                         1/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(ECX+4)
+    # *out = parse-hex-int(slice)
+    # . EAX = parse-hex-int(slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  parse-hex-int/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # *out = EAX
+    89/copy                         0/mod/indirect  3/rm32/EBX    .           .             .           0/r32/EAX   .               .                 # copy EAX to *EBX
+    # out += 4
+    81          0/subop/add         3/mod/direct    3/rm32/EBX    .           .             .           .           .               4/imm32           # add to EBX
+    # slice->start = slice->end
+    8b/copy                         1/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
+    89/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to *ECX
+    81          0/subop/add         3/mod/direct    1/rm32/ECX    .           .             .           .           .               4/imm32           # add to ECX
+    eb/jump  $parse-array-of-ints:loop2/disp8
+$parse-array-of-ints:end:
+    # return EDI
+    89/copy                         3/mod/direct    0/rm32/EAX    .           .             .           7/r32/EDI   .               .                 # copy EDI to EAX
+    # . reclaim locals
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-parse-array-of-ints:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var ECX = [1, 2, 3]
+    68/push  3/imm32
+    68/push  2/imm32
+    68/push  1/imm32
+    68/push  0xc/imm32/size
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # EAX = parse-array-of-ints(Heap, "1 2 3")
+    # . . push args
+    68/push  "1 2 3"/imm32
+    68/push  Heap/imm32
+    # . . call
+    e8/call  parse-array-of-ints/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EAX = array-equal?(ECX, EAX)
+    # . . push args
+    50/push-EAX
+    51/push-ECX
+    # . . call
+    e8/call  array-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-parse-array-of-ints"/imm32
+    68/push  1/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-parse-array-of-ints-empty:
+    # - empty string = empty array
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # EAX = parse-array-of-ints(Heap, "")
+    # . . push args
+    68/push  ""/imm32
+    68/push  Heap/imm32
+    # . . call
+    e8/call  parse-array-of-ints/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(*EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-parse-array-of-ints-empty"/imm32
+    68/push  0/imm32/size
+    ff          6/subop/push        0/mod/indirect  0/rm32/EAX    .           .             .           .           .               .                 # push *EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-parse-array-of-ints-just-whitespace:
+    # - just whitespace = empty array
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # EAX = parse-array-of-ints(Heap, " ")
+    # . . push args
+    68/push  " "/imm32
+    68/push  Heap/imm32
+    # . . call
+    e8/call  parse-array-of-ints/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(*EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-parse-array-of-ints-empty"/imm32
+    68/push  0/imm32/size
+    ff          6/subop/push        0/mod/indirect  0/rm32/EAX    .           .             .           .           .               .                 # push *EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-parse-array-of-ints-extra-whitespace:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var ECX = [1, 2, 3]
+    68/push  3/imm32
+    68/push  2/imm32
+    68/push  1/imm32
+    68/push  0xc/imm32/size
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # EAX = parse-array-of-ints(Heap, " 1 2  3  ")
+    # . . push args
+    68/push  " 1 2  3  "/imm32
+    68/push  Heap/imm32
+    # . . call
+    e8/call  parse-array-of-ints/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EAX = array-equal?(ECX, EAX)
+    # . . push args
+    50/push-EAX
+    51/push-ECX
+    # . . call
+    e8/call  array-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-parse-array-of-ints-extra-whitespace"/imm32
+    68/push  1/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# helper for later tests
+# compare an array with a string representation of an array literal
+check-array-equal:  # a : (address array int), expected : (address string), msg : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    # var b/ECX = parse-array-of-ints(Heap, expected)
+    # . EAX = parse-array-of-ints(Heap, expected)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    68/push  Heap/imm32
+    # . . call
+    e8/call  parse-array-of-ints/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . b = EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to ECX
+    # EAX = array-equal?(a, b)
+    # . . push args
+    51/push-ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  array-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    68/push  1/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$check-array-equal:end:
+    # . restore registers
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-check-array-equal:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var ECX = [1, 2, 3]
+    68/push  3/imm32
+    68/push  2/imm32
+    68/push  1/imm32
+    68/push  0xc/imm32/size
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # check-array-equal(ECX, "1 2 3", "msg")
+    # . . push args
+    68/push  "F - test-check-array-equal"/imm32
+    68/push  "1 2 3"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  check-array-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+== data
+
+Heap:
+  # curr
+  0/imm32
+  # limit
+  0/imm32
+
+# . . vim:nowrap:textwidth=0
diff --git a/subx/076zero-out.subx b/subx/076zero-out.subx
new file mode 100644
index 00000000..bc19dc21
--- /dev/null
+++ b/subx/076zero-out.subx
@@ -0,0 +1,84 @@
+# Fill a region of memory with zeroes.
+
+== code
+#   instruction                     effective address                                                   register    displacement    immediate
+# . op          subop               mod             rm32          base        index         scale       r32
+# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
+
+zero-out:  # start : address, len : int
+    # pseudocode:
+    #   curr/ESI = start
+    #   i/ECX = 0
+    #   while true
+    #     if (i >= len) break
+    #     *curr = 0
+    #     ++curr
+    #     ++i
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    56/push-ESI
+    # curr/ESI = start
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # i/ECX = 0
+    31/xor                          3/mod/direct    1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # clear ECX
+    # EDX = len
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           2/r32/EDX   0xc/disp8       .                 # copy *(EBP+12) to EDX
+$zero-out:loop:
+    # if (i >= len) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    7d/jump-if-greater-or-equal  $zero-out:end/disp8
+    # *curr = 0
+    c6          0/subop/copy        0/mod/direct    6/rm32/ESI    .           .             .           .           .               0/imm8            # copy byte to *ESI
+    # ++curr
+    46/increment-ESI
+    # ++i
+    41/increment-ECX
+    eb/jump  $zero-out:loop/disp8
+$zero-out:end:
+    # . restore registers
+    5e/pop-to-ESI
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-zero-out:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # region/ECX = 34, 35, 36, 37
+    68/push  0x37363534/imm32
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # zero-out(ECX, 3)
+    # . . push args
+    68/push  3/imm32/len
+    51/push-ECX
+    # . . call
+    e8/call  zero-out/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # first 3 bytes cleared, fourth left alone
+    # . check-ints-equal(*ECX, 0x37000000, msg)
+    # . . push args
+    68/push  "F - test-zero-out"/imm32
+    68/push  0x37000000/imm32
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# . . vim:nowrap:textwidth=0
diff --git a/subx/Readme.md b/subx/Readme.md
index cb00eae0..57efa780 100644
--- a/subx/Readme.md
+++ b/subx/Readme.md
@@ -627,18 +627,26 @@ allocated memory for it.)_
 #### writing to disk
 * `write`: string -> file
   - Can also be used to cat a string into a stream.
-  - Will abort the entire program if there isn't enough room.
+  - Will abort the entire program if destination is a stream and doesn't have
+    enough room.
 * `write-stream`: stream -> file
   - Can also be used to cat one stream into another.
-  - Will abort the entire program if there isn't enough room.
+  - Will abort the entire program if destination is a stream and doesn't have
+    enough room.
+* `write-slice`: slice -> stream
+  - Will abort the entire program if there isn't enough room in the
+    destination stream.
 * `append-byte`: int -> stream
-  - Will abort the entire program if there isn't enough room.
+  - Will abort the entire program if there isn't enough room in the
+    destination stream.
 * `append-byte-hex`: int -> stream
   - textual representation in hex, no '0x' prefix
-  - Will abort the entire program if there isn't enough room.
+  - Will abort the entire program if there isn't enough room in the
+    destination stream.
 * `print-int32`: int -> stream
   - textual representation in hex, including '0x' prefix
-  - Will abort the entire program if there isn't enough room.
+  - Will abort the entire program if there isn't enough room in the
+    destination stream.
 * `write-buffered`: string -> buffered-file
 * `write-slice-buffered`: slice -> buffered-file
 * `flush`: buffered-file
diff --git a/subx/apps/assort b/subx/apps/assort
index a330978f..c903ec47 100755
--- a/subx/apps/assort
+++ b/subx/apps/assort
Binary files differdiff --git a/subx/apps/assort.subx b/subx/apps/assort.subx
index 2350603b..a71270c5 100644
--- a/subx/apps/assort.subx
+++ b/subx/apps/assort.subx
@@ -34,11 +34,6 @@ Entry:
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 
-    # for debugging: run a single test
-#?     e8/call test-convert/disp32
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     eb/jump  $main:end/disp8
-
     # run tests if necessary, convert stdin if not
     # . prolog
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -86,8 +81,8 @@ $main:end:
     cd/syscall  0x80/imm8
 
 # data structure:
-#   row: pair of (address array byte) and (address stream byte)
-#   table: (address stream row)
+#   table: (address stream {string, (address stream byte)})     (8 bytes per row)
+# inefficient; uses sequential search for looking up segments by name
 
 convert:  # in : (address buffered-file), out : (address buffered-file) -> <void>
     # pseudocode:
@@ -439,7 +434,8 @@ test-convert:
     5d/pop-to-EBP
     c3/return
 
-read-segments:  # in : (address buffered-file), table : (address stream row)
+# beware: leaks memory (one name per segment read)
+read-segments:  # in : (address buffered-file), table : (address stream {string, (address stream byte)})
     # pseudocode:
     #   var curr-segment = null
     #   var line = new-stream(512, 1)
@@ -454,13 +450,14 @@ read-segments:  # in : (address buffered-file), table : (address stream row)
     #       continue
     #     if slice-equal?(word-slice, "==")
     #       var segment-name = next-word(line)
-    #       curr-segment = get-or-insert-segment(table, segment-name, Segment-size)
-    #       if curr-segment->write == 0
-    #         rewind-stream(line)
-    #         write-stream(curr-segment, line)
-    #     else
-    #       rewind-stream(line)
-    #       write-stream(curr-segment, line)  # abort if curr-segment overflows
+    #       segment-slot = leaky-get-or-insert-slice(table, segment-name, row-size=8)
+    #       curr-segment = *segment-slot
+    #       if curr-segment != 0
+    #         continue
+    #       curr-segment = new-stream(Segment-size)
+    #       *segment-slot = curr-segment
+    #     rewind-stream(line)
+    #     write-stream(curr-segment, line)  # abort if curr-segment overflows
     #
     # word-slice and segment-name are both slices with disjoint lifetimes, so
     # we'll use the same address for them.
@@ -489,7 +486,7 @@ read-segments:  # in : (address buffered-file), table : (address stream row)
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # var word-slice/EDX = {0, 0}
     68/push  0/imm32/end
-    68/push  0/imm32/curr
+    68/push  0/imm32/start
     89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
 $read-segments:loop:
     # clear-stream(line)
@@ -610,10 +607,7 @@ $read-segments:check-for-segment-header:
 #?     # . . discard args
 #?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 #?     # }}}
-    # if slice-equal?(word-slice, "==")
-    #   segment-name = next-word(line)
-    #   curr-segment = get-or-insert(table, segment-name)
-    #   if (curr-segment->write > 0) continue
+    # if !slice-equal?(word-slice, "==") goto next check
     # . EAX = slice-equal?(word-slice, "==")
     # . . push args
     68/push  "=="/imm32
@@ -625,7 +619,7 @@ $read-segments:check-for-segment-header:
     # . if (EAX == 0) goto check3
     3d/compare-EAX-and  0/imm32
     0f 84/jump-if-equal  $read-segments:regular-line/disp32
-    # . next-word(line, segment-name)
+    # segment-name = next-word(line)
     # . . push args
     52/push-EDX
     51/push-ECX
@@ -675,21 +669,38 @@ $read-segments:check-for-segment-header:
 #?     # . . discard args
 #?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 #?     # }}}
-    # . EAX = get-or-insert-segment(table, segment-name, Segment-size)
+    # segment-slot/EAX = leaky-get-or-insert-slice(table, segment-name, row-size=8)
     # . . push args
-    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Segment-size/disp32               # push *Segment-size
+    68/push  8/imm32/row-size
     52/push-EDX
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
     # . . call
-    e8/call  get-or-insert-segment/disp32
+    e8/call  leaky-get-or-insert-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # curr-segment = *segment-slot
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # copy *EAX to EBX
+    # if (curr-segment != 0) continue
+    81          7/subop/compare     3/mod/direct    3/rm32/EBX    .           .             .           .           .               0/imm32           # compare EBX
+    0f 85/jump-if-not-equal  $read-segments:loop/disp32
+    # curr-segment = new-stream(Heap, Segment-size, 1)
+    # . save segment-slot
+    50/push-EAX
+    # . EAX = new-stream(Heap, Segment-size, 1)
+    # . . push args
+    68/push  1/imm32
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Segment-size/disp32               # push *Segment-size
+    68/push  Heap/imm32
+    # . . call
+    e8/call  new-stream/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
     # . curr-segment = EAX
     89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EBX
-    # . if (curr-segment->write > 0) continue
-    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # copy *EAX to EAX
-    3d/compare-EAX-and  0/imm32
-    0f 8f/jump-if-greater  $read-segments:loop/disp32
+    # . restore segment-slot
+    58/pop-to-EAX
+    # *segment-slot = curr-segment
+    89/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # copy EBX to *EAX
     # fall through
 $read-segments:regular-line:
     # rewind-stream(line)
@@ -724,7 +735,7 @@ $read-segments:end:
     5d/pop-to-EBP
     c3/return
 
-write-segments:  # out : (address buffered-file), table : (address stream row)
+write-segments:  # out : (address buffered-file), table : (address stream {string, (address stream byte)})
     # pseudocode:
     #   var curr = table->data
     #   var max = table->data + table->write
@@ -752,7 +763,7 @@ write-segments:  # out : (address buffered-file), table : (address stream row)
 $write-segments:loop:
     # if (curr >= max) break
     39/compare                      3/mod/direct    6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # compare ESI with EDX
-    7d/jump-if-greater-or-equal  $write-segments:break/disp8
+    73/jump-if-greater-or-equal-unsigned  $write-segments:break/disp8
     # stream/EAX = table[i].stream
     8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
     # write-stream-data(out, stream)
@@ -785,534 +796,9 @@ $write-segments:end:
     5d/pop-to-EBP
     c3/return
 
-## helpers
-
-# TODO: pass in an allocation descriptor
-get-or-insert-segment:  # table : (address stream row), s : (address slice), n : int -> EAX : (address stream)
-    # pseudocode:
-    #   curr = table->data
-    #   max = &table->data[table->write]
-    #   while curr < max
-    #     if slice-equal?(s, *curr)
-    #       return *(curr+4)
-    #     curr += 8
-    #   if table->write < table->length
-    #     *max = slice-to-string(Heap, s)
-    #     result = new-stream(Heap, n, 1)
-    #     *(max+4) = result
-    #     table->write += 8
-    #     return result
-    #   return 0
-    #
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # . save registers
-    51/push-ECX
-    52/push-EDX
-    56/push-ESI
-    # ESI = table
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
-    # curr/ECX = table->data
-    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   0xc/disp8       .                 # copy ESI+12 to ECX
-    # max/EDX = table->data + table->write
-    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
-    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ECX  2/index/EDX   .           2/r32/EDX   .               .                 # copy ECX+EDX to EDX
-$get-or-insert-segment:search-loop:
-    # if (curr >= max) break
-    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $get-or-insert-segment:not-found/disp8
-    # if (slice-equal?(s, *curr)) return *(curr+4)
-    # . EAX = slice-equal?(s, *curr)
-    # . . push args
-    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
-    # . . call
-    e8/call  slice-equal?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . if (EAX != 0) return EAX = *(curr+4)
-    3d/compare-EAX-and  0/imm32
-    74/jump-if-equal  $get-or-insert-segment:mismatch/disp8
-    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
-    eb/jump  $get-or-insert-segment:end/disp8
-$get-or-insert-segment:mismatch:
-    # curr += 8
-    81          0/subop/add         3/mod/direct    1/rm32/ECX    .           .             .           .           .               8/imm32           # add to ECX
-    # loop
-    eb/jump  $get-or-insert-segment:search-loop/disp8
-$get-or-insert-segment:not-found:
-    # result/EAX = 0
-    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
-    # if (table->write >= table->length) abort
-    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
-    3b/compare                      1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   8/disp8         .                 # compare ECX with *(ESI+8)
-    7d/jump-if-greater-or-equal  $get-or-insert-segment:abort/disp8
-    # *max = slice-to-string(Heap, s)
-    # . EAX = slice-to-string(Heap, s)
-    # . . push args
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
-    68/push  Heap/imm32
-    # . . call
-    e8/call  slice-to-string/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . *max = EAX
-    89/copy                         0/mod/indirect  2/rm32/EDX    .           .             .           0/r32/EAX   .               .                 # copy EAX to *EDX
-    # result/EAX = new-stream(Heap, n, 1)
-    # . . push args
-    68/push  1/imm32
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
-    68/push  Heap/imm32
-    # . . call
-    e8/call  new-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # *(max+4) = result
-    89/copy                         1/mod/*+disp8   2/rm32/EDX    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDX+4)
-    # table->write += 8
-    81          0/subop/add         0/mod/indirect  6/rm32/ESI    .           .             .           .           .               8/imm32           # add to *ESI
-$get-or-insert-segment:end:
-    # . restore registers
-    5e/pop-to-ESI
-    5a/pop-to-EDX
-    59/pop-to-ECX
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-$get-or-insert-segment:abort:
-    # . _write(2/stderr, error)
-    # . . push args
-    68/push  "get-or-insert-segment: too many segments\n"/imm32
-    68/push  2/imm32/stderr
-    # . . call
-    e8/call  _write/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . syscall(exit, 1)
-    bb/copy-to-EBX  1/imm32
-    b8/copy-to-EAX  1/imm32/exit
-    cd/syscall  0x80/imm8
-    # never gets here
-
-test-get-or-insert-segment:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var table/ECX : (address stream byte) = stream(2 * 8)
-    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # subtract from ESP
-    68/push  0x10/imm32/length
-    68/push  0/imm32/read
-    68/push  0/imm32/write
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # EDX : (address slice) = "code"
-    68/push  _test-code-segment-end/imm32/end
-    68/push  _test-code-segment/imm32/start
-    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
-$test-get-or-insert-segment:first-call:
-    # - start with an empty table, insert one segment, verify that it was inserted
-    # segment/EAX = get-or-insert-segment(table, "code" slice, 10)
-    # . . push args
-    68/push  0xa/imm32/segment-length
-    52/push-EDX
-    51/push-ECX
-    # . . call
-    e8/call  get-or-insert-segment/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # save segment
-    50/push-EAX
-    # if (segment != 0) goto next check
-    3d/compare-EAX-and  0/imm32
-    75/jump-if-not-equal  $test-get-or-insert-segment:check1/disp8
-    # fail test
-    # . _write(2/stderr, msg)
-    # . . push args
-    68/push  "F - test-get-or-insert-segment/0\n"/imm32
-    68/push  2/imm32/stderr
-    # . . call
-    e8/call  _write/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . increment Num-test-failures
-    ff          0/subop/increment   0/mod/indirect  5/rm32/.disp32            .             .           .           Num-test-failures/disp32          # increment *Num-test-failures
-    e9/jump  $test-get-or-insert-segment:end/disp32
-$test-get-or-insert-segment:check1:
-    # check-ints-equal(segment->length, 10, msg)
-    # . . push args
-    68/push  "F - test-get-or-insert-segment/1"/imm32
-    68/push  0xa/imm32/segment-length
-    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           8/disp8         .                 # push *(EAX+8)
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-$test-get-or-insert-segment:check2:
-    # check-ints-equal(table->write, rowsize = 8, msg)
-    # . . push args
-    68/push  "F - test-get-or-insert-segment/2"/imm32
-    68/push  8/imm32/row-size
-    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # EAX = string-equal?(*table->data, "code")
-    # . . push args
-    68/push  "code"/imm32
-    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           0xc/disp8       .                 # push *(ECX+12)
-    # . . call
-    e8/call  string-equal?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(EAX, 1, msg)
-    # . . push args
-    68/push  "F - test-get-or-insert-segment/3"/imm32
-    68/push  1/imm32
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-$test-get-or-insert-segment:check3:
-    # stream/EAX = *(table->data+4)
-    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   0x10/disp8      .                 # copy *(ECX+16) to EAX
-    # check-ints-equal(stream->length, 10, msg)
-    # . . push args
-    68/push  "F - test-get-or-insert-segment/4"/imm32
-    68/push  0xa/imm32/segment-size
-    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           8/disp8         .                 # push *(EAX+8)
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-$test-get-or-insert-segment:second-call:
-    # - insert the same segment name again, verify that it was reused
-    # segment2/EAX = get-or-insert-segment(table, "code" slice, 8)
-    # . . push args
-    68/push  8/imm32/segment-length
-    52/push-EDX
-    51/push-ECX
-    # . . call
-    e8/call  get-or-insert-segment/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # restore old segment1
-    5a/pop-to-EDX
-    # check-ints-equal(segment2/EAX, segment1/EDX, msg)
-    # . . push args
-    68/push  "F - test-get-or-insert-segment/5"/imm32
-    52/push-EDX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # no change to table size
-    # . check-ints-equal(table->write, rowsize = 8, msg)
-    # . . push args
-    68/push  "F - test-get-or-insert-segment/6"/imm32
-    68/push  8/imm32/row-size
-    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-$test-get-or-insert-segment:third-call:
-    # - insert a new segment name, verify that it was inserted
-    # EDX : (address slice) = "data"
-    c7          0/subop/copy        0/mod/indirect  2/rm32/EDX    .           .             .           .           .               _test-data-segment/imm32  # copy to *EDX
-    c7          0/subop/copy        1/mod/*+disp8   2/rm32/EDX    .           .             .           .           4/disp8         _test-data-segment-end/imm32  # copy to *(EDX+4)
-    # segment2/EAX = get-or-insert-segment(table, "data" slice, 8)
-    # . . push args
-    68/push  8/imm32/segment-length
-    52/push-EDX
-    51/push-ECX
-    # . . call
-    e8/call  get-or-insert-segment/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # table gets a new row
-    # . check-ints-equal(table->write, 2 rows = 16, msg)
-    # . . push args
-    68/push  "F - test-get-or-insert-segment/7"/imm32
-    68/push  0x10/imm32/two-rows
-    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-$test-get-or-insert-segment:end:
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-# (re)compute the bounds of the next word in the line
-# return empty string on reaching end of file
-next-word:  # line : (address stream byte), out : (address slice)
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # . save registers
-    50/push-EAX
-    51/push-ECX
-    56/push-ESI
-    57/push-EDI
-    # ESI = line
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
-    # EDI = out
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   0xc/disp8       .                 # copy *(EBP+12) to EDI
-    # skip-chars-matching(line, ' ')
-    # . . push args
-    68/push  0x20/imm32/space
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
-    # . . call
-    e8/call  skip-chars-matching/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-$next-word:check0:
-    # if (line->read >= line->write) clear out and return
-    # . EAX = line->read
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
-    # . if (EAX < line->write) goto next check
-    3b/compare                      0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # compare EAX with *ESI
-    7c/jump-if-lesser  $next-word:check-for-comment/disp8
-    # . return out = {0, 0}
-    c7          0/subop/copy        0/mod/direct    7/rm32/EDI    .           .             .           .           .               0/imm32           # copy to *EDI
-    c7          0/subop/copy        1/mod/*+disp8   7/rm32/EDI    .           .             .           .           4/disp8         0/imm32           # copy to *(EDI+4)
-    eb/jump  $next-word:end/disp8
-$next-word:check-for-comment:
-    # out->start = &line->data[line->read]
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
-    89/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           0/r32/EAX   .               .                 # copy EAX to *EDI
-    # if (line->data[line->read] == '#') out->end = &line->data[line->write]), skip rest of stream and return
-    # . EAX = line->data[line->read]
-    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
-    8a/copy-byte                    1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/AL    0xc/disp8       .                 # copy byte at *(ESI+ECX+12) to AL
-    # . compare
-    3d/compare-EAX-and  0x23/imm32/pound
-    75/jump-if-not-equal  $next-word:regular-word/disp8
-$next-word:comment:
-    # . out->end = &line->data[line->write]
-    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  0/index/EAX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+EAX+12 to EAX
-    89/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDI+4)
-    # . line->read = line->write
-    89/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(ESI+4)
-    # . return
-    eb/jump  $next-word:end/disp8
-$next-word:regular-word:
-    # otherwise skip-chars-not-matching-whitespace(line)  # including trailing newline
-    # . . push args
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
-    # . . call
-    e8/call  skip-chars-not-matching-whitespace/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # out->end = &line->data[line->read]
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
-    89/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDI+4)
-$next-word:end:
-    # . restore registers
-    5f/pop-to-EDI
-    5e/pop-to-ESI
-    59/pop-to-ECX
-    58/pop-to-EAX
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-next-word:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-stream)
-    # . . push args
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = {0, 0}
-    68/push  0/imm32/end
-    68/push  0/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # write(_test-stream, "  ab")
-    # . . push args
-    68/push  "  ab"/imm32
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  write/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-stream, slice)
-    # . . push args
-    51/push-ECX
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  next-word/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(slice->start - _test-stream->data, 2, msg)
-    # . check-ints-equal(slice->start - _test-stream, 14, msg)
-    # . . push args
-    68/push  "F - test-next-word: start"/imm32
-    68/push  0xe/imm32
-    # . . push slice->start - _test-stream
-    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
-    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # check-ints-equal(slice->end - _test-stream->data, 4, msg)
-    # . check-ints-equal(slice->end - _test-stream, 16, msg)
-    # . . push args
-    68/push  "F - test-next-word: end"/imm32
-    68/push  0x10/imm32
-    # . . push slice->end - _test-stream
-    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
-    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-next-word-returns-whole-comment:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-stream)
-    # . . push args
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = {0, 0}
-    68/push  0/imm32/end
-    68/push  0/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # write(_test-stream, "  # a")
-    # . . push args
-    68/push  "  # a"/imm32
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  write/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-stream, slice)
-    # . . push args
-    51/push-ECX
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  next-word/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(slice->start - _test-stream->data, 2, msg)
-    # . check-ints-equal(slice->start - _test-stream, 14, msg)
-    # . . push args
-    68/push  "F - test-next-word-returns-whole-comment: start"/imm32
-    68/push  0xe/imm32
-    # . . push slice->start - _test-stream
-    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
-    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # check-ints-equal(slice->end - _test-stream->data, 5, msg)
-    # . check-ints-equal(slice->end - _test-stream, 17, msg)
-    # . . push args
-    68/push  "F - test-next-word-returns-whole-comment: end"/imm32
-    68/push  0x11/imm32
-    # . . push slice->end - _test-stream
-    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
-    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-next-word-returns-empty-string-on-eof:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-stream)
-    # . . push args
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = {0, 0}
-    68/push  0/imm32/end
-    68/push  0/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # write nothing to _test-stream
-    # next-word(_test-stream, slice)
-    # . . push args
-    51/push-ECX
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  next-word/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(slice->end - slice->start, 0, msg)
-    # . . push args
-    68/push  "F - test-next-word-returns-empty-string-on-eof"/imm32
-    68/push  0/imm32
-    # . . push slice->end - slice->start
-    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
-    2b/subtract                     0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # subtract *ECX from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
 == data
 
-_test-code-segment:
-  63/c 6f/o 64/d 65/e
-_test-code-segment-end:
-
-_test-data-segment:
-  64/d 61/a 74/t 61/a
-_test-data-segment-end:
-
 Segment-size:
   0x1000/imm32/4KB
 
-Heap:
-  # curr
-  0/imm32
-  # limit
-  0/imm32
-
 # . . vim:nowrap:textwidth=0
diff --git a/subx/apps/crenshaw2-1 b/subx/apps/crenshaw2-1
index 04957ac1..e5e6560c 100755
--- a/subx/apps/crenshaw2-1
+++ b/subx/apps/crenshaw2-1
Binary files differdiff --git a/subx/apps/crenshaw2-1.subx b/subx/apps/crenshaw2-1.subx
index 2f1fec9e..0c3f180b 100644
--- a/subx/apps/crenshaw2-1.subx
+++ b/subx/apps/crenshaw2-1.subx
@@ -31,10 +31,15 @@
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
 Entry:  # run tests if necessary, call 'compile' if not
-
-#?     # for debugging: run a single test; don't bother setting status code
-#?     e8/call test-get-num-aborts-on-non-digit-in-Look/disp32
-#?     eb/jump  $main:end/disp8
+    # initialize heap
+    # . Heap = new-segment(64KB)
+    # . . push args
+    68/push  Heap/imm32
+    68/push  0x10000/imm32/64KB
+    # . . call
+    e8/call  new-segment/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 
     # . prolog
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
diff --git a/subx/apps/crenshaw2-1b b/subx/apps/crenshaw2-1b
index 660d69e9..eebd667a 100755
--- a/subx/apps/crenshaw2-1b
+++ b/subx/apps/crenshaw2-1b
Binary files differdiff --git a/subx/apps/crenshaw2-1b.subx b/subx/apps/crenshaw2-1b.subx
index 8dee0dbc..e1bb6448 100644
--- a/subx/apps/crenshaw2-1b.subx
+++ b/subx/apps/crenshaw2-1b.subx
@@ -31,10 +31,15 @@
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
 Entry:  # run tests if necessary, call 'compile' if not
-
-#?     # for debugging: run a single test; don't bother setting status code
-#?     e8/call test-get-num-reads-single-digit/disp32
-#?     eb/jump  $main:end/disp8
+    # initialize heap
+    # . Heap = new-segment(64KB)
+    # . . push args
+    68/push  Heap/imm32
+    68/push  0x10000/imm32/64KB
+    # . . call
+    e8/call  new-segment/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 
     # . prolog
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
diff --git a/subx/apps/dquotes b/subx/apps/dquotes
index 47b3b7d7..5e277311 100755
--- a/subx/apps/dquotes
+++ b/subx/apps/dquotes
Binary files differdiff --git a/subx/apps/dquotes.subx b/subx/apps/dquotes.subx
index a59c9593..85a42f53 100644
--- a/subx/apps/dquotes.subx
+++ b/subx/apps/dquotes.subx
@@ -30,11 +30,6 @@ Entry:
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 
-    # for debugging: run a single test
-#?     e8/call  test-string-length-at-start-of-slice-escaped/disp32
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     eb/jump  $main:end/disp8
-
     # run tests if necessary, convert stdin if not
     # . prolog
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -95,7 +90,7 @@ convert:  # in : (address buffered-file), out : (address buffered-file) -> <void
     #     read-line-buffered(in, line)
     #     if (line->write == 0) break               # end of file
     #     while true
-    #       var word-slice = next-word(line)
+    #       var word-slice = next-word-or-string(line)
     #       if slice-empty?(word-slice)             # end of line
     #         break
     #       if slice-starts-with?(word-slice, "#")  # comment
@@ -127,7 +122,7 @@ convert:  # in : (address buffered-file), out : (address buffered-file) -> <void
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # var word-slice/EDX = {0, 0}
     68/push  0/imm32/end
-    68/push  0/imm32/curr
+    68/push  0/imm32/start
     89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
     # new-data-segment/EDI = new-stream(Heap, Segment-size, 1)
     # . EAX = new-stream(Heap, Segment-size, 1)
@@ -170,12 +165,12 @@ $convert:check0:
     81          7/subop/compare     0/mod/indirect  1/rm32/ECX    .           .             .           .           .               0/imm32           # compare *ECX
     0f 84/jump-if-equal  $convert:break/disp32
 $convert:word-loop:
-    # next-word(line, word-slice)
+    # next-word-or-string(line, word-slice)
     # . . push args
     52/push-EDX
     51/push-ECX
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 $convert:check1:
@@ -913,7 +908,7 @@ $emit-string-literal-data:loop-init:
 $emit-string-literal-data:loop:
     # if (curr >= max) break
     39/compare                      3/mod/direct    2/rm32/EDX    .           .             .           6/r32/ESI   .               .                 # compare EDX with ESI
-    7d/jump-if-greater-or-equal  $emit-string-literal-data:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $emit-string-literal-data:end/disp8
     # CL = *curr
     8a/copy-byte                    0/mod/indirect  2/rm32/EDX    .           .             .           1/r32/CL    .               .                 # copy byte at *EDX to CL
     # if (ECX == '"') break
@@ -926,7 +921,7 @@ $emit-string-literal-data:loop:
     42/increment-EDX
     # . if (curr >= max) break
     39/compare                      3/mod/direct    2/rm32/EDX    .           .             .           6/r32/ESI   .               .                 # compare EDX with ESI
-    7d/jump-if-greater-or-equal  $emit-string-literal-data:end/disp8
+    73/jump-if-greater-or-equal-unsigned  $emit-string-literal-data:end/disp8
     # . CL = *curr
     8a/copy-byte                    0/mod/indirect  2/rm32/EDX    .           .             .           1/r32/CL    .               .                 # copy byte at *EDX to CL
 $emit-string-literal-data:emit:
@@ -1037,7 +1032,7 @@ test-emit-string-literal-data:
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # var slice/ECX = '"abc"/d'
-    68/push  _test-slice-abc-metadata-end/imm32
+    68/push  _test-slice-abc-limit/imm32
     68/push  _test-slice-abc/imm32
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-string-literal-data(_test-output-stream, slice)
@@ -1101,8 +1096,8 @@ test-emit-string-literal-data-empty:
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # var slice/ECX = '""'
-    68/push  _test-slice-empty-string-literal-end/imm32
-    68/push  _test-slice-empty-string-literal/imm32
+    68/push  0/imm32/end
+    68/push  0/imm32/start
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-string-literal-data(_test-output-stream, slice)
     # . . push args
@@ -1166,7 +1161,7 @@ test-emit-string-literal-data-no-metadata-for-non-alphanumerics:
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # var slice/ECX = '"a b"'
-    68/push  _test-slice-a-space-b-end/imm32
+    68/push  _test-slice-a-space-b-limit/imm32
     68/push  _test-slice-a-space-b/imm32
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-string-literal-data(_test-output-stream, slice)
@@ -1230,7 +1225,7 @@ test-emit-string-literal-data-handles-escape-sequences:
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # var slice/ECX = '"a\"b"'
-    68/push  _test-slice-a-dquote-b-end/imm32
+    68/push  _test-slice-a-dquote-b-limit/imm32
     68/push  _test-slice-a-dquote-b/imm32
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-string-literal-data(_test-output-stream, slice)
@@ -1295,7 +1290,7 @@ emit-metadata:  # out : (address buffered-file), word : (address slice)
     #       if *curr == '/'
     #         break
     #       ++curr
-    #   slice->curr = curr
+    #   slice->start = curr
     #   write-slice-buffered(out, slice)
     #
     # . prolog
@@ -1349,7 +1344,7 @@ $emit-metadata:skip-datum-loop:
     41/increment-ECX
     eb/jump  $emit-metadata:skip-datum-loop/disp8
 $emit-metadata:emit:
-    # slice->curr = ECX
+    # slice->start = ECX
     89/copy                         0/mod/indirect  3/rm32/EBX    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EBX
     # write-slice-buffered(out, slice)
     # . . push args
@@ -1394,9 +1389,14 @@ test-emit-metadata:
     e8/call  clear-stream/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "abc/def"
-    68/push  _test-slice-word-end/imm32
-    68/push  _test-slice-word/imm32/start
+    # (EAX..ECX) = "abc/def"
+    b8/copy-to-EAX  "abc/def"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-metadata(_test-output-buffered-file, slice)
     # . . push args
@@ -1448,9 +1448,14 @@ test-emit-metadata-none:
     e8/call  clear-stream/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "abc"
-    68/push  _test-slice-word-datum-end/imm32
-    68/push  _test-slice-word/imm32/start
+    # (EAX..ECX) = "abc"
+    b8/copy-to-EAX  "abc"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-metadata(_test-output-buffered-file, slice)
     # . . push args
@@ -1502,9 +1507,14 @@ test-emit-metadata-multiple:
     e8/call  clear-stream/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "abc/def/ghi"
-    68/push  _test-slice-word-end2/imm32
-    68/push  _test-slice-word/imm32/start
+    # (EAX..ECX) = "abc/def/ghi"
+    b8/copy-to-EAX  "abc/def/ghi"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-metadata(_test-output-buffered-file, slice)
     # . . push args
@@ -1618,7 +1628,7 @@ test-emit-metadata-in-string-literal:
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # var slice/ECX = "\"abc/def\"/ghi"
-    68/push  _test-slice-literal-string-with-metadata-end/imm32
+    68/push  _test-slice-literal-string-with-limit/imm32
     68/push  _test-slice-literal-string/imm32/start
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # emit-metadata(_test-output-buffered-file, slice)
@@ -1678,7 +1688,7 @@ test-emit-metadata-in-string-literal:
 
 # (re)compute the bounds of the next word in the line
 # return empty string on reaching end of file
-next-word:  # line : (address stream byte), out : (address slice)
+next-word-or-string:  # line : (address stream byte), out : (address slice)
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1699,18 +1709,18 @@ next-word:  # line : (address stream byte), out : (address slice)
     e8/call  skip-chars-matching/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-$next-word:check0:
+$next-word-or-string:check0:
     # if (line->read >= line->write) clear out and return
     # . EAX = line->read
     8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
     # . if (EAX < line->write) goto next check
     3b/compare                      0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # compare EAX with *ESI
-    7c/jump-if-lesser  $next-word:check-for-comment/disp8
+    7c/jump-if-lesser  $next-word-or-string:check-for-comment/disp8
     # . return out = {0, 0}
     c7          0/subop/copy        0/mod/direct    7/rm32/EDI    .           .             .           .           .               0/imm32           # copy to *EDI
     c7          0/subop/copy        1/mod/*+disp8   7/rm32/EDI    .           .             .           .           4/disp8         0/imm32           # copy to *(EDI+4)
-    eb/jump  $next-word:end/disp8
-$next-word:check-for-comment:
+    eb/jump  $next-word-or-string:end/disp8
+$next-word-or-string:check-for-comment:
     # out->start = &line->data[line->read]
     8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
     8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
@@ -1721,8 +1731,8 @@ $next-word:check-for-comment:
     8a/copy-byte                    1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/AL    0xc/disp8       .                 # copy byte at *(ESI+ECX+12) to AL
     # . compare
     3d/compare-EAX-and  0x23/imm32/pound
-    75/jump-if-not-equal  $next-word:check-for-string-literal/disp8
-$next-word:comment:
+    75/jump-if-not-equal  $next-word-or-string:check-for-string-literal/disp8
+$next-word-or-string:comment:
     # out->end = &line->data[line->write]
     8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
     8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  0/index/EAX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+EAX+12 to EAX
@@ -1731,16 +1741,16 @@ $next-word:comment:
     8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
     89/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(ESI+4)
     # return
-    eb/jump  $next-word:end/disp8
-$next-word:check-for-string-literal:
+    eb/jump  $next-word-or-string:end/disp8
+$next-word-or-string:check-for-string-literal:
     # if line->data[line->read] == '"'
     # . EAX = line->data[line->read]
     31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
     8a/copy-byte                    1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/AL    0xc/disp8       .                 # copy byte at *(ESI+ECX+12) to AL
     # . compare
     3d/compare-EAX-and  0x22/imm32/dquote
-    75/jump-if-not-equal  $next-word:regular-word/disp8
-$next-word:string-literal:
+    75/jump-if-not-equal  $next-word-or-string:regular-word/disp8
+$next-word-or-string:string-literal:
     # skip-string(line)
     # . . push args
     56/push-ESI
@@ -1749,7 +1759,7 @@ $next-word:string-literal:
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
     # fall through
-$next-word:regular-word:
+$next-word-or-string:regular-word:
     # skip-chars-not-matching-whitespace(line)  # including trailing newline
     # . . push args
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
@@ -1761,7 +1771,7 @@ $next-word:regular-word:
     8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
     8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
     89/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDI+4)
-$next-word:end:
+$next-word-or-string:end:
     # . restore registers
     5f/pop-to-EDI
     5e/pop-to-ESI
@@ -1772,7 +1782,7 @@ $next-word:end:
     5d/pop-to-EBP
     c3/return
 
-test-next-word:
+test-next-word-or-string:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1796,17 +1806,17 @@ test-next-word:
     e8/call  write/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # check-ints-equal(_test-input-stream->read, 4, msg)
     # . . push args
-    68/push  "F - test-next-word/updates-stream-read-correctly"/imm32
+    68/push  "F - test-next-word-or-string/updates-stream-read-correctly"/imm32
     68/push  4/imm32
     b8/copy-to-EAX  _test-input-stream/imm32
     ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
@@ -1817,7 +1827,7 @@ test-next-word:
     # check-ints-equal(slice->start - _test-input-stream->data, 2, msg)
     # . check-ints-equal(slice->start - _test-input-stream, 14, msg)
     # . . push args
-    68/push  "F - test-next-word: start"/imm32
+    68/push  "F - test-next-word-or-string: start"/imm32
     68/push  0xe/imm32
     # . . push slice->start - _test-input-stream
     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
@@ -1830,7 +1840,7 @@ test-next-word:
     # check-ints-equal(slice->end - _test-input-stream->data, 4, msg)
     # . check-ints-equal(slice->end - _test-input-stream, 16, msg)
     # . . push args
-    68/push  "F - test-next-word: end"/imm32
+    68/push  "F - test-next-word-or-string: end"/imm32
     68/push  0x10/imm32
     # . . push slice->end - _test-input-stream
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -1845,7 +1855,7 @@ test-next-word:
     5d/pop-to-EBP
     c3/return
 
-test-next-word-returns-whole-comment:
+test-next-word-or-string-returns-whole-comment:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1869,17 +1879,17 @@ test-next-word-returns-whole-comment:
     e8/call  write/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # check-ints-equal(_test-input-stream->read, 5, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-whole-comment/updates-stream-read-correctly"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-comment/updates-stream-read-correctly"/imm32
     68/push  5/imm32
     b8/copy-to-EAX  _test-input-stream/imm32
     ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
@@ -1890,7 +1900,7 @@ test-next-word-returns-whole-comment:
     # check-ints-equal(slice->start - _test-input-stream->data, 2, msg)
     # . check-ints-equal(slice->start - _test-input-stream, 14, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-whole-comment: start"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-comment: start"/imm32
     68/push  0xe/imm32
     # . . push slice->start - _test-input-stream
     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
@@ -1903,7 +1913,7 @@ test-next-word-returns-whole-comment:
     # check-ints-equal(slice->end - _test-input-stream->data, 5, msg)
     # . check-ints-equal(slice->end - _test-input-stream, 17, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-whole-comment: end"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-comment: end"/imm32
     68/push  0x11/imm32
     # . . push slice->end - _test-input-stream
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -1918,7 +1928,7 @@ test-next-word-returns-whole-comment:
     5d/pop-to-EBP
     c3/return
 
-test-next-word-returns-empty-string-on-eof:
+test-next-word-or-string-returns-empty-string-on-eof:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1935,17 +1945,17 @@ test-next-word-returns-empty-string-on-eof:
     68/push  0/imm32/start
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # write nothing to _test-input-stream
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # check-ints-equal(slice->end - slice->start, 0, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-empty-string-on-eof"/imm32
+    68/push  "F - test-next-word-or-string-returns-empty-string-on-eof"/imm32
     68/push  0/imm32
     # . . push slice->end - slice->start
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -1960,7 +1970,7 @@ test-next-word-returns-empty-string-on-eof:
     5d/pop-to-EBP
     c3/return
 
-test-next-word-returns-whole-string:
+test-next-word-or-string-returns-whole-string:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -1984,18 +1994,18 @@ test-next-word-returns-whole-string:
     e8/call  write/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
     # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-whole-string: start"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-string: start"/imm32
     68/push  0xd/imm32
     # . . push slice->start - _test-input-stream
     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
@@ -2008,7 +2018,7 @@ test-next-word-returns-whole-string:
     # check-ints-equal(slice->end - _test-input-stream->data, 12, msg)
     # . check-ints-equal(slice->end - _test-input-stream, 24, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-whole-string: end"/imm32
+    68/push  "F - test-next-word-or-string-returns-whole-string: end"/imm32
     68/push  0x18/imm32
     # . . push slice->end - _test-input-stream
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -2023,7 +2033,7 @@ test-next-word-returns-whole-string:
     5d/pop-to-EBP
     c3/return
 
-test-next-word-returns-string-with-escapes:
+test-next-word-or-string-returns-string-with-escapes:
     # . prolog
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -2047,18 +2057,18 @@ test-next-word-returns-string-with-escapes:
     e8/call  write/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-input-stream, slice)
+    # next-word-or-string(_test-input-stream, slice)
     # . . push args
     51/push-ECX
     68/push  _test-input-stream/imm32
     # . . call
-    e8/call  next-word/disp32
+    e8/call  next-word-or-string/disp32
     # . . discard args
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
     # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-string-with-escapes: start"/imm32
+    68/push  "F - test-next-word-or-string-returns-string-with-escapes: start"/imm32
     68/push  0xd/imm32
     # . . push slice->start - _test-input-stream
     8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
@@ -2071,7 +2081,7 @@ test-next-word-returns-string-with-escapes:
     # check-ints-equal(slice->end - _test-input-stream->data, 9, msg)
     # . check-ints-equal(slice->end - _test-input-stream, 21, msg)
     # . . push args
-    68/push  "F - test-next-word-returns-string-with-escapes: end"/imm32
+    68/push  "F - test-next-word-or-string-returns-string-with-escapes: end"/imm32
     68/push  0x15/imm32
     # . . push slice->end - _test-input-stream
     8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
@@ -2634,12 +2644,6 @@ Segment-size:
 Next-string-literal:  # tracks the next auto-generated variable name
   1/imm32
 
-Heap:
-  # curr
-  0/imm32
-  # limit
-  0/imm32
-
 # length-prefixed string containing just a single space
 Space:
     # size
@@ -2656,30 +2660,16 @@ Slash:
 
 _test-slice-abc:
   22/dquote 61/a 62/b 63/c 22/dquote  # "abc"
-_test-slice-abc-end:
   2f/slash 64/d
-_test-slice-abc-metadata-end:
-
-_test-slice-empty-string-literal:
-  22/dquote 22/dquote  # ""
-_test-slice-empty-string-literal-end:
+_test-slice-abc-limit:
 
 _test-slice-a-space-b:
   22/dquote 61/a 20/space 62/b 22/dquote  # "a b"
-_test-slice-a-space-b-end:
+_test-slice-a-space-b-limit:
 
 _test-slice-a-dquote-b:
   22/dquote 61/a 5c/backslash 22/dquote 62/b 22/dquote  # "a\"b"
-_test-slice-a-dquote-b-end:
-
-# abc/def/ghi
-_test-slice-word:
-  61/a 62/b 63/c  # abc
-_test-slice-word-datum-end:
-  2f/slash 64/d 65/e 66/f  # /def
-_test-slice-word-end:
-  2f/slash 67/g 68/h 69/i  # /ghi
-_test-slice-word-end2:
+_test-slice-a-dquote-b-limit:
 
 # "abc/def"/ghi
 _test-slice-literal-string:
@@ -2687,8 +2677,7 @@ _test-slice-literal-string:
   61/a 62/b 63/c  # abc
   2f/slash 64/d 65/e 66/f  # /def
   22/dquote
-_test-slice-literal-string-end:
   2f/slash 67/g 68/h 69/i  # /ghi
-_test-slice-literal-string-with-metadata-end:
+_test-slice-literal-string-with-limit:
 
 # . . vim:nowrap:textwidth=0
diff --git a/subx/apps/factorial b/subx/apps/factorial
index 3c3942a3..55a59123 100755
--- a/subx/apps/factorial
+++ b/subx/apps/factorial
Binary files differdiff --git a/subx/apps/factorial.subx b/subx/apps/factorial.subx
index 71780eb4..e4b7a057 100644
--- a/subx/apps/factorial.subx
+++ b/subx/apps/factorial.subx
@@ -19,10 +19,15 @@
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
 Entry:  # run tests if necessary, compute `factorial(5)` if not
-
-#?     # for debugging: run a single test; don't bother setting status code
-#?     e8/call test-get-num-reads-single-digit/disp32
-#?     eb/jump  $main:end/disp8
+    # initialize heap
+    # . Heap = new-segment(64KB)
+    # . . push args
+    68/push  Heap/imm32
+    68/push  0x10000/imm32/64KB
+    # . . call
+    e8/call  new-segment/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 
     # . prolog
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
diff --git a/subx/apps/handle b/subx/apps/handle
index 3d355737..3ea729c6 100755
--- a/subx/apps/handle
+++ b/subx/apps/handle
Binary files differdiff --git a/subx/apps/hex b/subx/apps/hex
index 8a2f240a..315e855c 100755
--- a/subx/apps/hex
+++ b/subx/apps/hex
Binary files differdiff --git a/subx/apps/hex.subx b/subx/apps/hex.subx
index 3730351f..a24c5a2e 100644
--- a/subx/apps/hex.subx
+++ b/subx/apps/hex.subx
@@ -18,11 +18,15 @@
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
 Entry:  # run tests if necessary, convert stdin if not
-
-#?     # for debugging: run a single test
-#?     e8/call test-convert-next-octet-aborts-on-single-hex-byte/disp32
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     eb/jump  $main:end/disp8
+    # initialize heap
+    # . Heap = new-segment(64KB)
+    # . . push args
+    68/push  Heap/imm32
+    68/push  0x10000/imm32/64KB
+    # . . call
+    e8/call  new-segment/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 
     # . prolog
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
diff --git a/subx/apps/pack b/subx/apps/pack
index fed8a27f..9c3ed916 100755
--- a/subx/apps/pack
+++ b/subx/apps/pack
Binary files differdiff --git a/subx/apps/pack.subx b/subx/apps/pack.subx
index c035c361..8cd24813 100644
--- a/subx/apps/pack.subx
+++ b/subx/apps/pack.subx
@@ -19,11 +19,15 @@
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
 Entry:  # run tests if necessary, convert stdin if not
-
-    # for debugging: run a single test
-#?     e8/call test-emit-non-number-with-all-hex-digits-and-metadata/disp32
-#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
-#?     eb/jump  $main:end/disp8
+    # initialize heap
+    # . Heap = new-segment(64KB)
+    # . . push args
+    68/push  Heap/imm32
+    68/push  0x10000/imm32/64KB
+    # . . call
+    e8/call  new-segment/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 
     # . prolog
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
@@ -129,7 +133,7 @@ convert:  # in : (address buffered-file), out : (address buffered-file) -> <void
     89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
     # var word-slice/EDX = {0, 0}
     68/push  0/imm32/end
-    68/push  0/imm32/curr
+    68/push  0/imm32/start
     89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
     # var in-code?/EBX = false
     31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
@@ -242,10 +246,7 @@ $convert:check2:
 #?     # . . discard args
 #?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 #?     # }}}
-    # if (slice-equal?(word-slice, "=="))
-    #   word-slice = next-word(line)
-    #   in-code? = slice-equal?(word-slice, "code")
-    #   write-stream-data(out, line)
+    # if (!slice-equal?(word-slice, "==")) goto next check
     # . EAX = slice-equal?(word-slice, "==")
     # . . push args
     68/push  "=="/imm32
@@ -257,7 +258,7 @@ $convert:check2:
     # . if (EAX == 0) goto check3
     3d/compare-EAX-and  0/imm32
     0f 84/jump-if-equal  $convert:check3/disp32
-    # . next-word(line, word-slice)
+    # word-slice = next-word(line)
     # . . push args
     52/push-EDX
     51/push-ECX
@@ -307,7 +308,7 @@ $convert:check2:
 #?     # . . discard args
 #?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
 #?     # }}}
-    # . in-code? = slice-equal?(word-slice, "code")
+    # in-code? = slice-equal?(word-slice, "code")
     # . . push args
     68/push  "code"/imm32
     52/push-EDX
@@ -317,7 +318,7 @@ $convert:check2:
     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
     # . . in-code? = EAX
     89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EBX
-    # . goto pass-through
+    # write-stream-data(out, line)
     eb/jump  $convert:pass-through/disp8
 $convert:check3:
     # else rewind-stream(line)
@@ -5831,1449 +5832,6 @@ test-convert-instruction-handles-imm8-operand:
     5d/pop-to-EBP
     c3/return
 
-# (re)compute the bounds of the next word in the line
-# return empty string on reaching end of file
-next-word:  # line : (address stream byte), out : (address slice)
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # . save registers
-    50/push-EAX
-    51/push-ECX
-    56/push-ESI
-    57/push-EDI
-    # ESI = line
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
-    # EDI = out
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   0xc/disp8       .                 # copy *(EBP+12) to EDI
-    # skip-chars-matching(line, ' ')
-    # . . push args
-    68/push  0x20/imm32/space
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
-    # . . call
-    e8/call  skip-chars-matching/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-$next-word:check0:
-    # if (line->read >= line->write) clear out and return
-    # . EAX = line->read
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
-    # . if (EAX < line->write) goto next check
-    3b/compare                      0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # compare EAX with *ESI
-    7c/jump-if-lesser  $next-word:check-for-comment/disp8
-    # . return out = {0, 0}
-    c7          0/subop/copy        0/mod/direct    7/rm32/EDI    .           .             .           .           .               0/imm32           # copy to *EDI
-    c7          0/subop/copy        1/mod/*+disp8   7/rm32/EDI    .           .             .           .           4/disp8         0/imm32           # copy to *(EDI+4)
-    eb/jump  $next-word:end/disp8
-$next-word:check-for-comment:
-    # out->start = &line->data[line->read]
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
-    89/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           0/r32/EAX   .               .                 # copy EAX to *EDI
-    # if (line->data[line->read] == '#') out->end = &line->data[line->write]), skip rest of stream and return
-    # . EAX = line->data[line->read]
-    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
-    8a/copy-byte                    1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/AL    0xc/disp8       .                 # copy byte at *(ESI+ECX+12) to AL
-    # . compare
-    3d/compare-EAX-and  0x23/imm32/pound
-    75/jump-if-not-equal  $next-word:regular-word/disp8
-$next-word:comment:
-    # . out->end = &line->data[line->write]
-    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  0/index/EAX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+EAX+12 to EAX
-    89/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDI+4)
-    # . line->read = line->write
-    89/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(ESI+4)
-    # . return
-    eb/jump  $next-word:end/disp8
-$next-word:regular-word:
-    # otherwise skip-chars-not-matching-whitespace(line)  # including trailing newline
-    # . . push args
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
-    # . . call
-    e8/call  skip-chars-not-matching-whitespace/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # out->end = &line->data[line->read]
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
-    89/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDI+4)
-$next-word:end:
-    # . restore registers
-    5f/pop-to-EDI
-    5e/pop-to-ESI
-    59/pop-to-ECX
-    58/pop-to-EAX
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-next-word:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-stream)
-    # . . push args
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = {0, 0}
-    68/push  0/imm32/end
-    68/push  0/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # write(_test-stream, "  ab")
-    # . . push args
-    68/push  "  ab"/imm32
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  write/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-stream, slice)
-    # . . push args
-    51/push-ECX
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  next-word/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(slice->start - _test-stream->data, 2, msg)
-    # . check-ints-equal(slice->start - _test-stream, 14, msg)
-    # . . push args
-    68/push  "F - test-next-word: start"/imm32
-    68/push  0xe/imm32
-    # . . push slice->start - _test-stream
-    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
-    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # check-ints-equal(slice->end - _test-stream->data, 4, msg)
-    # . check-ints-equal(slice->end - _test-stream, 16, msg)
-    # . . push args
-    68/push  "F - test-next-word: end"/imm32
-    68/push  0x10/imm32
-    # . . push slice->end - _test-stream
-    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
-    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-next-word-returns-whole-comment:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-stream)
-    # . . push args
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = {0, 0}
-    68/push  0/imm32/end
-    68/push  0/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # write(_test-stream, "  # a")
-    # . . push args
-    68/push  "  # a"/imm32
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  write/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # next-word(_test-stream, slice)
-    # . . push args
-    51/push-ECX
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  next-word/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(slice->start - _test-stream->data, 2, msg)
-    # . check-ints-equal(slice->start - _test-stream, 14, msg)
-    # . . push args
-    68/push  "F - test-next-word-returns-whole-comment: start"/imm32
-    68/push  0xe/imm32
-    # . . push slice->start - _test-stream
-    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
-    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # check-ints-equal(slice->end - _test-stream->data, 5, msg)
-    # . check-ints-equal(slice->end - _test-stream, 17, msg)
-    # . . push args
-    68/push  "F - test-next-word-returns-whole-comment: end"/imm32
-    68/push  0x11/imm32
-    # . . push slice->end - _test-stream
-    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
-    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-next-word-returns-empty-string-on-eof:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-stream)
-    # . . push args
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = {0, 0}
-    68/push  0/imm32/end
-    68/push  0/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # write nothing to _test-stream
-    # next-word(_test-stream, slice)
-    # . . push args
-    51/push-ECX
-    68/push  _test-stream/imm32
-    # . . call
-    e8/call  next-word/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(slice->end - slice->start, 0, msg)
-    # . . push args
-    68/push  "F - test-next-word-returns-empty-string-on-eof"/imm32
-    68/push  0/imm32
-    # . . push slice->end - slice->start
-    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
-    2b/subtract                     0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # subtract *ECX from EAX
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-has-metadata?:  # word : (address slice), s : (address string) -> EAX : boolean
-    # pseudocode:
-    #   var twig : &slice = next-token-from-slice(word->start, word->end, '/')  # skip name
-    #   curr = twig->end
-    #   while true
-    #     twig = next-token-from-slice(curr, word->end, '/')
-    #     if (twig.empty()) break
-    #     if (slice-equal?(twig, s)) return true
-    #     curr = twig->end
-    #   return false
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # . save registers
-    51/push-ECX
-    52/push-EDX
-    56/push-ESI
-    57/push-EDI
-    # ESI = word
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
-    # EDX = word->end
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           2/r32/EDX   4/disp8         .                 # copy *(ESI+4) to EDX
-    # var twig/EDI : (address slice) = {0, 0}
-    68/push  0/imm32/end
-    68/push  0/imm32/start
-    89/copy                         3/mod/direct    7/rm32/EDI    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDI
-    # next-token-from-slice(word->start, word->end, '/', twig)
-    # . . push args
-    57/push-EDI
-    68/push  0x2f/imm32/slash
-    52/push-EDX
-    ff          6/subop/push        0/mod/indirect  6/rm32/ESI    .           .             .           .           .               .                 # push *ESI
-    # . . call
-    e8/call  next-token-from-slice/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
-    # curr/ECX = twig->end
-    8b/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(EDI+4) to ECX
-$has-metadata?:loop:
-    # next-token-from-slice(curr, word->end, '/', twig)
-    # . . push args
-    57/push-EDI
-    68/push  0x2f/imm32/slash
-    52/push-EDX
-    51/push-ECX
-    # . . call
-    e8/call  next-token-from-slice/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
-    # if (slice-empty?(twig)) return false
-    # . EAX = slice-empty?(twig)
-    # . . push args
-    57/push-EDI
-    # . . call
-    e8/call  slice-empty?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . if (EAX != 0) return false
-    3d/compare-EAX-and  0/imm32
-    75/jump-if-not-equal  $has-metadata?:false/disp8
-    # if (slice-equal?(twig, s)) return true
-    # . EAX = slice-equal?(twig, s)
-    # . . push args
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
-    57/push-EDI
-    # . . call
-    e8/call  slice-equal?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . if (EAX != 0) return true
-    3d/compare-EAX-and  0/imm32
-    75/jump-if-not-equal  $has-metadata?:true/disp8
-    # curr = twig->end
-    8b/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(EDI+4) to ECX
-    eb/jump  $has-metadata?:loop/disp8
-$has-metadata?:true:
-    b8/copy-to-EAX  1/imm32/true
-    eb/jump  $has-metadata?:end/disp8
-$has-metadata?:false:
-    b8/copy-to-EAX  0/imm32/false
-$has-metadata?:end:
-    # . reclaim locals
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . restore registers
-    5f/pop-to-EDI
-    5e/pop-to-ESI
-    5a/pop-to-EDX
-    59/pop-to-ECX
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-has-metadata-true:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # (EAX..ECX) = "ab/c"
-    b8/copy-to-EAX  "ab/c"/imm32
-    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
-    05/add-to-EAX  4/imm32
-    # var in/ESI : (address slice) = {EAX, ECX}
-    51/push-ECX
-    50/push-EAX
-    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
-    # EAX = has-metadata?(ESI, "c")
-    # . . push args
-    68/push  "c"/imm32
-    56/push-ESI
-    # . . call
-    e8/call  has-metadata?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(EAX, 1, msg)
-    # . . push args
-    68/push  "F - test-has-metadata-true"/imm32
-    68/push  1/imm32/true
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-has-metadata-false:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # (EAX..ECX) = "ab/c"
-    b8/copy-to-EAX  "ab/c"/imm32
-    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
-    05/add-to-EAX  4/imm32
-    # var in/ESI : (address slice) = {EAX, ECX}
-    51/push-ECX
-    50/push-EAX
-    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
-    # EAX = has-metadata?(ESI, "c")
-    # . . push args
-    68/push  "d"/imm32
-    56/push-ESI
-    # . . call
-    e8/call  has-metadata?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(EAX, 0, msg)
-    # . . push args
-    68/push  "F - test-has-metadata-false"/imm32
-    68/push  0/imm32/false
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-has-metadata-ignore-name:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # (EAX..ECX) = "a/b"
-    b8/copy-to-EAX  "a/b"/imm32
-    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
-    05/add-to-EAX  4/imm32
-    # var in/ESI : (address slice) = {EAX, ECX}
-    51/push-ECX
-    50/push-EAX
-    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
-    # EAX = has-metadata?(ESI, "a")
-    # . . push args
-    68/push  "a"/imm32
-    56/push-ESI
-    # . . call
-    e8/call  has-metadata?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(EAX, 0, msg)
-    # . . push args
-    68/push  "F - test-has-metadata-ignore-name"/imm32
-    68/push  0/imm32/false
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-has-metadata-multiple-true:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # (EAX..ECX) = "a/b/c"
-    b8/copy-to-EAX  "a/b/c"/imm32
-    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
-    05/add-to-EAX  4/imm32
-    # var in/ESI : (address slice) = {EAX, ECX}
-    51/push-ECX
-    50/push-EAX
-    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
-    # EAX = has-metadata?(ESI, "c")
-    # . . push args
-    68/push  "c"/imm32
-    56/push-ESI
-    # . . call
-    e8/call  has-metadata?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(EAX, 1, msg)
-    # . . push args
-    68/push  "F - test-has-metadata-multiple-true"/imm32
-    68/push  1/imm32/true
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-has-metadata-multiple-false:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # (EAX..ECX) = "a/b/c"
-    b8/copy-to-EAX  "a/b/c"/imm32
-    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
-    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
-    05/add-to-EAX  4/imm32
-    # var in/ESI : (address slice) = {EAX, ECX}
-    51/push-ECX
-    50/push-EAX
-    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
-    # EAX = has-metadata?(ESI, "d")
-    # . . push args
-    68/push  "d"/imm32
-    56/push-ESI
-    # . . call
-    e8/call  has-metadata?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # check-ints-equal(EAX, 0, msg)
-    # . . push args
-    68/push  "F - test-has-metadata-multiple-false"/imm32
-    68/push  0/imm32/false
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-# If datum of 'word' is not a valid name, it must be a hex int. Parse and print
-# it in 'width' bytes of hex, least significant first.
-# Otherwise just print the entire word including metadata.
-# Always print a trailing space.
-emit:  # out : (address buffered-file), word : (address slice), width : int -> <void>
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # . save registers
-    50/push-EAX
-    56/push-ESI
-    57/push-EDI
-    # ESI = word
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
-    # var name/EDI : (address slice) = {0, 0}
-    68/push  0/imm32/end
-    68/push  0/imm32/start
-    89/copy                         3/mod/direct    7/rm32/EDI    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDI
-    # datum = next-token-from-slice(word->start, word->end, '/')
-    # . . push args
-    57/push-EDI
-    68/push  0x2f/imm32/slash
-    ff          6/subop/push        1/mod/*+disp8   6/rm32/ESI    .           .             .           .           4/disp8         .                 # push *(ESI+4)
-    ff          6/subop/push        0/mod/indirect  6/rm32/ESI    .           .             .           .           .               .                 # push *ESI
-    # . . call
-    e8/call  next-token-from-slice/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
-    # if (is-valid-name?(datum)) write-slice-buffered(out, word) and return
-    # . EAX = is-valid-name?(name)
-    # . . push args
-    57/push-EDI
-    # . . call
-    e8/call  is-valid-name?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . if (EAX != 0)
-    3d/compare-EAX-and  0/imm32
-    74/jump-if-equal  $emit:hex-int/disp8
-$emit:name:
-    # . write-slice-buffered(out, word)
-    # . . push args
-    56/push-ESI
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
-    # . . call
-    e8/call  write-slice-buffered/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . write-buffered(out, " ")
-    # . . push args
-    68/push  " "/imm32
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
-    # . . call
-    e8/call  write-buffered/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . return
-    eb/jump  $emit:end/disp8
-    # otherwise emit-hex(out, parse-hex-int(datum), width)
-    #   (Weird shit can happen here if the datum of 'word' isn't either a valid
-    #   name or a hex number, but we're only going to be passing in real legal
-    #   programs. We just want to make sure that valid names aren't treated as
-    #   (valid) hex numbers.)
-$emit:hex-int:
-    # . value/EAX = parse-hex-int(datum)
-    # . . push args
-    57/push-EDI
-    # . . call
-    e8/call  parse-hex-int/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . emit-hex(out, value, width)
-    # . . push args
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
-    50/push-EAX
-    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
-    # . . call
-    e8/call  emit-hex/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-$emit:end:
-    # . reclaim locals
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # . restore registers
-    5f/pop-to-EDI
-    5e/pop-to-ESI
-    58/pop-to-EAX
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-emit-number:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "30"
-    68/push  _test-slice-three-zero-end/imm32/end
-    68/push  _test-slice-three-zero/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # emit(_test-output-buffered-file, slice, 1)
-    # . . push args
-    68/push  1/imm32
-    51/push-ECX
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-stream-equal(_test-output-stream, "30 ", msg)
-    # . . push args
-    68/push  "F - test-emit-number/1"/imm32
-    68/push  "30 "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-emit-negative-number:
-    # test support for sign-extending negative numbers
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "-2"
-    68/push  _test-slice-negative-two-end/imm32/end
-    68/push  _test-slice-negative-two/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # emit(_test-output-buffered-file, slice, 2)
-    # . . push args
-    68/push  2/imm32
-    51/push-ECX
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-stream-equal(_test-output-stream, "fe ff ", msg)
-    # . . push args
-    68/push  "F - test-emit-number/1"/imm32
-    68/push  "fe ff "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-emit-number-with-metadata:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "-2/foo"
-    68/push  _test-slice-negative-two-metadata-end/imm32/end
-    68/push  _test-slice-negative-two/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # emit(_test-output-buffered-file, slice, 2)
-    # . . push args
-    68/push  2/imm32
-    51/push-ECX
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # the '/foo' will have no impact on the output
-    # check-stream-equal(_test-output-stream, "fe ff ", msg)
-    # . . push args
-    68/push  "F - test-emit-number-with-metadata"/imm32
-    68/push  "fe ff "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-emit-non-number:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "xyz"
-    68/push  _test-slice-non-number-word-end/imm32/end
-    68/push  _test-slice-non-number-word/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # emit(_test-output-buffered-file, slice, 2)
-    # . . push args
-    68/push  2/imm32
-    51/push-ECX
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-stream-equal(_test-output-stream, "xyz", msg)
-    # . . push args
-    68/push  "F - test-emit-non-number"/imm32
-    68/push  "xyz "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-emit-non-number-with-metadata:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "xyz/"
-    68/push  _test-slice-non-number-word-metadata-end/imm32/end
-    68/push  _test-slice-non-number-word/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # emit(_test-output-buffered-file, slice, 2)
-    # . . push args
-    68/push  2/imm32
-    51/push-ECX
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-stream-equal(_test-output-stream, "xyz/", msg)
-    # . . push args
-    68/push  "F - test-emit-non-number-with-metadata"/imm32
-    68/push  "xyz/ "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-emit-non-number-with-all-hex-digits-and-metadata:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # var slice/ECX = "abcd/xyz"
-    68/push  _test-slice-hexlike-non-number-word-metadata-end/imm32/end
-    68/push  _test-slice-hexlike-non-number-word/imm32/start
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # emit(_test-output-buffered-file, slice, 2)
-    # . . push args
-    68/push  2/imm32
-    51/push-ECX
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-#?     # dump output {{{
-#?     # . write(2/stderr, "^")
-#?     # . . push args
-#?     68/push  "^"/imm32
-#?     68/push  2/imm32/stderr
-#?     # . . call
-#?     e8/call  write/disp32
-#?     # . . discard args
-#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-#?     # . write-stream(2/stderr, _test-output-stream)
-#?     # . . push args
-#?     68/push  _test-output-stream/imm32
-#?     68/push  2/imm32/stderr
-#?     # . . call
-#?     e8/call  write-stream/disp32
-#?     # . . discard args
-#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-#?     # . write(2/stderr, "$\n")
-#?     # . . push args
-#?     68/push  "$\n"/imm32
-#?     68/push  2/imm32/stderr
-#?     # . . call
-#?     e8/call  write/disp32
-#?     # . . discard args
-#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-#?     # }}}
-    # check-stream-equal(_test-output-stream, "abcd/xyz")
-    # . . push args
-    68/push  "F - test-emit-non-number-with-all-hex-digits"/imm32
-    68/push  "abcd/xyz "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-# conditions for 'valid' names that are not at risk of looking like hex numbers
-# keep in sync with the rules in labels.cc
-#: - if it starts with a digit, it's treated as a number. If it can't be
-#:   parsed as hex it will raise an error.
-#: - if it starts with '-' it's treated as a number.
-#: - if it starts with '0x' it's treated as a number. (redundant)
-#: - if it's two characters long, it can't be a name. Either it's a hex
-#:   byte, or it raises an error.
-is-valid-name?:  # in : (address slice) -> EAX : boolean
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # . save registers
-    51/push-ECX
-    56/push-ESI
-    # ESI = in
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
-    # start/ECX = in->start
-    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
-    # end/EAX = in->end
-    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
-$is-valid-name?:check0:
-    # if (start >= end) return false
-    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # compare ECX with EAX
-    7d/jump-if-greater-or-equal  $is-valid-name?:false/disp8
-$is-valid-name?:check1:
-    # EAX -= ECX
-    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
-    # if (EAX == 2) return false
-    3d/compare-EAX-and  2/imm32
-    74/jump-if-equal  $is-valid-name?:false/disp8
-$is-valid-name?:check2:
-    # c/EAX = *ECX
-    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
-    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
-    # if (c == "-") return false
-    3d/compare-EAX-and  2d/imm32/-
-    74/jump-if-equal  $is-valid-name?:false/disp8
-$is-valid-name?:check3a:
-    # if (c < "0") return true
-    3d/compare-EAX-with  30/imm32/0
-    7c/jump-if-lesser  $is-valid-name?:true/disp8
-$is-valid-name?:check3b:
-    # if (c > "9") return true
-    3d/compare-EAX-with  39/imm32/9
-    7f/jump-if-greater  $is-valid-name?:true/disp8
-$is-valid-name?:false:
-    # return false
-    b8/copy-to-EAX  0/imm32/false
-    eb/jump  $is-valid-name?:end/disp8
-$is-valid-name?:true:
-    # return true
-    b8/copy-to-EAX  1/imm32/true
-$is-valid-name?:end:
-    # . restore registers
-    5e/pop-to-ESI
-    59/pop-to-ECX
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-is-valid-name-digit-prefix:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "34"
-    68/push  _test-slice-hex-int-end/imm32
-    68/push  _test-slice-hex-int/imm32
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # EAX = is-valid-name?(slice)
-    # . . push args
-    51/push-ECX
-    # . . call
-    e8/call  is-valid-name?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-ints-equal(EAX, 0, msg)
-    # . . push args
-    68/push  "F - test-is-valid-name-digit-prefix"/imm32
-    68/push  0/imm32/false
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-is-valid-name-negative-prefix:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "-0x34"
-    68/push  _test-slice-hex-int-with-0x-prefix-end/imm32
-    68/push  _test-slice-hex-int-with-0x-prefix-negative/imm32
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # EAX = is-valid-name?(slice)
-    # . . push args
-    51/push-ECX
-    # . . call
-    e8/call  is-valid-name?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-ints-equal(EAX, 0, msg)
-    # . . push args
-    68/push  "F - test-is-valid-name-negative-prefix"/imm32
-    68/push  0/imm32/false
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-is-valid-name-0x-prefix:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "0x34"
-    68/push  _test-slice-hex-int-with-0x-prefix-end/imm32
-    68/push  _test-slice-hex-int-with-0x-prefix/imm32
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # EAX = is-valid-name?(slice)
-    # . . push args
-    51/push-ECX
-    # . . call
-    e8/call  is-valid-name?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-ints-equal(EAX, 0, msg)
-    # . . push args
-    68/push  "F - test-is-valid-name-0x-prefix"/imm32
-    68/push  0/imm32/false
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-is-valid-name-starts-with-pre-digit:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "/03"
-    68/push  _test-slice-with-slash-prefix-end/imm32
-    68/push  _test-slice-with-slash-prefix/imm32
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # EAX = is-valid-name?(slice)
-    # . . push args
-    51/push-ECX
-    # . . call
-    e8/call  is-valid-name?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-ints-equal(EAX, 1, msg)
-    # . . push args
-    68/push  "F - test-is-valid-name-starts-with-pre-digit"/imm32
-    68/push  1/imm32/true
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-is-valid-name-starts-with-post-digit:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "q34"
-    68/push  _test-slice-char-and-digits-end/imm32
-    68/push  _test-slice-char-and-digits/imm32
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # EAX = is-valid-name?(slice)
-    # . . push args
-    51/push-ECX
-    # . . call
-    e8/call  is-valid-name?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-ints-equal(EAX, 1, msg)
-    # . . push args
-    68/push  "F - test-is-valid-name-starts-with-post-digit"/imm32
-    68/push  1/imm32/true
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-is-valid-name-starts-with-digit:
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # var slice/ECX = "0x34"
-    68/push  _test-slice-hex-int-with-0x-prefix-end/imm32
-    68/push  _test-slice-hex-int-with-0x-prefix/imm32
-    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
-    # EAX = is-valid-name?(slice)
-    # . . push args
-    51/push-ECX
-    # . . call
-    e8/call  is-valid-name?/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-ints-equal(EAX, 0, msg)
-    # . . push args
-    68/push  "F - test-is-valid-name-starts-with-digit"/imm32
-    68/push  0/imm32/false
-    50/push-EAX
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-# print 'n' in hex in 'width' bytes in lower-endian order, with a space after every byte
-emit-hex:  # out : (address buffered-file), n : int, width : int -> <void>
-    # . prolog
-    55/push-EBP
-    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
-    # . save registers
-    50/push-EAX
-    51/push-ECX
-    52/push-EDX
-    53/push-EBX
-    57/push-EDI
-    # EDI = out
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   8/disp8         .                 # copy *(EBP+8) to EDI
-    # EBX = n
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           3/r32/EBX   0xc/disp8       .                 # copy *(EBP+12) to EBX
-    # EDX = width
-    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           2/r32/EDX   0x10/disp8      .                 # copy *(EBP+16) to EDX
-    # var curr/ECX = 0
-    31/xor                          3/mod/direct    1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # clear ECX
-$emit-hex:loop:
-    # if (curr >= width) break
-    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
-    7d/jump-if-greater-or-equal  $emit-hex:end/disp8
-    # print-byte-buffered(out, EBX)
-    # . . push args
-    53/push-EBX
-    57/push-EDI
-    # . . call
-    e8/call  print-byte-buffered/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # write-byte-buffered(out, ' ')
-    # . . push args
-    68/push  0x20/imm32/space
-    57/push-EDI
-    # . . call
-    e8/call  write-byte-buffered/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
-    # EBX = EBX >> 8
-    c1/shift    5/subop/logic-right 3/mod/direct    3/rm32/EBX    .           .             .           .           .               8/imm8            # shift EBX right by 8 bits, while padding zeroes
-$emit-hex:continue:
-    # ++curr
-    41/increment-ECX
-    eb/jump  $emit-hex:loop/disp8
-$emit-hex:end:
-    # . restore registers
-    5f/pop-to-EDI
-    5b/pop-to-EBX
-    5a/pop-to-EDX
-    59/pop-to-ECX
-    58/pop-to-EAX
-    # . epilog
-    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
-    5d/pop-to-EBP
-    c3/return
-
-test-emit-hex-single-byte:
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # emit-hex(_test-output-buffered-file, 0xab, 1)
-    # . . push args
-    68/push  1/imm32
-    68/push  0xab/imm32
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit-hex/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-ints-equal(*_test-output-stream->data, 'ab ', msg)
-    # . . push args
-    68/push  "F - test-emit-hex-single-byte"/imm32
-    68/push  0x206261/imm32
-    # . . push *_test-output-stream->data
-    b8/copy-to-EAX  _test-output-stream/imm32
-    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           0xc/disp8       .                 # push *(EAX+12)
-    # . . call
-    e8/call  check-ints-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . end
-    c3/return
-
-test-emit-hex-multiple-byte:
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # emit-hex(_test-output-buffered-file, 0x1234, 2)
-    # . . push args
-    68/push  2/imm32
-    68/push  0x1234/imm32
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit-hex/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-stream-equal(_test-output-stream, "34 12 ", msg)
-    # . . push args
-    68/push  "F - test-emit-hex-multiple-byte/1"/imm32
-    68/push  "34 12 "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . end
-    c3/return
-
-test-emit-hex-zero-pad:
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # emit-hex(_test-output-buffered-file, 0xab, 2)
-    # . . push args
-    68/push  2/imm32
-    68/push  0xab/imm32
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit-hex/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check(_test-output-stream->data == 'ab 00 ')
-    # . . push args
-    68/push  "F - test-emit-hex-zero-pad/1"/imm32
-    68/push  "ab 00 "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . end
-    c3/return
-
-test-emit-hex-negative:
-    # setup
-    # . clear-stream(_test-output-stream)
-    # . . push args
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # . clear-stream(_test-output-buffered-file+4)
-    # . . push args
-    b8/copy-to-EAX  _test-output-buffered-file/imm32
-    05/add-to-EAX  4/imm32
-    50/push-EAX
-    # . . call
-    e8/call  clear-stream/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # emit-hex(_test-output-buffered-file, -1, 2)
-    # . . push args
-    68/push  2/imm32
-    68/push  -1/imm32
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  emit-hex/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # flush(_test-output-buffered-file)
-    # . . push args
-    68/push  _test-output-buffered-file/imm32
-    # . . call
-    e8/call  flush/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
-    # check-stream-equal(_test-output-stream == "ff ff ")
-    # . . push args
-    68/push  "F - test-emit-hex-negative/1"/imm32
-    68/push  "ff ff "/imm32
-    68/push  _test-output-stream/imm32
-    # . . call
-    e8/call  check-stream-equal/disp32
-    # . . discard args
-    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
-    # . end
-    c3/return
-
 # shortcut for parse-hex-int(next-token-from-slice(word->start, word->end, '/'))
 parse-datum-of-word:  # word : (address slice) -> value/EAX
     # . prolog
@@ -7316,33 +5874,4 @@ $parse-datum-of-word:end:
     5d/pop-to-EBP
     c3/return
 
-== data
-
-_test-slice-negative-two:
-    2d/- 32/2
-_test-slice-negative-two-end:
-    2f/slash 66/f 6f/o 6f/o
-_test-slice-negative-two-metadata-end:
-
-_test-slice-three-zero:
-    33/3 30/0
-_test-slice-three-zero-end:
-
-_test-slice-non-number-word:
-    78/x 79/y 7a/z
-_test-slice-non-number-word-end:
-    2f/slash
-_test-slice-non-number-word-metadata-end:
-
-_test-slice-hexlike-non-number-word:
-    61/a 62/b 63/c 64/d
-_test-slice-hexlike-non-number-word-end:
-    2f/slash
-    78/x 79/y 7a/z
-_test-slice-hexlike-non-number-word-metadata-end:
-
-_test-slice-with-slash-prefix:
-  2f/slash 30/0 33/3
-_test-slice-with-slash-prefix-end:
-
 # . . vim:nowrap:textwidth=0
diff --git a/subx/apps/subx-common.subx b/subx/apps/subx-common.subx
index cba1e9cc..e3af9381 100644
--- a/subx/apps/subx-common.subx
+++ b/subx/apps/subx-common.subx
@@ -5,6 +5,1157 @@
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
+# - managing tables
+# SubX has rudimentary support for tables.
+#
+# Each table is a stream of rows.
+#
+# Each row consists of a 4-byte row (address to a string) and a variable-size
+# value.
+#
+# Accessing the table performs a linear scan for a key string, and always
+# requires passing in the row size.
+#
+# Table primitives:
+#   get(stream, string, row-size)
+#     aborts if not found
+#   get-or-insert(stream, string, row-size)
+#     inserts if not found
+#   get-slice(stream, slice, row-size)
+#     aborts if not found
+#   leaky-get-or-insert-slice(stream, slice, row-size)
+#     inserts if not found
+
+# 'table' is a stream of (key, value) rows
+# keys are always strings (addresses; size 4 bytes)
+# values may be any type, but rows (key+value) always occupy 'row-size' bytes
+# scan 'table' for a row with a key 'key' and return the address of the corresponding value
+# if no row is found, abort
+get:  # table : (address stream {string, _}), key : (address string), row-size : int -> EAX : (address _)
+    # pseudocode:
+    #   curr = table->data
+    #   max = &table->data[table->write]
+    #   while curr < max
+    #     if string-equal?(key, *curr)
+    #       return curr+4
+    #     curr += row-size
+    #   abort
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    56/push-ESI
+    # ESI = table
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # curr/ECX = table->data
+    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   0xc/disp8       .                 # copy ESI+12 to ECX
+    # max/EDX = table->data + table->write
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ECX  2/index/EDX   .           2/r32/EDX   .               .                 # copy ECX+EDX to EDX
+$get:search-loop:
+    # if (curr >= max) abort
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $get:abort/disp8
+    # if (string-equal?(key, *curr)) return curr+4
+    # . EAX = string-equal?(key, *curr)
+    # . . push args
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  string-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return EAX = curr+4
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $get:mismatch/disp8
+    8d/copy-address                 1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy ECX+4 to EAX
+    eb/jump  $get:end/disp8
+$get:mismatch:
+    # curr += row-size
+    03/add                          1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   0x10/disp8      .                 # add *(EBP+16) to ECX
+    # loop
+    eb/jump  $get:search-loop/disp8
+$get:end:
+    # . restore registers
+    5e/pop-to-ESI
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+$get:abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "get: key not found: "/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . _write(2/stderr, key)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+test-get:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # - setup: create a table with a couple of keys
+    # var table/ECX : (address stream {string, number}) = stream(2 rows * 8 bytes)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # subtract from ESP
+    68/push  0x10/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # insert(table, "code", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "code"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # insert(table, "data", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "data"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get:check1:
+    # EAX = get(table, "code", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "code"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(EAX - table->data, 4, msg)
+    # . check-ints-equal(EAX - table, 16, msg)
+    # . . push args
+    68/push  "F - test-get/0"/imm32
+    68/push  0x10/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get:check2:
+    # EAX = get(table, "data", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "data"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(EAX - table->data, 12, msg)
+    # . check-ints-equal(EAX - table, 24, msg)
+    # . . push args
+    68/push  "F - test-get/1"/imm32
+    68/push  0x18/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# 'table' is a stream of (key, value) rows
+# keys are always strings (addresses; size 4 bytes)
+# values may be any type, but rows (key+value) always occupy 'row-size' bytes
+# scan 'table' for a row with a key 'key' and return the address of the corresponding value
+# if no row is found, abort
+get-slice:  # table : (address stream {string, _}), key : (address slice), row-size : int -> EAX : (address _)
+    # pseudocode:
+    #   curr = table->data
+    #   max = &table->data[table->write]
+    #   while curr < max
+    #     if slice-equal?(key, *curr)
+    #       return curr+4
+    #     curr += row-size
+    #   abort
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    56/push-ESI
+    # ESI = table
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # curr/ECX = table->data
+    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   0xc/disp8       .                 # copy ESI+12 to ECX
+    # max/EDX = table->data + table->write
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ECX  2/index/EDX   .           2/r32/EDX   .               .                 # copy ECX+EDX to EDX
+$get-slice:search-loop:
+    # if (curr >= max) abort
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $get-slice:abort/disp8
+    # if (slice-equal?(key, *curr)) return curr+4
+    # . EAX = slice-equal?(key, *curr)
+    # . . push args
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  slice-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return EAX = curr+4
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $get-slice:mismatch/disp8
+    8d/copy-address                 1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy ECX+4 to EAX
+    eb/jump  $get-slice:end/disp8
+$get-slice:mismatch:
+    # curr += row-size
+    03/add                          1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   0x10/disp8      .                 # add *(EBP+16) to ECX
+    # loop
+    eb/jump  $get-slice:search-loop/disp8
+$get-slice:end:
+    # . restore registers
+    5e/pop-to-ESI
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+$get-slice:abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "get-slice: key not found: "/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write-slice-buffered(Stderr, key)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    68/push  Stderr/imm32
+    # . . call
+    e8/call  write-slice-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . flush(Stderr)
+    # . . push args
+    68/push  Stderr/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+test-get-slice:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # - setup: create a table with a couple of keys
+    # var table/ECX : (address stream {string, number}) = stream(2 rows * 8 bytes)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # subtract from ESP
+    68/push  0x10/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # insert(table, "code", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "code"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # insert(table, "data", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "data"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get-slice:check1:
+    # (EAX..EDX) = "code"
+    b8/copy-to-EAX  "code"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # copy *EAX to EDX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  2/index/EDX   .           2/r32/EDX   4/disp8         .                 # copy EAX+EDX+4 to EDX
+    05/add-to-EAX  4/imm32
+    # var slice/EDX = {EAX, EDX}
+    52/push-EDX
+    50/push-EAX
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # EAX = get-slice(table, "code", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  get-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(EAX - table->data, 4, msg)  # first row's value slot returned
+    # . check-ints-equal(EAX - table, 16, msg)
+    # . . push args
+    68/push  "F - test-get-slice/0"/imm32
+    68/push  0x10/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get-slice:check2:
+    # (EAX..EDX) = "data"
+    b8/copy-to-EAX  "data"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # copy *EAX to EDX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  2/index/EDX   .           2/r32/EDX   4/disp8         .                 # copy EAX+EDX+4 to EDX
+    05/add-to-EAX  4/imm32
+    # var slice/EDX = {EAX, EDX}
+    52/push-EDX
+    50/push-EAX
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # EAX = get-slice(table, "data" slice, 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  get-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(EAX - table->data, 12, msg)
+    # . check-ints-equal(EAX - table, 24, msg)
+    # . . push args
+    68/push  "F - test-get-slice/1"/imm32
+    68/push  0x18/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get-slice:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# 'table' is a stream of (key, value) rows
+# keys are always strings (addresses; size 4 bytes)
+# values may be any type, but rows (key+value) always occupy 'row-size' bytes
+# scan 'table' for a row with a key 'key' and return the address of the corresponding value
+# if no row is found, save 'key' to the next available row
+# if there are no rows free, abort
+# return the address of the value
+# Beware: assume keys are immutable; they're inserted by reference
+# TODO: pass in an allocation descriptor
+get-or-insert:  # table : (address stream {string, _}), key : (address string), row-size : int -> EAX : (address _)
+    # pseudocode:
+    #   curr = table->data
+    #   max = &table->data[table->write]
+    #   while curr < max
+    #     if string-equal?(key, *curr)
+    #       return curr+4
+    #     curr += row-size
+    #   if table->write >= table->length
+    #     abort
+    #   zero-out(max, row-size)
+    #   *max = key
+    #   table->write += row-size
+    #   return max+4
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    56/push-ESI
+    # ESI = table
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # curr/ECX = table->data
+    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   0xc/disp8       .                 # copy ESI+12 to ECX
+    # max/EDX = table->data + table->write
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ECX  2/index/EDX   .           2/r32/EDX   .               .                 # copy ECX+EDX to EDX
+$get-or-insert:search-loop:
+    # if (curr >= max) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $get-or-insert:not-found/disp8
+    # if (string-equal?(key, *curr)) return curr+4
+    # . EAX = string-equal?(key, *curr)
+    # . . push args
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  string-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return EAX = curr+4
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $get-or-insert:mismatch/disp8
+    8d/copy-address                 1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy ECX+4 to EAX
+    eb/jump  $get-or-insert:end/disp8
+$get-or-insert:mismatch:
+    # curr += row-size
+    03/add                          1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   0x10/disp8      .                 # add *(EBP+16) to ECX
+    # loop
+    eb/jump  $get-or-insert:search-loop/disp8
+$get-or-insert:not-found:
+    # result/EAX = 0
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    # if (table->write >= table->length) abort
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
+    3b/compare                      1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   8/disp8         .                 # compare ECX with *(ESI+8)
+    73/jump-if-greater-or-equal-unsigned  $get-or-insert:abort/disp8
+    # zero-out(max, row-size)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    52/push-EDX
+    # . . call
+    e8/call  zero-out/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # *max = key
+    # . EAX = key
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy *(EBP+12) to EAX
+    # . *max = EAX
+    89/copy                         0/mod/indirect  2/rm32/EDX    .           .             .           0/r32/EAX   .               .                 # copy EAX to *EDX
+    # table->write += row-size
+    # . EAX = row-size
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0x10/disp8      .                 # copy *(EBP+16) to EAX
+    # . table->write += EAX
+    01/add                          0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # add EAX to *ESI
+    # return max+4
+    # . EAX = max
+    89/copy                         3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # copy EDX to EAX
+    # . EAX += 4
+    05/add-to-EAX  4/imm32
+$get-or-insert:end:
+    # . restore registers
+    5e/pop-to-ESI
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+$get-or-insert:abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "get-or-insert: too many segments"/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+test-get-or-insert:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var table/ECX : (address stream {string, number}) = stream(2 rows * 8 bytes)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # subtract from ESP
+    68/push  0x10/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+$test-get-or-insert:first-call:
+    # - start with an empty table, insert one key, verify that it was inserted
+    # EAX = get-or-insert(table, "code", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "code"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(EAX - table->data, 4, msg)  # first row's value slot returned
+    # . check-ints-equal(EAX - table, 16, msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/0"/imm32
+    68/push  0x10/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get-or-insert:check2:
+    # check-ints-equal(table->write, row-size = 8, msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/1"/imm32
+    68/push  8/imm32/row-size
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-string-equal(*table->data, "code", msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/2"/imm32
+    68/push  "code"/imm32
+    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           0xc/disp8       .                 # push *(ECX+12)
+    # . . call
+    e8/call  check-string-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get-or-insert:second-call:
+    # - insert the same key again, verify that it was reused
+    # EAX = get-or-insert(table, "code", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "code"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(EAX - table->data, 4, msg)
+    # . check-ints-equal(EAX - table, 16, msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/3"/imm32
+    68/push  0x10/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # no new row inserted
+    # . check-ints-equal(table->write, row-size = 8, msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/4"/imm32
+    68/push  8/imm32/row-size
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-string-equal(*table->data, "code", msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/5"/imm32
+    68/push  "code"/imm32
+    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           0xc/disp8       .                 # push *(ECX+12)
+    # . . call
+    e8/call  check-string-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get-or-insert:third-call:
+    # - insert a new key, verify that it was inserted
+    # EAX = get-or-insert(table, "data", 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    68/push  "data"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # table gets a new row
+    # check-ints-equal(EAX - table->data, 12, msg)  # second row's value slot returned
+    # . check-ints-equal(EAX - table, 24, msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/6"/imm32
+    68/push  0x18/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(table->write, 2 rows = 16, msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/7"/imm32
+    68/push  0x10/imm32/two-rows
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-string-equal(*table->data+8, "data", msg)
+    # check-string-equal(*(table+20), "data", msg)
+    # . . push args
+    68/push  "F - test-get-or-insert/8"/imm32
+    68/push  "data"/imm32
+    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           0x14/disp8      .                 # push *(ECX+20)
+    # . . call
+    e8/call  check-string-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-get-or-insert:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# 'table' is a stream of (key, value) rows
+# keys are always strings (addresses; size 4 bytes)
+# values may be any type, but rows (key+value) always occupy 'row-size' bytes
+# scan 'table' for a row with a key 'key' and return the address of the corresponding value
+# if no row is found, save 'key' in the next available row
+# if there are no rows free, abort
+# WARNING: leaks memory
+# TODO: pass in an allocation descriptor
+leaky-get-or-insert-slice:  # table : (address stream {string, _}), key : (address slice), row-size : int -> EAX : (address _)
+    # pseudocode:
+    #   curr = table->data
+    #   max = &table->data[table->write]
+    #   while curr < max
+    #     if slice-equal?(key, *curr)
+    #       return curr+4
+    #     curr += row-size
+    #   if table->write >= table->length
+    #     abort
+    #   zero-out(max, row-size)
+    #   *max = slice-to-string(Heap, key)
+    #   table->write += row-size
+    #   return max+4
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    56/push-ESI
+    # ESI = table
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # curr/ECX = table->data
+    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   0xc/disp8       .                 # copy ESI+12 to ECX
+    # max/EDX = table->data + table->write
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           2/r32/EDX   .               .                 # copy *ESI to EDX
+    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ECX  2/index/EDX   .           2/r32/EDX   .               .                 # copy ECX+EDX to EDX
+$leaky-get-or-insert-slice:search-loop:
+    # if (curr >= max) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $leaky-get-or-insert-slice:not-found/disp8
+    # if (slice-equal?(key, *curr)) return curr+4
+    # . EAX = slice-equal?(key, *curr)
+    # . . push args
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  slice-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return EAX = curr+4
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $leaky-get-or-insert-slice:mismatch/disp8
+    8d/copy-address                 1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy ECX+4 to EAX
+    eb/jump  $leaky-get-or-insert-slice:end/disp8
+$leaky-get-or-insert-slice:mismatch:
+    # curr += row-size
+    03/add                          1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   0x10/disp8      .                 # add *(EBP+16) to ECX
+    # loop
+    eb/jump  $leaky-get-or-insert-slice:search-loop/disp8
+$leaky-get-or-insert-slice:not-found:
+    # result/EAX = 0
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    # if (table->write >= table->length) abort
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
+    3b/compare                      1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   8/disp8         .                 # compare ECX with *(ESI+8)
+    7d/jump-if-greater-or-equal  $leaky-get-or-insert-slice:abort/disp8
+    # zero-out(max, row-size)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    52/push-EDX
+    # . . call
+    e8/call  zero-out/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # *max = slice-to-string(Heap, key)
+    # . EAX = slice-to-string(Heap, key)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    68/push  Heap/imm32
+    # . . call
+    e8/call  slice-to-string/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . *max = EAX
+    89/copy                         0/mod/indirect  2/rm32/EDX    .           .             .           0/r32/EAX   .               .                 # copy EAX to *EDX
+    # table->write += row-size
+    # . EAX = row-size
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0x10/disp8      .                 # copy *(EBP+16) to EAX
+    # . table->write += EAX
+    01/add                          0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # add EAX to *ESI
+    # return max+4
+    # . EAX = max
+    89/copy                         3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # copy EDX to EAX
+    # . EAX += 4
+    05/add-to-EAX  4/imm32
+$leaky-get-or-insert-slice:end:
+    # . restore registers
+    5e/pop-to-ESI
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+$leaky-get-or-insert-slice:abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "leaky-get-or-insert-slice: too many segments"/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+test-leaky-get-or-insert-slice:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # var table/ECX : (address stream {string, number}) = stream(2 rows * 8 bytes)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # subtract from ESP
+    68/push  0x10/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # (EAX..EDX) = "code"
+    b8/copy-to-EAX  "code"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # copy *EAX to EDX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  2/index/EDX   .           2/r32/EDX   4/disp8         .                 # copy EAX+EDX+4 to EDX
+    05/add-to-EAX  4/imm32
+    # var slice/EDX = {EAX, EDX}
+    52/push-EDX
+    50/push-EAX
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+$test-leaky-get-or-insert-slice:first-call:
+    # - start with an empty table, insert one key, verify that it was inserted
+    # EAX = leaky-get-or-insert-slice(table, "code" slice, 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  leaky-get-or-insert-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(EAX - table->data, 4, msg)  # first row's value slot returned
+    # . check-ints-equal(EAX - table, 16, msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/0"/imm32
+    68/push  0x10/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-leaky-get-or-insert-slice:check2:
+    # check-ints-equal(table->write, row-size = 8, msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/1"/imm32
+    68/push  8/imm32/row-size
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-string-equal(*table->data, "code", msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/2"/imm32
+    68/push  "code"/imm32
+    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           0xc/disp8       .                 # push *(ECX+12)
+    # . . call
+    e8/call  check-string-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-leaky-get-or-insert-slice:second-call:
+    # - insert the same key again, verify that it was reused
+    # EAX = leaky-get-or-insert-slice(table, "code" slice, 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  leaky-get-or-insert-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(EAX - table->data, 4, msg)
+    # . check-ints-equal(EAX - table, 16, msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/3"/imm32
+    68/push  0x10/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # no new row inserted
+    # . check-ints-equal(table->write, row-size = 8, msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/4"/imm32
+    68/push  8/imm32/row-size
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-string-equal(*table->data, "code", msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/5"/imm32
+    68/push  "code"/imm32
+    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           0xc/disp8       .                 # push *(ECX+12)
+    # . . call
+    e8/call  check-string-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-leaky-get-or-insert-slice:third-call:
+    # - insert a new key, verify that it was inserted
+    # (EAX..EDX) = "data"
+    b8/copy-to-EAX  "data"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # copy *EAX to EDX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  2/index/EDX   .           2/r32/EDX   4/disp8         .                 # copy EAX+EDX+4 to EDX
+    05/add-to-EAX  4/imm32
+    # var slice/EDX = {EAX, EDX}
+    52/push-EDX
+    50/push-EAX
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # EAX = leaky-get-or-insert-slice(table, "data" slice, 8 bytes per row)
+    # . . push args
+    68/push  8/imm32/row-size
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  leaky-get-or-insert-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # table gets a new row
+    # check-ints-equal(EAX - table->data, 12, msg)  # second row's value slot returned
+    # . check-ints-equal(EAX - table, 24, msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/6"/imm32
+    68/push  0x18/imm32
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(table->write, 2 rows = 16, msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/7"/imm32
+    68/push  0x10/imm32/two-rows
+    ff          6/subop/push        0/mod/indirect  1/rm32/ECX    .           .             .           .           .               .                 # push *ECX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-string-equal(*table->data+8, "data", msg)
+    # check-string-equal(*(table+20), "data", msg)
+    # . . push args
+    68/push  "F - test-leaky-get-or-insert-slice/8"/imm32
+    68/push  "data"/imm32
+    ff          6/subop/push        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           0x14/disp8      .                 # push *(ECX+20)
+    # . . call
+    e8/call  check-string-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-leaky-get-or-insert-slice:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# (re)compute the bounds of the next word in the line
+# return empty string on reaching end of file
+next-word:  # line : (address stream byte), out : (address slice)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    56/push-ESI
+    57/push-EDI
+    # ESI = line
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # EDI = out
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   0xc/disp8       .                 # copy *(EBP+12) to EDI
+    # skip-chars-matching(line, ' ')
+    # . . push args
+    68/push  0x20/imm32/space
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  skip-chars-matching/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+$next-word:check0:
+    # if (line->read >= line->write) clear out and return
+    # . EAX = line->read
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
+    # . if (EAX < line->write) goto next check
+    3b/compare                      0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # compare EAX with *ESI
+    7c/jump-if-lesser  $next-word:check-for-comment/disp8
+    # . return out = {0, 0}
+    c7          0/subop/copy        0/mod/direct    7/rm32/EDI    .           .             .           .           .               0/imm32           # copy to *EDI
+    c7          0/subop/copy        1/mod/*+disp8   7/rm32/EDI    .           .             .           .           4/disp8         0/imm32           # copy to *(EDI+4)
+    eb/jump  $next-word:end/disp8
+$next-word:check-for-comment:
+    # out->start = &line->data[line->read]
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
+    89/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           0/r32/EAX   .               .                 # copy EAX to *EDI
+    # if (line->data[line->read] == '#') out->end = &line->data[line->write]), skip rest of stream and return
+    # . EAX = line->data[line->read]
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    8a/copy-byte                    1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/AL    0xc/disp8       .                 # copy byte at *(ESI+ECX+12) to AL
+    # . compare
+    3d/compare-EAX-and  0x23/imm32/pound
+    75/jump-if-not-equal  $next-word:regular-word/disp8
+$next-word:comment:
+    # . out->end = &line->data[line->write]
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  0/index/EAX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+EAX+12 to EAX
+    89/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDI+4)
+    # . line->read = line->write
+    89/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(ESI+4)
+    # . return
+    eb/jump  $next-word:end/disp8
+$next-word:regular-word:
+    # otherwise skip-chars-not-matching-whitespace(line)  # including trailing newline
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  skip-chars-not-matching-whitespace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # out->end = &line->data[line->read]
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ESI+4) to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  1/index/ECX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+ECX+12 to EAX
+    89/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EDI+4)
+$next-word:end:
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-next-word:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # var slice/ECX = {0, 0}
+    68/push  0/imm32/end
+    68/push  0/imm32/start
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # write(_test-stream, "  ab")
+    # . . push args
+    68/push  "  ab"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # next-word(_test-stream, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  next-word/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(slice->start - _test-stream->data, 2, msg)
+    # . check-ints-equal(slice->start - _test-stream, 14, msg)
+    # . . push args
+    68/push  "F - test-next-word: start"/imm32
+    68/push  0xe/imm32
+    # . . push slice->start - _test-stream
+    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
+    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(slice->end - _test-stream->data, 4, msg)
+    # . check-ints-equal(slice->end - _test-stream, 16, msg)
+    # . . push args
+    68/push  "F - test-next-word: end"/imm32
+    68/push  0x10/imm32
+    # . . push slice->end - _test-stream
+    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
+    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-next-word-returns-whole-comment:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # var slice/ECX = {0, 0}
+    68/push  0/imm32/end
+    68/push  0/imm32/start
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # write(_test-stream, "  # a")
+    # . . push args
+    68/push  "  # a"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # next-word(_test-stream, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  next-word/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(slice->start - _test-stream->data, 2, msg)
+    # . check-ints-equal(slice->start - _test-stream, 14, msg)
+    # . . push args
+    68/push  "F - test-next-word-returns-whole-comment: start"/imm32
+    68/push  0xe/imm32
+    # . . push slice->start - _test-stream
+    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
+    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # check-ints-equal(slice->end - _test-stream->data, 5, msg)
+    # . check-ints-equal(slice->end - _test-stream, 17, msg)
+    # . . push args
+    68/push  "F - test-next-word-returns-whole-comment: end"/imm32
+    68/push  0x11/imm32
+    # . . push slice->end - _test-stream
+    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
+    81          5/subop/subtract    3/mod/direct    0/rm32/EAX    .           .             .           .           .               _test-stream/imm32 # subtract from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-next-word-returns-empty-string-on-eof:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # var slice/ECX = {0, 0}
+    68/push  0/imm32/end
+    68/push  0/imm32/start
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # write nothing to _test-stream
+    # next-word(_test-stream, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  next-word/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(slice->end - slice->start, 0, msg)
+    # . . push args
+    68/push  "F - test-next-word-returns-empty-string-on-eof"/imm32
+    68/push  0/imm32
+    # . . push slice->end - slice->start
+    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ECX+4) to EAX
+    2b/subtract                     0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # subtract *ECX from EAX
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
 # write an entire stream's contents to a buffered-file
 # ways to do this:
 #   - construct a 'maximal slice' and pass it to write-slice-buffered
@@ -117,6 +1268,1720 @@ test-write-stream-data:
     5d/pop-to-EBP
     c3/return
 
+has-metadata?:  # word : (address slice), s : (address string) -> EAX : boolean
+    # pseudocode:
+    #   var twig : &slice = next-token-from-slice(word->start, word->end, '/')  # skip name
+    #   curr = twig->end
+    #   while true
+    #     twig = next-token-from-slice(curr, word->end, '/')
+    #     if (twig.empty()) break
+    #     if (slice-equal?(twig, s)) return true
+    #     curr = twig->end
+    #   return false
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    56/push-ESI
+    57/push-EDI
+    # ESI = word
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # EDX = word->end
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           2/r32/EDX   4/disp8         .                 # copy *(ESI+4) to EDX
+    # var twig/EDI : (address slice) = {0, 0}
+    68/push  0/imm32/end
+    68/push  0/imm32/start
+    89/copy                         3/mod/direct    7/rm32/EDI    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDI
+    # next-token-from-slice(word->start, word->end, '/', twig)
+    # . . push args
+    57/push-EDI
+    68/push  0x2f/imm32/slash
+    52/push-EDX
+    ff          6/subop/push        0/mod/indirect  6/rm32/ESI    .           .             .           .           .               .                 # push *ESI
+    # . . call
+    e8/call  next-token-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+    # curr/ECX = twig->end
+    8b/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(EDI+4) to ECX
+$has-metadata?:loop:
+    # next-token-from-slice(curr, word->end, '/', twig)
+    # . . push args
+    57/push-EDI
+    68/push  0x2f/imm32/slash
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  next-token-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+    # if (slice-empty?(twig)) return false
+    # . EAX = slice-empty?(twig)
+    # . . push args
+    57/push-EDI
+    # . . call
+    e8/call  slice-empty?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . if (EAX != 0) return false
+    3d/compare-EAX-and  0/imm32
+    75/jump-if-not-equal  $has-metadata?:false/disp8
+    # if (slice-equal?(twig, s)) return true
+    # . EAX = slice-equal?(twig, s)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    57/push-EDI
+    # . . call
+    e8/call  slice-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return true
+    3d/compare-EAX-and  0/imm32
+    75/jump-if-not-equal  $has-metadata?:true/disp8
+    # curr = twig->end
+    8b/copy                         1/mod/*+disp8   7/rm32/EDI    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(EDI+4) to ECX
+    eb/jump  $has-metadata?:loop/disp8
+$has-metadata?:true:
+    b8/copy-to-EAX  1/imm32/true
+    eb/jump  $has-metadata?:end/disp8
+$has-metadata?:false:
+    b8/copy-to-EAX  0/imm32/false
+$has-metadata?:end:
+    # . reclaim locals
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-has-metadata-true:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "ab/imm32"
+    b8/copy-to-EAX  "ab/imm32"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var in/ESI : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
+    # EAX = has-metadata?(ESI, "imm32")
+    # . . push args
+    68/push  "imm32"/imm32
+    56/push-ESI
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-has-metadata-true"/imm32
+    68/push  1/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-has-metadata-false:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "ab/c"
+    b8/copy-to-EAX  "ab/c"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var in/ESI : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
+    # EAX = has-metadata?(ESI, "d")
+    # . . push args
+    68/push  "d"/imm32
+    56/push-ESI
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-has-metadata-false"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-has-metadata-ignore-name:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "a/b"
+    b8/copy-to-EAX  "a/b"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var in/ESI : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
+    # EAX = has-metadata?(ESI, "a")
+    # . . push args
+    68/push  "a"/imm32
+    56/push-ESI
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-has-metadata-ignore-name"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-has-metadata-multiple-true:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "a/b/c"
+    b8/copy-to-EAX  "a/b/c"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var in/ESI : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
+    # EAX = has-metadata?(ESI, "c")
+    # . . push args
+    68/push  "c"/imm32
+    56/push-ESI
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-has-metadata-multiple-true"/imm32
+    68/push  1/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-has-metadata-multiple-false:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "a/b/c"
+    b8/copy-to-EAX  "a/b/c"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var in/ESI : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           4/r32/ESP   .               .                 # copy ESP to ESI
+    # EAX = has-metadata?(ESI, "d")
+    # . . push args
+    68/push  "d"/imm32
+    56/push-ESI
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-has-metadata-multiple-false"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# If datum of 'word' is not a valid name, it must be a hex int. Parse and print
+# it in 'width' bytes of hex, least significant first.
+# Otherwise just print the entire word including metadata.
+# Always print a trailing space.
+emit:  # out : (address buffered-file), word : (address slice), width : int -> <void>
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    56/push-ESI
+    57/push-EDI
+    # ESI = word
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
+    # var name/EDI : (address slice) = {0, 0}
+    68/push  0/imm32/end
+    68/push  0/imm32/start
+    89/copy                         3/mod/direct    7/rm32/EDI    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDI
+    # datum = next-token-from-slice(word->start, word->end, '/')
+    # . . push args
+    57/push-EDI
+    68/push  0x2f/imm32/slash
+    ff          6/subop/push        1/mod/*+disp8   6/rm32/ESI    .           .             .           .           4/disp8         .                 # push *(ESI+4)
+    ff          6/subop/push        0/mod/indirect  6/rm32/ESI    .           .             .           .           .               .                 # push *ESI
+    # . . call
+    e8/call  next-token-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+    # if (is-valid-name?(datum)) write-slice-buffered(out, word) and return
+    # . EAX = is-valid-name?(name)
+    # . . push args
+    57/push-EDI
+    # . . call
+    e8/call  is-valid-name?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . if (EAX != 0)
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $emit:hex-int/disp8
+$emit:name:
+    # . write-slice-buffered(out, word)
+    # . . push args
+    56/push-ESI
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  write-slice-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write-buffered(out, " ")
+    # . . push args
+    68/push  " "/imm32
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  write-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . return
+    eb/jump  $emit:end/disp8
+    # otherwise emit-hex(out, parse-hex-int(datum), width)
+    #   (Weird shit can happen here if the datum of 'word' isn't either a valid
+    #   name or a hex number, but we're only going to be passing in real legal
+    #   programs. We just want to make sure that valid names aren't treated as
+    #   (valid) hex numbers.)
+$emit:hex-int:
+    # . value/EAX = parse-hex-int(datum)
+    # . . push args
+    57/push-EDI
+    # . . call
+    e8/call  parse-hex-int/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . emit-hex(out, value, width)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    50/push-EAX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$emit:end:
+    # . reclaim locals
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-number:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # (EAX..ECX) = "30"
+    b8/copy-to-EAX  "30"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit(_test-output-buffered-file, slice, 1)
+    # . . push args
+    68/push  1/imm32
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "30 ", msg)
+    # . . push args
+    68/push  "F - test-emit-number/1"/imm32
+    68/push  "30 "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-negative-number:
+    # test support for sign-extending negative numbers
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # (EAX..ECX) = "-2"
+    b8/copy-to-EAX  "-2"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit(_test-output-buffered-file, slice, 2)
+    # . . push args
+    68/push  2/imm32
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "fe ff ", msg)
+    # . . push args
+    68/push  "F - test-emit-number/1"/imm32
+    68/push  "fe ff "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-number-with-metadata:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # (EAX..ECX) = "-2/foo"
+    b8/copy-to-EAX  "-2/foo"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit(_test-output-buffered-file, slice, 2)
+    # . . push args
+    68/push  2/imm32
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # the '/foo' will have no impact on the output
+    # check-stream-equal(_test-output-stream, "fe ff ", msg)
+    # . . push args
+    68/push  "F - test-emit-number-with-metadata"/imm32
+    68/push  "fe ff "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-non-number:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # (EAX..ECX) = "xyz"
+    b8/copy-to-EAX  "xyz"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit(_test-output-buffered-file, slice, 2)
+    # . . push args
+    68/push  2/imm32
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "xyz", msg)
+    # . . push args
+    68/push  "F - test-emit-non-number"/imm32
+    68/push  "xyz "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-non-number-with-metadata:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # (EAX..ECX) = "xyz/"
+    b8/copy-to-EAX  "xyz/"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit(_test-output-buffered-file, slice, 2)
+    # . . push args
+    68/push  2/imm32
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "xyz/", msg)
+    # . . push args
+    68/push  "F - test-emit-non-number-with-metadata"/imm32
+    68/push  "xyz/ "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-non-number-with-all-hex-digits-and-metadata:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # (EAX..ECX) = "abcd/xyz"
+    b8/copy-to-EAX  "abcd/xyz"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit(_test-output-buffered-file, slice, 2)
+    # . . push args
+    68/push  2/imm32
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # dump output {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, _test-output-stream)
+#?     # . . push args
+#?     68/push  _test-output-stream/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check-stream-equal(_test-output-stream, "abcd/xyz")
+    # . . push args
+    68/push  "F - test-emit-non-number-with-all-hex-digits"/imm32
+    68/push  "abcd/xyz "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# conditions for 'valid' names that are not at risk of looking like hex numbers
+# keep in sync with the rules in labels.cc
+#: - if it starts with a digit, it's treated as a number. If it can't be
+#:   parsed as hex it will raise an error.
+#: - if it starts with '-' it's treated as a number.
+#: - if it starts with '0x' it's treated as a number. (redundant)
+#: - if it's two characters long, it can't be a name. Either it's a hex
+#:   byte, or it raises an error.
+is-valid-name?:  # in : (address slice) -> EAX : boolean
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    56/push-ESI
+    # ESI = in
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # start/ECX = in->start
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
+    # end/EAX = in->end
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
+$is-valid-name?:check0:
+    # if (start >= end) return false
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # compare ECX with EAX
+    73/jump-if-greater-or-equal-unsigned  $is-valid-name?:false/disp8
+$is-valid-name?:check1:
+    # EAX -= ECX
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EAX
+    # if (EAX == 2) return false
+    3d/compare-EAX-and  2/imm32
+    74/jump-if-equal  $is-valid-name?:false/disp8
+$is-valid-name?:check2:
+    # c/EAX = *ECX
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
+    # if (c == "-") return false
+    3d/compare-EAX-and  2d/imm32/-
+    74/jump-if-equal  $is-valid-name?:false/disp8
+$is-valid-name?:check3a:
+    # if (c < "0") return true
+    3d/compare-EAX-with  30/imm32/0
+    7c/jump-if-lesser  $is-valid-name?:true/disp8
+$is-valid-name?:check3b:
+    # if (c > "9") return true
+    3d/compare-EAX-with  39/imm32/9
+    7f/jump-if-greater  $is-valid-name?:true/disp8
+$is-valid-name?:false:
+    # return false
+    b8/copy-to-EAX  0/imm32/false
+    eb/jump  $is-valid-name?:end/disp8
+$is-valid-name?:true:
+    # return true
+    b8/copy-to-EAX  1/imm32/true
+$is-valid-name?:end:
+    # . restore registers
+    5e/pop-to-ESI
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-is-valid-name-digit-prefix:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "34"
+    b8/copy-to-EAX  "34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # EAX = is-valid-name?(slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  is-valid-name?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-is-valid-name-digit-prefix"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-is-valid-name-negative-prefix:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "-0x34"
+    b8/copy-to-EAX  "-0x34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # EAX = is-valid-name?(slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  is-valid-name?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-is-valid-name-negative-prefix"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-is-valid-name-0x-prefix:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "0x34"
+    b8/copy-to-EAX  "0x34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # EAX = is-valid-name?(slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  is-valid-name?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-is-valid-name-0x-prefix"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-is-valid-name-starts-with-pre-digit:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "/03"
+    b8/copy-to-EAX  "/03"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # EAX = is-valid-name?(slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  is-valid-name?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-is-valid-name-starts-with-pre-digit"/imm32
+    68/push  1/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-is-valid-name-starts-with-post-digit:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "q34"
+    b8/copy-to-EAX  "q34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # EAX = is-valid-name?(slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  is-valid-name?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-is-valid-name-starts-with-post-digit"/imm32
+    68/push  1/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-is-valid-name-starts-with-digit:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # (EAX..ECX) = "0x34"
+    b8/copy-to-EAX  "0x34"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # EAX = is-valid-name?(slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  is-valid-name?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-is-valid-name-starts-with-digit"/imm32
+    68/push  0/imm32/false
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# print 'n' in hex in 'width' bytes in lower-endian order, with a space after every byte
+emit-hex:  # out : (address buffered-file), n : int, width : int -> <void>
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    57/push-EDI
+    # EDI = out
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   8/disp8         .                 # copy *(EBP+8) to EDI
+    # EBX = n
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           3/r32/EBX   0xc/disp8       .                 # copy *(EBP+12) to EBX
+    # EDX = width
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           2/r32/EDX   0x10/disp8      .                 # copy *(EBP+16) to EDX
+    # var curr/ECX = 0
+    31/xor                          3/mod/direct    1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # clear ECX
+$emit-hex:loop:
+    # if (curr >= width) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    7d/jump-if-greater-or-equal  $emit-hex:end/disp8
+    # print-byte-buffered(out, EBX)
+    # . . push args
+    53/push-EBX
+    57/push-EDI
+    # . . call
+    e8/call  print-byte-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write-byte-buffered(out, ' ')
+    # . . push args
+    68/push  0x20/imm32/space
+    57/push-EDI
+    # . . call
+    e8/call  write-byte-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EBX = EBX >> 8
+    c1/shift    5/subop/logic-right 3/mod/direct    3/rm32/EBX    .           .             .           .           .               8/imm8            # shift EBX right by 8 bits, while padding zeroes
+$emit-hex:continue:
+    # ++curr
+    41/increment-ECX
+    eb/jump  $emit-hex:loop/disp8
+$emit-hex:end:
+    # . restore registers
+    5f/pop-to-EDI
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-hex-single-byte:
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # emit-hex(_test-output-buffered-file, 0xab, 1)
+    # . . push args
+    68/push  1/imm32
+    68/push  0xab/imm32
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(*_test-output-stream->data, 'ab ', msg)
+    # . . push args
+    68/push  "F - test-emit-hex-single-byte"/imm32
+    68/push  0x206261/imm32
+    # . . push *_test-output-stream->data
+    b8/copy-to-EAX  _test-output-stream/imm32
+    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           0xc/disp8       .                 # push *(EAX+12)
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . end
+    c3/return
+
+test-emit-hex-multiple-byte:
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # emit-hex(_test-output-buffered-file, 0x1234, 2)
+    # . . push args
+    68/push  2/imm32
+    68/push  0x1234/imm32
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "34 12 ", msg)
+    # . . push args
+    68/push  "F - test-emit-hex-multiple-byte/1"/imm32
+    68/push  "34 12 "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . end
+    c3/return
+
+test-emit-hex-zero-pad:
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # emit-hex(_test-output-buffered-file, 0xab, 2)
+    # . . push args
+    68/push  2/imm32
+    68/push  0xab/imm32
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check(_test-output-stream->data == 'ab 00 ')
+    # . . push args
+    68/push  "F - test-emit-hex-zero-pad/1"/imm32
+    68/push  "ab 00 "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . end
+    c3/return
+
+test-emit-hex-negative:
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # emit-hex(_test-output-buffered-file, -1, 2)
+    # . . push args
+    68/push  2/imm32
+    68/push  -1/imm32
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream == "ff ff ")
+    # . . push args
+    68/push  "F - test-emit-hex-negative/1"/imm32
+    68/push  "ff ff "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . end
+    c3/return
+
+# print 'arr' in hex with a space after every byte
+emit-hex-array:  # out : (address buffered-file), arr : (address array byte) -> <void>
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    57/push-EDI
+    # EDI = out
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           7/r32/EDI   8/disp8         .                 # copy *(EBP+8) to EDI
+    # EDX = arr  # <== 0xbdffffe4
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           2/r32/EDX   0xc/disp8       .                 # copy *(EBP+12) to EDX
+    # curr/ECX = arr->data
+    8d/copy-address                 1/mod/*+disp8   2/rm32/EDX    .           .             .           1/r32/ECX   4/disp8         .                 # copy EDX+4 to ECX
+    # max/EDX = arr->data + arr->length
+    8b/copy                         0/mod/indirect  2/rm32/EDX    .           .             .           2/r32/EDX   .               .                 # copy *EDX to EDX
+    01/add                          3/mod/direct    2/rm32/EDX    .           .             .           1/r32/ECX   .               .                 # add ECX to EDX
+    # EAX = 0
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+$emit-hex-array:loop:
+    # if (curr >= width) break
+    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
+    73/jump-if-greater-or-equal-unsigned  $emit-hex-array:end/disp8
+    # emit-hex(out, *curr, width=1)
+    # . . push args
+    68/push  1/imm32/width
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/AL    .               .                 # copy byte at *ECX to AL
+    50/push-EAX
+    57/push-EDI
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # ++curr
+    41/increment-ECX
+    eb/jump  $emit-hex-array:loop/disp8
+$emit-hex-array:end:
+    # . restore registers
+    5f/pop-to-EDI
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-hex-array:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # var arr/ECX (address array byte) = [01, 02, 03]
+    68/push  0x00030201/imm32  # bytes 01 02 03
+    68/push  3/imm32/length
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit-hex-array(_test-output-buffered-file, arr)
+    # . . push args
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-hex-array/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # dump output {{{
+#?     # . write(2/stderr, "result: ^")
+#?     # . . push args
+#?     68/push  "result: ^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, _test-output-stream)
+#?     # . . push args
+#?     68/push  _test-output-stream/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . rewind-stream(_test-output-stream)
+#?     # . . push args
+#?     68/push  _test-output-stream/imm32
+#?     # . . call
+#?     e8/call  rewind-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # }}}
+    # check-next-stream-line-equal(_test-output-stream, "01 02 03 ", msg)
+    # . . push args
+    68/push  "F - test-emit-hex-array"/imm32
+    68/push  "01 02 03 "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-next-stream-line-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+compute-width: # word : (address array byte) -> EAX : int
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    # EAX = word
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # ECX = word + word->length
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    # EAX = word->data
+    05/add-to-EAX  4/imm32
+    # var in/ECX : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # return compute-width-of-slice(ECX)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  compute-width-of-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+$compute-width:end:
+    # . reclaim locals
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . restore registers
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+compute-width-of-slice: # s : (address slice) -> EAX : int
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    # ECX = s
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # if (has-metadata?(word, "imm32")) return 4
+    # . EAX = has-metadata?(word, "imm32")
+    # . . push args
+    68/push  "imm32"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return 4
+    3d/compare-EAX-and  0/imm32
+    b8/copy-to-EAX  4/imm32         # ZF is set, so we can overwrite EAX now
+    75/jump-if-not-equal  $compute-width-of-slice:end/disp8
+    # if (has-metadata?(word, "disp32")) return 4
+    # . EAX = has-metadata?(word, "disp32")
+    # . . push args
+    68/push  "disp32"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return 4
+    3d/compare-EAX-and  0/imm32
+    b8/copy-to-EAX  4/imm32         # ZF is set, so we can overwrite EAX now
+    75/jump-if-not-equal  $compute-width-of-slice:end/disp8
+    # if (has-metadata?(word, "imm16")) return 2
+    # . EAX = has-metadata?(word, "imm16")
+    # . . push args
+    68/push  "imm16"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return 2
+    3d/compare-EAX-and  0/imm32
+    b8/copy-to-EAX  2/imm32         # ZF is set, so we can overwrite EAX now
+    75/jump-if-not-equal  $compute-width-of-slice:end/disp8
+    # if (has-metadata?(word, "disp16")) return 2
+    # . EAX = has-metadata?(word, "disp16")
+    # . . push args
+    68/push  "disp16"/imm32
+    51/push-ECX
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) return 2
+    3d/compare-EAX-and  0/imm32
+    b8/copy-to-EAX  2/imm32         # ZF is set, so we can overwrite EAX now
+    75/jump-if-not-equal  $compute-width-of-slice:end/disp8
+    # otherwise return 1
+    b8/copy-to-EAX  1/imm32
+$compute-width-of-slice:end:
+    # . restore registers
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-compute-width:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+$test-compute-width:imm8:
+    # EAX = compute-width("0x2/imm8")
+    # . . push args
+    68/push  "0x2/imm8"/imm32
+    # . . call
+    e8/call  compute-width/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-compute-width: 0x2/imm8"/imm32
+    50/push-EAX
+    68/push  1/imm32
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-compute-width:imm16:
+    # EAX = compute-width("4/imm16")
+    # . . push args
+    68/push  "4/imm16"/imm32
+    # . . call
+    e8/call  compute-width/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 2, msg)
+    # . . push args
+    68/push  "F - test-compute-width: 4/imm16"/imm32
+    50/push-EAX
+    68/push  2/imm32
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-compute-width:imm32:
+    # EAX = compute-width("4/imm32")
+    # . . push args
+    68/push  "4/imm32"/imm32
+    # . . call
+    e8/call  compute-width/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 4, msg)
+    # . . push args
+    68/push  "F - test-compute-width: 4/imm32"/imm32
+    50/push-EAX
+    68/push  4/imm32
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-compute-width:disp8:
+    # EAX = compute-width("foo/disp8")
+    # . . push args
+    68/push  "foo/disp8"/imm32
+    # . . call
+    e8/call  compute-width/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-compute-width: foo/disp8"/imm32
+    50/push-EAX
+    68/push  1/imm32
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-compute-width:disp16:
+    # EAX = compute-width("foo/disp16")
+    # . . push args
+    68/push  "foo/disp16"/imm32
+    # . . call
+    e8/call  compute-width/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 2, msg)
+    # . . push args
+    68/push  "F - test-compute-width: foo/disp16"/imm32
+    50/push-EAX
+    68/push  2/imm32
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-compute-width:disp32:
+    # EAX = compute-width("foo/disp32")
+    # . . push args
+    68/push  "foo/disp32"/imm32
+    # . . call
+    e8/call  compute-width/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 4, msg)
+    # . . push args
+    68/push  "F - test-compute-width: foo/disp32"/imm32
+    50/push-EAX
+    68/push  4/imm32
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-compute-width:no-metadata:
+    # EAX = compute-width("45")
+    # . . push args
+    68/push  "45"/imm32
+    # . . call
+    e8/call  compute-width/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-compute-width: 45 (no metadata)"/imm32
+    50/push-EAX
+    68/push  1/imm32
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+is-label?: # word : (address slice) -> EAX : boolean
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    # ECX = word
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
+    # ECX = word->end
+    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(ECX+4) to ECX
+    # return *(word->end - 1) == ':'
+    # . EAX = 0
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    # . EAX = *((char *) word->end - 1)
+    8a/copy-byte                    1/mod/*+disp8   1/rm32/ECX    .           .             .           0/r32/AL    -1/disp8         .                 # copy byte at *(ECX-1) to AL
+    # . return (EAX == ':')
+    3d/compare-EAX-and  0x3a/imm32/colon
+    b8/copy-to-EAX  1/imm32/true
+    74/jump-if-equal  $is-label?:end/disp8
+    b8/copy-to-EAX  0/imm32/false
+$is-label?:end:
+    # . restore registers
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-is-label?:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+$test-is-label?:true:
+    # (EAX..ECX) = "AAA:"
+    b8/copy-to-EAX  "AAA:"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # is-label?(slice/ECX)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  is-label?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 1, msg)
+    # . . push args
+    68/push  "F - test-is-label?:true"/imm32
+    68/push  1/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$test-is-label?:false:
+    # (EAX..ECX) = "AAA"
+    b8/copy-to-EAX  "AAA"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var slice/ECX = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # is-label?(slice/ECX)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  is-label?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-is-label?:false"/imm32
+    68/push  0/imm32
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
 == data
 
 _test-input-stream:
@@ -163,8 +3028,24 @@ _test-output-stream:
     # current read index
     0/imm32
     # length
-    0x100/imm32  # 256 bytes
-    # data (16 lines x 16 bytes/line)
+    0x200/imm32  # 512 bytes
+    # data (32 lines x 16 bytes/line)
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
@@ -195,4 +3076,8 @@ _test-output-buffered-file:
     # data
     00 00 00 00 00 00  # 6 bytes
 
+_test-data-segment:
+  64/d 61/a 74/t 61/a
+_test-data-segment-end:
+
 # . . vim:nowrap:textwidth=0
diff --git a/subx/apps/survey b/subx/apps/survey
new file mode 100755
index 00000000..151464a5
--- /dev/null
+++ b/subx/apps/survey
Binary files differdiff --git a/subx/apps/survey.subx b/subx/apps/survey.subx
new file mode 100644
index 00000000..65557b20
--- /dev/null
+++ b/subx/apps/survey.subx
@@ -0,0 +1,3993 @@
+# Assign addresses (co-ordinates) to instructions (landmarks) in a program
+# (landscape).
+# Use the addresses assigned to:
+#   a) replace labels
+#   b) add segment headers with addresses and offsets correctly filled in
+#
+# To build (from the subx/ directory):
+#   $ ./subx translate *.subx apps/survey.subx -o apps/survey
+#
+# The expected input is a stream of bytes with segment headers, comments and
+# some interspersed labels.
+#   $ cat x
+#   == code 0x1
+#   l1:
+#   aa bb l1/imm8
+#   cc dd l2/disp32
+#   l2:
+#   ee foo/imm32
+#   == data 0x10
+#   foo:
+#     00
+#
+# The output is the stream of bytes without segment headers or label definitions,
+# and with label references replaced with numeric values/displacements.
+#
+#   $ cat x  |./subx run apps/assort
+#   ...ELF header bytes...
+#   # ELF header above will specify that code segment begins at this offset
+#   aa bb nn  # some computed address
+#   cc dd nn nn nn nn  # some computed displacement
+#   ee nn nn nn nn  # some computed address
+#   # ELF header above will specify that data segment begins at this offset
+#   00
+
+== code
+#   instruction                     effective address                                                   register    displacement    immediate
+# . op          subop               mod             rm32          base        index         scale       r32
+# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
+
+Entry:
+    # Heap = new-segment(64KB)
+    # . . push args
+    68/push  Heap/imm32
+    68/push  0x10000/imm32/64KB
+    # . . call
+    e8/call  new-segment/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # initialize-trace-stream(256KB)
+    # . . push args
+    68/push  0x40000/imm32/256KB
+    # . . call
+    e8/call  initialize-trace-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+
+    # run tests if necessary, convert stdin if not
+    # . prolog
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # initialize heap
+    # - if argc > 1 and argv[1] == "test", then return run_tests()
+    # . argc > 1
+    81          7/subop/compare     1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0/disp8         1/imm32           # compare *EBP
+    7e/jump-if-lesser-or-equal  $run-main/disp8
+    # . argv[1] == "test"
+    # . . push args
+    68/push  "test"/imm32
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  kernel-string-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check result
+    3d/compare-EAX-and  1/imm32
+    75/jump-if-not-equal  $run-main/disp8
+    # . run-tests()
+    e8/call  run-tests/disp32
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
+    eb/jump  $main:end/disp8
+$run-main:
+    # - otherwise convert stdin
+    # var ed/EAX : exit-descriptor
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
+    89/copy                         3/mod/direct    0/rm32/EAX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EAX
+    # configure ed to really exit()
+    # . ed->target = 0
+    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # copy to *EAX
+    # return convert(Stdin, 1/stdout, 2/stderr, ed)
+    # . . push args
+    50/push-EAX/ed
+    68/push  Stderr/imm32
+    68/push  Stdout/imm32
+    68/push  Stdin/imm32
+    # . . call
+    e8/call  convert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+    # . syscall(exit, 0)
+    bb/copy-to-EBX  0/imm32
+$main:end:
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+
+# data structures:
+#   segment-info: {address, file-offset, size}            (12 bytes)
+#   segments: (address stream {string, segment-info})     (16 bytes per row)
+#   label-info: {segment-name, segment-offset, address}   (12 bytes)
+#   labels: (address stream {string, label-info})         (16 bytes per row)
+# these are all inefficient; use sequential scans for lookups
+
+convert:  # in : (address buffered-file), out : (address buffered-file) -> <void>
+    # pseudocode
+    #   var segments = new-stream(10 rows, 16 bytes each)
+    #   var labels = new-stream(512 rows, 16 bytes each)
+    #   compute-offsets(in, segments, labels)
+    #   compute-addresses(segments, labels)
+    #   rewind-stream(in)
+    #   emit-output(in, out, segments, labels)
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    # var segments/ECX = stream(10 * 16)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xa0/imm32        # subtract from ESP
+    68/push  0xa0/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # var labels/EDX = stream(512 * 16)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x2000/imm32      # subtract from ESP
+    68/push  0x2000/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+#?     # write(2/stderr, "compute-offsets\n") {{{
+#?     # . . push args
+#?     68/push  "compute-offsets\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # compute-offsets(in, segments, labels)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  compute-offsets/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+#?     # write(2/stderr, "compute-addresses\n") {{{
+#?     # . . push args
+#?     68/push  "compute-addresses\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # compute-addresses(segments, labels)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  compute-addresses/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x8/imm32         # add to ESP
+    # rewind-stream(in)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  rewind-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # write(2/stderr, "emit-output\n") {{{
+#?     # . . push args
+#?     68/push  "emit-output\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+#?     # dump labels->write {{{
+#?     # . write(2/stderr, "labels->write after rewinding input: ")
+#?     # . . push args
+#?     68/push  "labels->write after rewinding input: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . clear-stream(Stderr+4)
+#?     # . . save EAX
+#?     50/push-EAX
+#?     # . . push args
+#?     b8/copy-to-EAX  Stderr/imm32
+#?     05/add-to-EAX  4/imm32
+#?     50/push-EAX
+#?     # . . call
+#?     e8/call  clear-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . . restore EAX
+#?     58/pop-to-EAX
+#?     # . print-int32-buffered(Stderr, labels)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  2/rm32/EDX    .           .             .           .           .               .                 # push *EDX
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  print-int32-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "\n")
+#?     # . . push args
+#?     68/push  "\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # emit-output(in, out, segments, labels)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  emit-output/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+$convert:end:
+    # . reclaim locals
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x214/imm32       # add to ESP
+    # . restore registers
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-convert-computes-addresses:
+    # input:
+    #   == code 0x1
+    #   Entry:
+    #   ab x/imm32
+    #   == data 0x1000
+    #   x:
+    #     01
+    #
+    # trace contains (in any order):
+    #   label x is at address 0x1079
+    #   segment code starts at address 0x74
+    #   segment code has size 5
+    #   segment data starts at address 0x1079
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-input-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-input-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # initialize input
+    # . write(_test-input-stream, "== code 0x1\n")
+    # . . push args
+    68/push  "== code 0x1\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "Entry:\n")
+    # . . push args
+    68/push  "Entry:\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "ab x/imm32\n")
+    # . . push args
+    68/push  "ab x/imm32\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "== data 0x1000\n")
+    # . . push args
+    68/push  "== data 0x1000\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "x:\n")
+    # . . push args
+    68/push  "x:\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "01\n")
+    # . . push args
+    68/push  "01\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # convert(_test-input-buffered-file, _test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    68/push  _test-input-buffered-file/imm32
+    # . . call
+    e8/call  convert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # check trace
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # . check-trace-contains("label 'x' is at address 0x00001079.", msg)
+    # . . push args
+    68/push  "F - test-convert-computes-addresses/0"/imm32
+    68/push  "label 'x' is at address 0x00001079."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("segment 'code' starts at address 0x00000074.", msg)
+    # . . push args
+    68/push  "F - test-convert-computes-addresses/1"/imm32
+    68/push  "segment 'code' starts at address 0x00000074."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("segment 'code' has size 0x00000005.", msg)
+    # . . push args
+    68/push  "F - test-convert-computes-addresses/2"/imm32
+    68/push  "segment 'code' has size 0x00000005."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("segment 'data' starts at address 0x00001079.", msg)
+    # . . push args
+    68/push  "F - test-convert-computes-addresses/3"/imm32
+    68/push  "segment 'data' starts at address 0x00001079."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# global scratch space for compute-offsets in the data segment
+== data
+
+compute-offsets:file-offset:  # int
+  0/imm32
+compute-offsets:segment-offset:  # int
+  0/imm32
+compute-offsets:word-slice:
+  0/imm32/start
+compute-offsets:word-slice:end:
+  0/imm32/end
+compute-offsets:segment-tmp:  # slice
+  0/imm32/start
+  0/imm32/end
+
+== code
+
+compute-offsets:  # in : (address buffered-file), segments : (address stream {string, segment-info}), labels : (address stream {string, label-info})
+    # skeleton:
+    #   for lines in 'in'
+    #     for words in line
+    #       switch word
+    #         case 1
+    #         case 2
+    #         ...
+    #         default
+    #
+    # pseudocode:
+    #   curr-segment-name : (address string) = 0
+    #   var line = new-stream(512, 1)
+    #   while true                                  # line loop
+    #     clear-stream(line)
+    #     read-line-buffered(in, line)
+    #     if (line->write == 0) break               # end of file
+    #     while true                                # word loop
+    #       word-slice = next-word(line)
+    #       if slice-empty?(word-slice)             # end of line
+    #         break
+    #       else if slice-starts-with?(word-slice, "#")  # comment
+    #         continue
+    #       else if slice-equal?(word-slice, "==")
+    #         if curr-segment-name != 0
+    #           seg = get-or-insert(segments, curr-segment-name)
+    #           seg->size = *file-offset - seg->file-offset
+    #           trace("segment '", curr-segment-name, "' has size ", seg->size)
+    #         segment-tmp = next-word(line)
+    #         curr-segment-name = slice-to-string(segment-tmp)
+    #         if empty?(curr-segment-name)
+    #           abort
+    #         segment-tmp = next-word(line)
+    #         if slice-empty?(segment-tmp)
+    #           abort
+    #         seg = get-or-insert(segments, curr-segment-name)
+    #         seg->starting-address = parse-hex-int(segment-tmp)
+    #         seg->file-offset = *file-offset
+    #         trace("segment '", curr-segment-name, "' is at file offset ", seg->file-offset)
+    #         segment-offset = 0
+    #         break  (next line)
+    #       else if is-label?(word-slice)
+    #         strip trailing ':' from word-slice
+    #         x : (address label-info) = get-or-insert(labels, name)
+    #         x->segment-name = curr-segment-name
+    #         trace("label '", word-slice, "' is in segment '", curr-segment-name, "'.")
+    #         x->segment-offset = segment-offset
+    #         trace("label '", word-slice, "' is at segment offset ", segment-offset, ".")
+    #         # labels occupy no space, so no need to increment offsets
+    #       else
+    #         width = compute-width-of-slice(word-slice)
+    #         *segment-offset += width
+    #         *file-offset += width
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    56/push-ESI
+    57/push-EDI
+    # curr-segment-name/ESI = 0
+    31/xor                          3/mod/direct    6/rm32/ESI    .           .             .           6/r32/ESI   .               .                 # clear ESI
+    # file-offset = 0
+    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           compute-offsets:file-offset/disp32  0/imm32               # copy to *compute-offsets:word-slice
+    # segment-offset = 0
+    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           compute-offsets:segment-offset/disp32  0/imm32            # copy to *compute-offsets:word-slice
+    # line/ECX = new-stream(512, 1)
+    # . EAX = new-stream(512, 1)
+    # . . push args
+    68/push  1/imm32
+    68/push  0x200/imm32
+    68/push  Heap/imm32
+    # . . call
+    e8/call  new-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . line/ECX = EAX
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to ECX
+$compute-offsets:line-loop:
+    # clear-stream(line/ECX)
+    51/push-ECX
+    e8/call  clear-stream/disp32
+    # . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # read-line-buffered(in, line/ECX)
+    51/push-ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    e8/call  read-line-buffered/disp32
+    # . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # if (line->write == 0) break
+    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
+    3d/compare-EAX-and  0/imm32
+    0f 84/jump-if-equal  $compute-offsets:break-line-loop/disp32
+$compute-offsets:word-loop:
+    # EDX = word-slice
+    ba/copy-to-EDX  compute-offsets:word-slice/imm32
+    # next-word(line/ECX, word-slice/EDX)
+    52/push-EDX
+    51/push-ECX
+    e8/call  next-word/disp32
+    # . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # dump word-slice and maybe curr-segment-name {{{
+#?     # . write(2/stderr, "AA: ")
+#?     # . . push args
+#?     68/push  "AA: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . clear-stream(Stderr+4)
+#?     # . . save EAX
+#?     50/push-EAX
+#?     # . . push args
+#?     b8/copy-to-EAX  Stderr/imm32
+#?     05/add-to-EAX  4/imm32
+#?     50/push-EAX
+#?     # . . call
+#?     e8/call  clear-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . . restore EAX
+#?     58/pop-to-EAX
+#?     # . write-slice-buffered(Stderr, word-slice)
+#?     # . . push args
+#?     52/push-EDX
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  write-slice-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . if (curr-segment-name == 0) print curr-segment-name
+#?     81          7/subop/compare     3/mod/direct    6/rm32/ESI    .           .             .           .           .               0/imm32           # compare ESI
+#?     74/jump-if-equal  $compute-offsets:check0/disp8
+#?     # . write(2/stderr, "segment at start of word: ")
+#?     # . . push args
+#?     68/push  "segment at start of word: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-buffered(Stderr, curr-segment-name)
+#?     # . . push args
+#?     56/push-ESI
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  write-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+$compute-offsets:check0:
+    # if slice-empty?(word/EDX) break
+    # . EAX = slice-empty?(word/EDX)
+    52/push-EDX
+    e8/call  slice-empty?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . if (EAX != 0) break
+    3d/compare-EAX-and  0/imm32
+    0f 85/jump-if-not-equal  $compute-offsets:line-loop/disp32
+    # if slice-starts-with?(word-slice, "#") continue
+    68/push  "#"/imm32
+    52/push-EDX
+    e8/call  slice-starts-with?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) continue
+    3d/compare-EAX-and  0/imm32
+    0f 85/jump-if-not-equal  $compute-offsets:word-loop/disp32
+$compute-offsets:case-segment-header:
+    # if (!slice-equal?(word-slice/EDX, "==")) goto next case
+    # . EAX = slice-equal?(word-slice/EDX, "==")
+    68/push  "=="/imm32
+    52/push-EDX
+    e8/call  slice-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX == 0) goto next case
+    3d/compare-EAX-and  0/imm32
+    0f 84/jump-if-equal  $compute-offsets:case-label/disp32
+    # if (curr-segment-name == 0) goto construct-next-segment
+    81          7/subop/compare     3/mod/direct    6/rm32/ESI    .           .             .           .           .               0/imm32           # compare ESI
+    74/jump-if-equal  $compute-offsets:construct-next-segment/disp8
+    # seg/EAX = get-or-insert(segments, curr-segment-name, row-size=16)
+    # . . push args
+    68/push  0x10/imm32/row-size
+    56/push-ESI
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # seg->size = file-offset - seg->file-offset
+    # . save ECX
+    51/push-ECX
+    # . EBX = *file-offset
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   compute-offsets:file-offset/disp32 # copy *file-offset to EBX
+    # . ECX = seg->file-offset
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(EAX+4) to ECX
+    # . EBX -= ECX
+    29/subtract                     3/mod/direct    3/rm32/EBX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EBX
+    # . seg->size = EBX
+    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           3/r32/EBX   8/disp8         .                 # copy EBX to *(EAX+8)
+    # . restore ECX
+    59/pop-to-ECX
+    # trace-sssns("segment '", curr-segment-name, "' has size ", seg->size, ".")
+    # . . push args
+    68/push  "."/imm32
+    53/push-EBX
+    68/push  "' has size "/imm32
+    56/push-ESI
+    68/push  "segment '"/imm32
+    # . . call
+    e8/call  trace-sssns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+$compute-offsets:construct-next-segment:
+    # next-word(line/ECX, segment-tmp)
+    68/push  compute-offsets:segment-tmp/imm32
+    51/push-ECX
+    e8/call  next-word/disp32
+    # . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # dump curr-segment-name if not null (clobbering EAX) {{{
+#?     # . if (curr-segment-name == 0) goto update-curr-segment-name
+#?     81          7/subop/compare     3/mod/direct    6/rm32/ESI    .           .             .           .           .               0/imm32           # compare ESI
+#?     74/jump-if-equal  $compute-offsets:update-curr-segment-name/disp8
+#?     # . write(2/stderr, "setting segment to: ")
+#?     # . . push args
+#?     68/push  "setting segment to: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . clear-stream(Stderr+4)
+#?     # . . push args
+#?     b8/copy-to-EAX  Stderr/imm32
+#?     05/add-to-EAX  4/imm32
+#?     50/push-EAX
+#?     # . . call
+#?     e8/call  clear-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . . restore EAX
+#?     58/pop-to-EAX
+#?     # . write-buffered(Stderr, curr-segment-name)
+#?     # . . push args
+#?     56/push-ESI
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  write-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+$compute-offsets:update-curr-segment-name:
+    # curr-segment-name = slice-to-string(segment-tmp)
+    # . EAX = slice-to-string(Heap, segment-tmp)
+    # . . push args
+    68/push  compute-offsets:segment-tmp/imm32
+    68/push  Heap/imm32
+    # . . call
+    e8/call  slice-to-string/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . curr-segment-name = EAX
+    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy EAX to ESI
+    # if empty?(curr-segment-name) abort
+    # . if (EAX == 0) abort
+    3d/compare-EAX-and  0/imm32
+    0f 84/jump-if-equal  $compute-offsets:abort/disp32
+    # next-word(line/ECX, segment-tmp)
+    68/push  compute-offsets:segment-tmp/imm32
+    51/push-ECX
+    e8/call  next-word/disp32
+    # . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # if slice-empty?(segment-tmp) abort
+    # . EAX = slice-empty?(segment-tmp)
+    68/push  compute-offsets:segment-tmp/imm32
+    e8/call  slice-empty?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . if (EAX != 0) abort
+    3d/compare-EAX-and  0/imm32
+    0f 85/jump-if-not-equal  $compute-offsets:abort/disp32
+    # seg/EBX = get-or-insert(segments, curr-segment-name, row-size=16)
+    # . . push args
+    68/push  0x10/imm32/row-size
+    56/push-ESI
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . EBX = EAX
+    89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EBX
+    # seg->address = parse-hex-int(segment-tmp)
+    # . EAX = parse-hex-int(segment-tmp)
+    68/push  compute-offsets:segment-tmp/imm32
+    e8/call  parse-hex-int/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . seg->address = EAX
+    89/copy                         0/mod/indirect  3/rm32/EBX    .           .             .           0/r32/EAX   .               .                 # copy EAX to *EBX
+    # seg->file-offset = *file-offset
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   compute-offsets:file-offset/disp32 # copy *file-offset to EAX
+    89/copy                         1/mod/*+disp8   3/rm32/EBX    .           .             .           0/r32/EAX   4/disp8         .                 # copy EAX to *(EBX+4)
+    # trace-sssns("segment '", curr-segment-name, "' is at file offset ", seg->file-offset, "")
+    # . . push args
+    68/push  "."/imm32
+    50/push-EAX
+    68/push  "' is at file offset "/imm32
+    56/push-ESI
+    68/push  "segment '"/imm32
+    # . . call
+    e8/call  trace-sssns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # segment-offset = 0
+    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .     compute-offsets:segment-offset/disp32  0/imm32           # copy to *segment-offset
+    # break
+    e9/jump $compute-offsets:line-loop/disp32
+$compute-offsets:case-label:
+    # if (!is-label?(word-slice/EDX)) goto next case
+    # . EAX = is-label?(word-slice/EDX)
+    # . . push args
+    52/push-EDX
+    # . . call
+    e8/call  is-label?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . if (EAX == 0) goto next case
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $compute-offsets:case-default/disp8
+    # strip trailing ':' from word-slice
+    ff          1/subop/decrement   1/mod/*+disp8   2/rm32/EDX    .           .             .           .           4/disp8         .                 # decrement *(EDX+4)
+    # x/EAX = leaky-get-or-insert-slice(labels, word-slice, row-size=16)
+    # . . push args
+    68/push  0x10/imm32/row-size
+    52/push-EDX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    # . . call
+    e8/call  leaky-get-or-insert-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+$compute-offsets:save-label-offset:
+    # x->segment-name = curr-segment-name
+    89/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           6/r32/ESI   .               .                 # copy ESI to *EAX
+    # trace-slsss("label '" word-slice/EDX "' is in segment '" current-segment-name "'.")
+    # . . push args
+    68/push  "'."/imm32
+    56/push-ESI
+    68/push  "' is in segment '"/imm32
+    52/push-EDX
+    68/push  "label '"/imm32
+    # . . call
+    e8/call  trace-slsss/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # x->segment-offset = segment-offset
+    # . EBX = segment-offset
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   compute-offsets:segment-offset/disp32  # copy *segment-offset to EBX
+    # . x->segment-offset = EBX
+    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           3/r32/EBX   4/disp8         .                 # copy EBX to *(EAX+4)
+    # trace-slsns("label '" word-slice/EDX "' is at segment offset " *segment-offset/EAX ".")
+    # . . EAX = file-offset
+    b8/copy-to-EAX compute-offsets:segment-offset/imm32
+    # . . EAX = *file-offset/EAX
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # copy *EAX to EAX
+    # . . push args
+    68/push  "."/imm32
+    50/push-EAX
+    68/push  "' is at segment offset "/imm32
+    52/push-EDX
+    68/push  "label '"/imm32
+    # . . call
+    e8/call  trace-slsns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # continue
+    e9/jump  $compute-offsets:word-loop/disp32
+$compute-offsets:case-default:
+    # width/EAX = compute-width-of-slice(word-slice)
+    # . . push args
+    52/push-EDX
+    # . . call
+    e8/call compute-width-of-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # segment-offset += width
+    01/add                          0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   compute-offsets:segment-offset/disp32 # add EAX to *segment-offset
+    # file-offset += width
+    01/add                          0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   compute-offsets:file-offset/disp32 # add EAX to *file-offset
+#?     # dump segment-offset {{{
+#?     # . write(2/stderr, "segment-offset: ")
+#?     # . . push args
+#?     68/push  "segment-offset: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . clear-stream(Stderr+4)
+#?     # . . save EAX
+#?     50/push-EAX
+#?     # . . push args
+#?     b8/copy-to-EAX  Stderr/imm32
+#?     05/add-to-EAX  4/imm32
+#?     50/push-EAX
+#?     # . . call
+#?     e8/call  clear-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . . restore EAX
+#?     58/pop-to-EAX
+#?     # . print-int32-buffered(Stderr, segment-offset)
+#?     # . . push args
+#?     52/push-EDX
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           compute-offsets:segment-offset/disp32  # push *segment-offset
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  print-int32-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "\n")
+#?     # . . push args
+#?     68/push  "\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    e9/jump $compute-offsets:word-loop/disp32
+$compute-offsets:break-line-loop:
+    # seg/EAX = get-or-insert(segments, curr-segment-name, row-size=16)
+    # . . push args
+    68/push  0x10/imm32/row-size
+    56/push-ESI
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  get-or-insert/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # seg->size = file-offset - seg->file-offset
+    # . save ECX
+    51/push-ECX
+    # . EBX = *file-offset
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   compute-offsets:file-offset/disp32 # copy *file-offset to EBX
+    # . ECX = seg->file-offset
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           1/r32/ECX   4/disp8         .                 # copy *(EAX+4) to ECX
+    # . EBX -= ECX
+    29/subtract                     3/mod/direct    3/rm32/EBX    .           .             .           1/r32/ECX   .               .                 # subtract ECX from EBX
+    # . seg->size = EBX
+    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           3/r32/EBX   8/disp8         .                 # copy EBX to *(EAX+8)
+    # . restore ECX
+    59/pop-to-ECX
+    # trace-sssns("segment '", curr-segment-name, "' has size ", seg->size, ".")
+    # . trace-sssns("segment '", curr-segment-name, "' has size ", EBX, ".")
+    # . . push args
+    68/push  "."/imm32
+    53/push-EBX
+    68/push  "' has size "/imm32
+    56/push-ESI
+    68/push  "segment '"/imm32
+    # . . call
+    e8/call  trace-sssns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+$compute-offsets:end:
+    # . reclaim locals
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+$compute-offsets:abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "'==' must be followed by segment name and segment-start\n"/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+test-compute-offsets:
+    # input:
+    #   == code 0x1
+    #   ab x/imm32
+    #   == data 0x1000
+    #   00
+    #   x:
+    #     34
+    #
+    # trace contains (in any order):
+    #   segment 'code' is at file offset 0x0.
+    #   segment 'code' has size 0x5.
+    #   segment 'data' is at file offset 0x5.
+    #   segment 'data' has size 0x2.
+    #   label 'x' is in segment 'data'.
+    #   label 'x' is at segment offset 0x1.
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-input-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-input-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # var segments/ECX = stream(2 * 16)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x20/imm32        # subtract from ESP
+    68/push  0x20/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # var labels/EDX = stream(2 * 16)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x20/imm32        # subtract from ESP
+    68/push  0x20/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # initialize input
+    # . write(_test-input-stream, "== code 0x1\n")
+    # . . push args
+    68/push  "== code 0x1\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "ab x/imm32\n")
+    # . . push args
+    68/push  "ab x/imm32\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "== data 0x1000\n")
+    # . . push args
+    68/push  "== data 0x1000\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "00\n")
+    # . . push args
+    68/push  "00\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "x:\n")
+    # . . push args
+    68/push  "x:\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "34\n")
+    # . . push args
+    68/push  "34\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # compute-offsets(_test-input-buffered-file, segments, labels)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    68/push  _test-input-buffered-file/imm32
+    # . . call
+    e8/call  compute-offsets/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32        # add to ESP
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check trace
+    # . check-trace-contains("segment 'code' is at file offset 0x00000000.", msg)
+    # . . push args
+    68/push  "F - test-compute-offsets/0"/imm32
+    68/push  "segment 'code' is at file offset 0x00000000."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("segment 'code' has size 0x00000005", msg)
+    # . . push args
+    68/push  "F - test-compute-offsets/1"/imm32
+    68/push  "segment 'code' has size 0x00000005."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("segment 'data' is at file offset 0x00000005.", msg)
+    # . . push args
+    68/push  "F - test-compute-offsets/2"/imm32
+    68/push  "segment 'data' is at file offset 0x00000005."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("segment 'data' has size 0x00000002.", msg)
+    # . . push args
+    68/push  "F - test-compute-offsets/3"/imm32
+    68/push  "segment 'data' has size 0x00000002."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("label 'x' is in segment 'data'.", msg)
+    # . . push args
+    68/push  "F - test-compute-offsets/4"/imm32
+    68/push  "label 'x' is in segment 'data'."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("label 'x' is at segment offset 0x00000001.", msg)
+    # . . push args
+    68/push  "F - test-compute-offsets/5"/imm32
+    68/push  "label 'x' is at segment offset 0x00000001."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-ints-equal(labels->write, 0x10, msg)
+    # . . push args
+    68/push  "F - test-compute-offsets-maintains-labels-write-index"/imm32
+    68/push  0x10/imm32/1-entry
+    ff          6/subop/push        0/mod/indirect  2/rm32/EDX    .           .             .           .           .               .                 # push *EDX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+compute-addresses:  # segments : (address stream {string, segment-info}), labels : (address stream {string, label-info})
+    # pseudocode:
+    #   srow : (address segment-info) = segments->data
+    #   max = segments->data + segments->write
+    #   num-segments = segments->write / 16
+    #   starting-offset = 0x34 + (num-segments * 0x20)
+    #   while true
+    #     if (srow >= max) break
+    #     s->file-offset += starting-offset
+    #     s->address &= 0xfffff000  # clear last 12 bits for p_align
+    #     s->address += (s->file-offset & 0x00000fff)
+    #     trace-sssns("segment " s->key " starts at address " s->address)
+    #     srow += 16  # row-size
+    #   lrow : (address label-info) = labels->data
+    #   max = labels->data + labels->write
+    #   while true
+    #     if (lrow >= max) break
+    #     seg-name : (address string) = lrow->segment-name
+    #     label-seg : (address segment-info) = get(segments, seg-name, row-size=16)
+    #     lrow->address = label-seg->address + lrow->segment-offset
+    #     trace-sssns("label " lrow->key " is at address " lrow->address)
+    #     lrow += 16  # row-size
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    56/push-ESI
+    57/push-EDI
+    # ESI = segments
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # starting-offset/EDI = 0x34 + (num-segments * 0x20)  # make room for ELF headers
+    # . EDI = segments->write / 16 (row-size)
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           7/r32/EDI   .               .                 # copy *ESI to EDI
+    c1/shift    5/subop/logic-right 3/mod/direct    7/rm32/EDI    .           .             .           .           .               4/imm8            # shift EDI right by 4 bits, while padding zeroes
+    # . EDI = (EDI * 0x20) + 0x34
+    c1/shift    4/subop/left        3/mod/direct    7/rm32/EDI    .           .             .           .           .               5/imm8            # shift EDI left by 5 bits
+    81          0/subop/add         3/mod/direct    7/rm32/EDI    .           .             .           .           .               0x34/imm32        # add to EDI
+    # srow/EAX = segments->data
+    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy ESI+12 to EAX
+    # max/ECX = segments->data + segments->write
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
+    01/add                          3/mod/direct    1/rm32/ECX    .           .             .           6/r32/ESI   .               .                 # add ESI to ECX
+$compute-addresses:segment-loop:
+    # if (srow >= max) break
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare EAX with ECX
+    73/jump-if-greater-or-equal-unsigned  $compute-addresses:segment-break/disp8
+    # srow->file-offset += starting-offset
+    01/add                          1/mod/*+disp8   0/rm32/EAX    .           .             .           7/r32/EDI   8/disp8         .                 # add EDI to *(EAX+8)
+    # clear last 12 bits of srow->address for p_align=0x1000
+    # . EDX = srow->address
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           2/r32/EDX   4/disp8         .                 # copy *(EAX+4) to EDX
+    # . EDX &= 0xfffff000
+    81          4/subop/and         3/mod/direct    2/rm32/EDX    .           .             .           .           .               0xfffff000/imm32  # bitwise and of EDX
+    # update last 12 bits from srow->file-offset
+    # . EBX = srow->file-offset
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           3/r32/EBX   8/disp8         .                 # copy *(EAX+8) to EBX
+    # . EBX &= 0xfff
+    81          4/subop/and         3/mod/direct    3/rm32/EBX    .           .             .           .           .               0x00000fff/imm32  # bitwise and of EBX
+    # . srow->address = EDX | EBX
+    09/or                           3/mod/direct    2/rm32/EDX    .           .             .           3/r32/EBX   .               .                 # EDX = bitwise OR with EBX
+    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           2/r32/EDX   4/disp8         .                 # copy EDX to *(EAX+4)
+    # trace-sssns("segment " srow " starts at address " srow->address ".")
+    # . . push args
+    68/push  "."/imm32
+    52/push-EDX
+    68/push  "' starts at address "/imm32
+    ff          6/subop/push        0/mod/indirect  0/rm32/EAX    .           .             .           .           .               .                 # push *EAX
+    68/push  "segment '"/imm32
+    # . . call
+    e8/call  trace-sssns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # srow += 16  # size of row
+    05/add-to-EAX  0x10/imm32
+    eb/jump  $compute-addresses:segment-loop/disp8
+$compute-addresses:segment-break:
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # ESI = labels
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
+    # lrow/EAX = labels->data
+    8d/copy-address                 1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy ESI+12 to EAX
+    # max/ECX = labels->data + labels->write
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           1/r32/ECX   .               .                 # copy *ESI to ECX
+    01/add                          3/mod/direct    1/rm32/ECX    .           .             .           6/r32/ESI   .               .                 # add ESI to ECX
+$compute-addresses:label-loop:
+    # if (lrow >= max) break
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare EAX with ECX
+    0f 83/jump-if-greater-or-equal-unsigned  $compute-addresses:end/disp32
+#?     # dump lrow->key {{{
+#?     # . write(2/stderr, "label: ")
+#?     # . . push args
+#?     68/push  "label: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, lrow->key)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  0/rm32/EAX    .           .             .           .           .               .                 # push *EAX
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # seg-name/EDX = lrow->segment-name
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           2/r32/EDX   4/disp8         .                 # copy *EAX to EDX
+#?     # dump seg-name {{{
+#?     # . write(2/stderr, "compute-addresses: seg-name: ")
+#?     # . . push args
+#?     68/push  "seg-name: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, seg-name)
+#?     # . . push args
+#?     52/push-EDX
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # label-seg/EDX : (address segment-info) = get(segments, seg-name, row-size=16)
+    # . save EAX
+    50/push-EAX
+    # . EAX = get(segments, seg-name, row-size=16)
+    # . . push args
+    68/push  0x10/imm32/row-size
+    52/push-EDX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  get/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . EDX = EAX
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EDX
+    # . restore EAX
+    58/pop-to-EAX
+    # EBX = label-seg->address
+    8b/copy                         0/mod/indirect  2/rm32/EDX    .           .             .           3/r32/EBX   .               .                 # copy *EDX to EBX
+    # EBX += lrow->segment-offset
+    03/add                          1/mod/*+disp8   0/rm32/EAX    .           .             .           3/r32/EBX   8/disp8         .                 # add *(EAX+8) to EBX
+    # lrow->address = EBX
+    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           3/r32/EBX   0xc/disp8       .                 # copy EBX to *(EAX+12)
+    # trace-sssns("label " lrow->key " is at address " lrow->address ".")
+    # . . push args
+    68/push  "."/imm32
+    53/push-EBX
+    68/push  "' is at address "/imm32
+    ff          6/subop/push        0/mod/indirect  0/rm32/EAX    .           .             .           .           .               .                 # push *EAX
+    68/push  "label '"/imm32
+    # . . call
+    e8/call  trace-sssns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # lrow += 16  # size of row
+    05/add-to-EAX  0x10/imm32
+    e9/jump  $compute-addresses:label-loop/disp32
+$compute-addresses:end:
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-compute-addresses:
+    # input:
+    #   segments:
+    #     - 'a': {0x1000, 0, 5}
+    #     - 'b': {0x2018, 5, 1}
+    #     - 'c': {0x5444, 6, 12}
+    #   labels:
+    #     - 'l1': {'a', 3, 0}
+    #     - 'l2': {'b', 0, 0}
+    #
+    # trace contains in any order (comments in parens):
+    #   segment 'a' starts at address 0x00001094.  (0x34 + 0x20 for each segment)
+    #   segment 'b' starts at address 0x00002099.  (0x018 discarded)
+    #   segment 'c' starts at address 0x0000509a.  (0x444 discarded)
+    #   label 'l1' is at address 0x00001097.       (0x1094 + segment-offset 3)
+    #   label 'l2' is at address 0x00002099.       (0x2099 + segment-offset 0)
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . var segments/ECX = stream(10 * 16)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xa0/imm32        # subtract from ESP
+    68/push  0xa0/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # . var labels/EDX = stream(512 * 16)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x2000/imm32      # subtract from ESP
+    68/push  0x2000/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # . stream-add4(segments, "a", 0x1000, 0, 5)
+    68/push  5/imm32/segment-size
+    68/push  0/imm32/file-offset
+    68/push  0x1000/imm32/start-address
+    68/push  "a"/imm32/segment-name
+    51/push-ECX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # . stream-add4(segments, "b", 0x2018, 5, 1)
+    68/push  1/imm32/segment-size
+    68/push  5/imm32/file-offset
+    68/push  0x2018/imm32/start-address
+    68/push  "b"/imm32/segment-name
+    51/push-ECX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # . stream-add4(segments, "c", 0x5444, 6, 12)
+    68/push  0xc/imm32/segment-size
+    68/push  6/imm32/file-offset
+    68/push  0x5444/imm32/start-address
+    68/push  "c"/imm32/segment-name
+    51/push-ECX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # . stream-add4(labels, "l1", "a", 3, 0)
+    68/push  0/imm32/label-address
+    68/push  3/imm32/segment-offset
+    68/push  "a"/imm32/segment-name
+    68/push  "l1"/imm32/label-name
+    52/push-EDX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # . stream-add4(labels, "l2", "b", 0, 0)
+    68/push  0/imm32/label-address
+    68/push  0/imm32/segment-offset
+    68/push  "b"/imm32/segment-name
+    68/push  "l2"/imm32/label-name
+    52/push-EDX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # component under test
+    # . compute-addresses(segments, labels)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  compute-addresses/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # checks
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # . check-trace-contains("segment 'a' starts at address 0x00001094.", msg)
+    # . . push args
+    68/push  "F - test-compute-addresses/0"/imm32
+    68/push  "segment 'a' starts at address 0x00001094."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("segment 'b' starts at address 0x00002099.", msg)
+    # . . push args
+    68/push  "F - test-compute-addresses/1"/imm32
+    68/push  "segment 'b' starts at address 0x00002099."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("segment 'c' starts at address 0x0000509a.", msg)
+    # . . push args
+    68/push  "F - test-compute-addresses/2"/imm32
+    68/push  "segment 'c' starts at address 0x0000509a."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("label 'l1' is at address 0x00001097.", msg)
+    # . . push args
+    68/push  "F - test-compute-addresses/3"/imm32
+    68/push  "label 'l1' is at address 0x00001097."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-trace-contains("label 'l2' is at address 0x00002099.", msg)
+    # . . push args
+    68/push  "F - test-compute-addresses/4"/imm32
+    68/push  "label 'l2' is at address 0x00002099."/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . check-ints-equal(labels->write, 0x20, msg)
+    # . . push args
+    68/push  "F - test-compute-addresses-maintains-labels-write-index"/imm32
+    68/push  0x20/imm32/2-entries
+    ff          6/subop/push        0/mod/indirect  2/rm32/EDX    .           .             .           .           .               .                 # push *EDX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+emit-output:  # in : (address buffered-file), out : (address buffered-file), segments : (address stream {string, segment-info}), labels : (address stream {string, label-info})
+    # pseudocode:
+    #   emit-headers(out, segments, labels)
+    #   emit-segments(in, out, segments, labels)
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+#?     # write(2/stderr, "emit-headers\n") {{{
+#?     # . . push args
+#?     68/push  "emit-headers\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # emit-headers(out, segments, labels)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8       .                # push *(EBP+20)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8       .                # push *(EBP+16)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8        .                # push *(EBP+12)
+    # . . call
+    e8/call  emit-headers/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+#?     # write(2/stderr, "emit-segments\n") {{{
+#?     # . . push args
+#?     68/push  "emit-segments\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # emit-segments(in, out, segments, labels)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8       .                # push *(EBP+20)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8       .                # push *(EBP+16)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  emit-segments/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+$emit-output:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+emit-segments:  # in : (address buffered-file), out : (address buffered-file), segments : (address stream {string, segment-info}), labels : (address stream {string, label-info})
+    # pseudocode:
+    #   var offset-of-next-instruction = 0
+    #   var line = new-stream(512, 1)
+    #   line-loop:
+    #   while true
+    #     clear-stream(line)
+    #     read-line-buffered(in, line)
+    #     if (line->write == 0) break               # end of file
+    #     offset-of-next-instruction += num-bytes(line)
+    #     while true
+    #       var word-slice = next-word(line)
+    #       if slice-empty?(word-slice)             # end of line
+    #         break
+    #       if slice-starts-with?(word-slice, "#")  # comment
+    #         break
+    #       if is-label?(word-slice)                # no need for label declarations anymore
+    #         goto line-loop                        # don't insert empty lines
+    #       if slice-equal?(word-slice, "==")       # no need for segment header lines
+    #         goto line-loop                        # don't insert empty lines
+    #       if length(word-slice) == 2
+    #         write-slice-buffered(out, word-slice)
+    #         write-buffered(out, " ")
+    #         continue
+    #       datum = next-token-from-slice(word-slice->start, word-slice->end, "/")
+    #       info = get-slice(labels, datum)
+    #       if has-metadata?(word-slice, "imm8")
+    #         abort  # label should never go to imm8
+    #       else if has-metadata?(word-slice, "imm32")
+    #         emit(out, info->address, 4)
+    #       else if has-metadata?(word-slice, "disp8")
+    #         value = info->offset - offset-of-next-instruction
+    #         emit(out, value, 1)
+    #       else if has-metadata?(word-slice, "disp32")
+    #         value = info->offset - offset-of-next-instruction
+    #         emit(out, value, 4)
+    #       else
+    #         abort
+    #     write-buffered(out, "\n")
+    #
+    # registers:
+    #   line: ECX
+    #   word-slice: EDX
+    #   offset-of-next-instruction: EBX
+    #   datum: EDI
+    #   info: ESI (inner loop only)
+    #   temporaries: EAX, ESI (outer loop)
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    56/push-ESI
+    57/push-EDI
+    # var line/ECX : (address stream byte) = stream(512)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x200/imm32       # subtract from ESP
+    68/push  0x200/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # var word-slice/EDX = {0, 0}
+    68/push  0/imm32/end
+    68/push  0/imm32/start
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # var datum/EDI = {0, 0}
+    68/push  0/imm32/end
+    68/push  0/imm32/start
+    89/copy                         3/mod/direct    7/rm32/EDI    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDI
+    # offset-of-next-instruction/EBX = 0
+    31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
+$emit-segments:line-loop:
+    # clear-stream(line)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # read-line-buffered(in, line)
+    # . . push args
+    51/push-ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  read-line-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # dump line {{{
+#?     # . write(2/stderr, "LL: ")
+#?     # . . push args
+#?     68/push  "LL: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # write-stream(2/stderr, line)
+#?     # . . push args
+#?     51/push-ECX
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . rewind-stream(line)
+#?     # . . push args
+#?     51/push-ECX
+#?     # . . call
+#?     e8/call  rewind-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # }}}
+$emit-segments:check0:
+    # if (line->write == 0) break
+    81          7/subop/compare     0/mod/indirect  1/rm32/ECX    .           .             .           .           .               0/imm32           # compare *ECX
+    0f 84/jump-if-equal  $emit-segments:end/disp32
+    # offset-of-next-instruction += num-bytes(line)
+    # . EAX = num-bytes(line)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  num-bytes/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . EBX = EAX
+    89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EBX
+$emit-segments:word-loop:
+    # next-word(line, word-slice)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    # . . call
+    e8/call  next-word/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # dump word-slice {{{
+#?     # . write(2/stderr, "w: ")
+#?     # . . push args
+#?     68/push  "w: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-slice-buffered(Stderr, word-slice)
+#?     # . . push args
+#?     52/push-EDX
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  write-slice-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+$emit-segments:check1:
+    # if (slice-empty?(word-slice)) break
+    # . EAX = slice-empty?(word-slice)
+    # . . push args
+    52/push-EDX
+    # . . call
+    e8/call  slice-empty?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . if (EAX != 0) break
+    3d/compare-EAX-and  0/imm32
+    0f 85/jump-if-not-equal  $emit-segments:next-line/disp32
+$emit-segments:check-for-comment:
+    # if (slice-starts-with?(word-slice, "#")) break
+    # . start/ESI = word-slice->start
+    8b/copy                         0/mod/indirect  2/rm32/EDX    .           .             .           6/r32/ESI   .               .                 # copy *EDX to ESI
+    # . c/EAX = *start
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    8a/copy-byte                    0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/AL    .               .                 # copy byte at *ESI to AL
+    # . if (EAX == '#') break
+    3d/compare-EAX-and  0x23/imm32/hash
+    0f 84/jump-if-equal  $emit-segments:next-line/disp32
+$emit-segments:check-for-label:
+    # if is-label?(word-slice) break
+    # . EAX = is-label?(word-slice)
+    # . . push args
+    52/push-EDX
+    # . . call
+    e8/call  is-label?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . if (EAX != 0) break
+    3d/compare-EAX-and  0/imm32
+    0f 85/jump-if-not-equal  $emit-segments:line-loop/disp32
+$emit-segments:check-for-segment-header:
+    # if (slice-equal?(word-slice, "==")) break
+    # . EAX = slice-equal?(word-slice, "==")
+    # . . push args
+    68/push  "=="/imm32
+    52/push-EDX
+    # . . call
+    e8/call  slice-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) break
+    3d/compare-EAX-and  0/imm32
+    0f 85/jump-if-not-equal  $emit-segments:line-loop/disp32
+$emit-segments:2-character:
+    # if (length(word-slice) != 2) goto next check
+    # . EAX = length(word-slice)
+    8b/copy                         1/mod/*+disp8   2/rm32/EDX    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(EDX+4) to EAX
+    2b/subtract                     0/mod/indirect  2/rm32/EDX    .           .             .           0/r32/EAX   .               .                 # subtract *EDX from EAX
+    # . if (EAX != 2) goto next check
+    3d/compare-EAX-and  2/imm32
+    75/jump-if-not-equal  $emit-segments:check-metadata/disp8
+    # write-slice-buffered(out, word-slice)
+    # . . push args
+    52/push-EDX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  write-slice-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write-buffered(out, " ")
+    # . . push args
+    68/push  " "/imm32
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  write-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # continue
+    e9/jump  $emit-segments:word-loop/disp32
+$emit-segments:check-metadata:
+    # - if we get here, 'word-slice' must be a label to be looked up
+    # datum/EDI = next-token-from-slice(word-slice->start, word-slice->end, "/")
+    # . . push args
+    57/push-EDI
+    68/push  0x2f/imm32/slash
+    ff          6/subop/push        1/mod/*+disp8   2/rm32/EDX    .           .             .           .           4/disp8         .                 # push *(EDX+4)
+    ff          6/subop/push        0/mod/indirect  2/rm32/EDX    .           .             .           .           .               .                 # push *EDX
+    # . . call
+    e8/call  next-token-from-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+#?     # dump word-slice {{{
+#?     # . write(2/stderr, "datum: ")
+#?     # . . push args
+#?     68/push  "datum: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-slice-buffered(Stderr, word-slice)
+#?     # . . push args
+#?     57/push-EDI
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  write-slice-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # info/ESI = get-slice(labels, datum, row-size=16)
+    # . EAX = get-slice(labels, datum, row-size=16)
+    # . . push args
+    68/push  0x10/imm32/row-size
+    57/push-EDI
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8      .                 # push *(EBP+20)
+    # . . call
+    e8/call  get-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . ESI = EAX
+    89/copy                         3/mod/direct    6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy EAX to ESI
+$emit-segments:check-for-imm8:
+    # if (has-metadata?(word-slice, "imm8")) abort
+    # . EAX = has-metadata?(EDX, "imm8")
+    # . . push args
+    68/push  "imm8"/imm32
+    52/push-EDX
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) abort
+    3d/compare-EAX-and  0/imm32
+    0f 85/jump-if-not-equal  $emit-segments:imm8-abort/disp32
+$emit-segments:check-for-imm32:
+    # if (!has-metadata?(word-slice, "imm32")) goto next check
+    # . EAX = has-metadata?(EDX, "imm32")
+    # . . push args
+    68/push  "imm32"/imm32
+    52/push-EDX
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX == 0) goto next check
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $emit-segments:check-for-disp8/disp8
+#?     # dump info->address {{{
+#?     # . write(2/stderr, "info->address: ")
+#?     # . . push args
+#?     68/push  "info->address: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . print-int32-buffered(Stderr, info->address)
+#?     # . . push args
+#?     ff          6/subop/push        1/mod/*+disp8   6/rm32/ESI    .           .             .           .           8/disp8         .                 # push *(ESI+8)
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  print-int32-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # emit-hex(out, info->address, 4)
+    # . . push args
+    68/push  4/imm32
+    ff          6/subop/push        1/mod/*+disp8   6/rm32/ESI    .           .             .           .           8/disp8         .                 # push *(ESI+8)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # continue
+    e9/jump  $emit-segments:word-loop/disp32
+$emit-segments:check-for-disp8:
+    # if (!has-metadata?(word-slice, "disp8")) goto next check
+    # . EAX = has-metadata?(EDX, "disp8")
+    # . . push args
+    68/push  "disp8"/imm32
+    52/push-EDX
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX == 0) goto next check
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $emit-segments:check-for-disp32/disp8
+    # emit-hex(out, info->offset - offset-of-next-instruction, 1)
+    # . . push args
+    68/push  1/imm32
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # subtract EBX from EAX
+    50/push-EAX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # continue
+    e9/jump  $emit-segments:word-loop/disp32
+$emit-segments:check-for-disp32:
+    # if (!has-metadata?(word-slice, "disp32")) abort
+    # . EAX = has-metadata?(EDX, "disp32")
+    # . . push args
+    68/push  "disp32"/imm32
+    52/push-EDX
+    # . . call
+    e8/call  has-metadata?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX == 0) abort
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $emit-segments:abort/disp8
+    # emit-hex(out, info->offset - offset-of-next-instruction, 4)
+    # . . push args
+    68/push  4/imm32
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
+    29/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # subtract EBX from EAX
+    50/push-EAX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  emit-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # continue
+    e9/jump  $emit-segments:word-loop/disp32
+$emit-segments:next-line:
+    # write-buffered(out, "\n")
+    # . . push args
+    68/push  Newline/imm32
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    # . . call
+    e8/call  write-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # loop
+    e9/jump  $emit-segments:line-loop/disp32
+$emit-segments:end:
+    # . reclaim locals
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x21c/imm32       # add to ESP
+    # . restore registers
+    5f/pop-to-EDI
+    5e/pop-to-ESI
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+$emit-segments:imm8-abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "emit-segments: unexpected /imm8"/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+$emit-segments:abort:
+    # print(stderr, "missing metadata in " word-slice)
+    # . _write(2/stderr, "missing metadata in word ")
+    # . . push args
+    68/push  "emit-segments: missing metadata in "/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write-slice-buffered(Stderr, word-slice)
+    # . . push args
+    52/push-EDX
+    68/push  Stderr/imm32
+    # . . call
+    e8/call  write-slice-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . flush(Stderr)
+    # . . push args
+    68/push  Stderr/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+test-emit-segments:
+    # input:
+    #   in:
+    #     == code 0x1000
+    #     ab cd ef gh
+    #     ij x/imm32
+    #     == data 0x2000
+    #     00
+    #     x:
+    #       34
+    #   segments:
+    #     - 'code': {0x1074, 0, 5}
+    #     - 'data': {0x2079, 5, 1}
+    #   labels:
+    #     - 'l1': {'code', 3, 0x1077}
+    #     - 'x': {'data', 1, 0x207a}
+    #
+    # output:
+    #   ab cd ef gh
+    #   ij 7a 20 00 00
+    #   00
+    #   34
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-input-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-input-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . var segments/ECX = stream(10 * 16)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xa0/imm32        # subtract from ESP
+    68/push  0xa0/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # . var labels/EDX = stream(512 * 16)
+    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x2000/imm32      # subtract from ESP
+    68/push  0x2000/imm32/length
+    68/push  0/imm32/read
+    68/push  0/imm32/write
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # initialize input
+    # . write(_test-input-stream, "== code 0x1000\n")
+    # . . push args
+    68/push  "== code 0x1000\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "ab cd ef gh\n")
+    # . . push args
+    68/push  "ab cd ef gh\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "ij x/imm32\n")
+    # . . push args
+    68/push  "ij x/imm32\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "== data 0x2000\n")
+    # . . push args
+    68/push  "== data 0x2000\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "00\n")
+    # . . push args
+    68/push  "00\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "x:\n")
+    # . . push args
+    68/push  "x:\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . write(_test-input-stream, "34\n")
+    # . . push args
+    68/push  "34\n"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . stream-add4(segments, "code", 0x1074, 0, 5)
+    68/push  5/imm32/segment-size
+    68/push  0/imm32/file-offset
+    68/push  0x1074/imm32/start-address
+    68/push  "code"/imm32/segment-name
+    51/push-ECX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # . stream-add4(segments, "data", 0x2079, 5, 1)
+    68/push  1/imm32/segment-size
+    68/push  5/imm32/file-offset
+    68/push  0x2079/imm32/start-address
+    68/push  "data"/imm32/segment-name
+    51/push-ECX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # . stream-add4(labels, "l1", "code", 3, 0x1077)
+    68/push  0x1077/imm32/label-address
+    68/push  3/imm32/segment-offset
+    68/push  "code"/imm32/segment-name
+    68/push  "l1"/imm32/label-name
+    52/push-EDX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # . stream-add4(labels, "x", "data", 1, 0x207a)
+    68/push  0x207a/imm32/label-address
+    68/push  1/imm32/segment-offset
+    68/push  "data"/imm32/segment-name
+    68/push  "x"/imm32/label-name
+    52/push-EDX
+    # . . call
+    e8/call  stream-add4/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+    # component under test
+    # . emit-segments(_test-input-buffered-file, _test-output-buffered-file, segments, labels)
+    # . . push args
+    52/push-EDX
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    68/push  _test-input-buffered-file/imm32
+    # . . call
+    e8/call  emit-segments/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x10/imm32        # add to ESP
+    # checks
+#?     # dump output {{{
+#?     # . write(2/stderr, "result: ^")
+#?     # . . push args
+#?     68/push  "result: ^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, _test-output-stream)
+#?     # . . push args
+#?     68/push  _test-output-stream/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . rewind-stream(_test-output-stream)
+#?     # . . push args
+#?     68/push  _test-output-stream/imm32
+#?     # . . call
+#?     e8/call  rewind-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # }}}
+    # . check-next-stream-line-equal(_test-output-stream, "ab cd ef gh ", msg)
+    # . . push args
+    68/push  "F - test-emit-segments/0"/imm32
+    68/push  "ab cd ef gh "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-next-stream-line-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . check-next-stream-line-equal(_test-output-stream, "ij 7a 20 00 00 ", msg)
+    # . . push args
+    68/push  "F - test-emit-segments/1"/imm32
+    68/push  "ij 7a 20 00 00 "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-next-stream-line-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . check-next-stream-line-equal(_test-output-stream, "00 ", msg)
+    # . . push args
+    68/push  "F - test-emit-segments/2"/imm32
+    68/push  "00 "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-next-stream-line-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . check-next-stream-line-equal(_test-output-stream, "34 ", msg)
+    # . . push args
+    68/push  "F - test-emit-segments/3"/imm32
+    68/push  "34 "/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-next-stream-line-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+emit-headers:  # out : (address buffered-file), segments : (address stream {string, segment-info}), labels : (address stream {string, label-info})
+    # pseudocode:
+    #   emit-elf-header(out, segments, labels)
+    #   curr-segment = segments->data
+    #   max = segments->data + segments->write
+    #   while true
+    #     if (curr-segment >= max) break
+    #     emit-elf-program-header-entry(out, curr-segment)
+    #     curr-segment += 16                        # size of a row
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+#?     # write(2/stderr, "emit-elf-header\n") {{{
+#?     # . . push args
+#?     68/push  "emit-elf-header\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # emit-elf-header(out, segments, labels)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  emit-elf-header/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # EAX = segments
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy *(EBP+12) to EAX
+    # ECX = segments->write
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    # curr-segment/EAX = segments->data
+    8d/copy-address                 1/mod/*+disp8   0/rm32/EAX    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy EAX+12 to EAX
+    # max/ECX = segments->data + segments->write
+    01/add                          3/mod/direct    1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # add EAX to ECX
+$emit-headers:loop:
+    # if (curr-segment >= max) break
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # compare EAX with ECX
+    0f 83/jump-if-greater-or-equal-unsigned  $emit-headers:end/disp32
+#?     # dump curr-segment->name {{{
+#?     # . write(2/stderr, "about to emit ph entry: segment->name: ")
+#?     # . . push args
+#?     68/push  "about to emit ph entry: segment->name: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . clear-stream(Stderr+4)
+#?     # . . save EAX
+#?     50/push-EAX
+#?     # . . push args
+#?     b8/copy-to-EAX  Stderr/imm32
+#?     05/add-to-EAX  4/imm32
+#?     50/push-EAX
+#?     # . . call
+#?     e8/call  clear-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . . restore EAX
+#?     58/pop-to-EAX
+#?     # . print-int32-buffered(Stderr, &curr-segment)
+#?     # . . push args
+#?     50/push-EAX
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  print-int32-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, " -> ")
+#?     # . . push args
+#?     68/push  " -> "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . print-int32-buffered(Stderr, curr-segment->name)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  0/rm32/EAX    .           .             .           .           .               .                 # push *EAX
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  print-int32-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "\n")
+#?     # . . push args
+#?     68/push  "\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+#?     # write(2/stderr, "emit-segment-header\n") {{{
+#?     # . . push args
+#?     68/push  "emit-segment-header\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # emit-elf-program-header-entry(out, curr-segment)
+    # . . push args
+    50/push-EAX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  emit-elf-program-header-entry/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # curr-segment += 16                        # size of a row
+    81          0/subop/add         3/mod/direct    0/rm32/EAX    .           .             .           .           .               0x10/imm32        # add to EAX
+    e9/jump  $emit-headers:loop/disp32
+$emit-headers:end:
+    # . restore registers
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+emit-elf-header:  # out : (address buffered-file), segments : (address stream {string, segment-info}), labels : (address stream {string, label-info})
+    # pseudocode
+    #   *Elf_e_entry = get(labels, "Entry")->address
+    #   *Elf_e_phnum = segments->write / 20         # size of a row
+    #   emit-hex-array(out, Elf_header)
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX  # just because we need to call idiv
+    # *Elf_e_entry = get(labels, "Entry")->address
+    # . EAX = labels
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0x10/disp8      .                 # copy *(EBP+16) to EAX
+    # . label-info/EAX = get(labels, "Entry", row-size=16)
+    # . . push args
+    68/push  0x10/imm32/row-size
+    68/push  "Entry"/imm32
+    50/push-EAX
+    # . . call
+    e8/call  get/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . EAX = label-info->address
+    8b/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           0/r32/EAX   8/disp8         .                 # copy *(EAX+8) to EAX
+    # . *Elf_e_entry = EAX
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Elf_e_entry/disp32                # copy EAX to *Elf_e_entry
+    # *Elf_e_phnum = segments->write / 0x20
+    # . EAX = segments
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy *(EBP+12) to EAX
+    # . len/EAX = segments->write
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # copy *EAX to EAX
+    # . EAX = len / 0x20  (destroying EDX)
+    b9/copy-to-ECX  0x20/imm32
+    31/xor                          3/mod/direct    2/rm32/EDX    .           .             .           2/r32/EDX   .               .                 # clear EDX
+    f7          7/subop/idiv        3/mod/direct    1/rm32/ECX    .           .             .           .           .               .                 # divide EDX:EAX by ECX, storing quotient in EAX and remainder in EDX
+    # . *Elf_e_phnum = EAX
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Elf_e_phnum/disp32                # copy EAX to *Elf_e_phnum
+    # emit-hex-array(out, Elf_header)
+    # . . push args
+    68/push  Elf_header/imm32
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  emit-hex-array/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+$emit-elf-header:end:
+    # . restore registers
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+emit-elf-program-header-entry:  # out : (address buffered-file), curr-segment : (address {string, segment-info})
+    # pseudocode:
+    #   *Elf_p_offset = curr-segment->file-offset
+    #   *Elf_p_vaddr = curr-segment->address
+    #   *Elf_p_paddr = curr-segment->address
+    #   *Elf_p_filesz = curr-segment->size
+    #   *Elf_p_memsz = curr-segment->size
+    #   if curr-segment->name == "code"
+    #     *Elf_p_flags = 5  # r-x
+    #   else
+    #     *Elf_p_flags = 6  # rw-
+    #   emit-hex-array(out, Elf_program_header_entry)
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    56/push-ESI
+    # ESI = curr-segment
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
+    # *Elf_p_offset = curr-segment->file-offset
+    # . EAX = curr-segment->file-offset
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   8/disp8         .                 # copy *(ESI+8) to EAX
+    # . *Elf_p_offset = EAX
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Elf_p_offset/disp32               # copy EAX to *Elf_p_offset
+    # *Elf_p_vaddr = curr-segment->address
+    # . EAX = curr-segment->address
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # copy *(ESI+4) to EAX
+    # . *Elf_p_vaddr = EAX
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Elf_p_vaddr/disp32                # copy EAX to *Elf_p_vaddr
+    # *Elf_p_paddr = curr-segment->address
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Elf_p_paddr/disp32                # copy EAX to *Elf_p_paddr
+    # *Elf_p_filesz = curr-segment->size
+    # . EAX = curr-segment->size
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy *(ESI+12) to EAX
+    # . *Elf_p_filesz = EAX
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Elf_p_filesz/disp32               # copy EAX to *Elf_p_filesz
+    # *Elf_p_memsz = curr-segment->size
+    89/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Elf_p_memsz/disp32                # copy EAX to *Elf_p_memsz
+    # if (!string-equal?(curr-segment->name, "code") goto next check
+    # . EAX = curr-segment->name
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
+    # . EAX = string-equal?(curr-segment->name, "code")
+    # . . push args
+    68/push  "code"/imm32
+    50/push-EAX
+    # . . call
+    e8/call  string-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX == 0) goto next check
+    3d/compare-EAX-and  0/imm32
+    74/jump-if-equal  $emit-elf-program-header-entry:data/disp8
+    # *Elf_p_flags = rw-
+    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Elf_p_flags/disp32  6/imm32       # copy to *Elf_p_flags
+$emit-elf-program-header-entry:data:
+    # otherwise *Elf_p_flags = r-x
+    c7          0/subop/copy        0/mod/indirect  5/rm32/.disp32            .             .           .           Elf_p_flags/disp32  5/imm32       # copy to *Elf_p_flags
+    # emit-hex-array(out, Elf_program_header_entry)
+    # . . push args
+    68/push  Elf_program_header_entry/imm32
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  emit-hex-array/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+$emit-elf-program-header-entry:end:
+    # . restore registers
+    5e/pop-to-ESI
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+# - some helpers for tests
+
+stream-add4:  # in : (address stream byte), key : address, val1 : address, val2 : address, val3 : address
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+    56/push-ESI
+    # ESI = in
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           6/r32/ESI   8/disp8         .                 # copy *(EBP+8) to ESI
+    # curr/EAX = in->data + in->write
+    # . EAX = in->write
+    8b/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           0/r32/EAX   .               .                 # copy *ESI to EAX
+    # . EAX = ESI+EAX+12
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  0/index/EAX   .           0/r32/EAX   0xc/disp8       .                 # copy ESI+EAX+12 to EAX
+    # max/EDX = in->data + in->length
+    # . EDX = in->length
+    8b/copy                         1/mod/*+disp8   6/rm32/ESI    .           .             .           2/r32/EDX   8/disp8         .                 # copy *(ESI+8) to EDX
+    # . EDX = ESI+EDX+12
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/ESI  2/index/EDX   .           2/r32/EDX   0xc/disp8       .                 # copy ESI+EDX+12 to EDX
+    # if (curr >= max) abort
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # compare EAX with EDX
+    73/jump-if-greater-or-equal-unsigned  $stream-add4:abort/disp8
+    # *curr = key
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   0xc/disp8       .                 # copy *(EBP+12) to ECX
+    89/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EAX
+    # curr += 4
+    05/add-to-EAX  4/imm32
+    # if (curr >= max) abort
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # compare EAX with EDX
+    73/jump-if-greater-or-equal-unsigned  $stream-add4:abort/disp8
+    # *curr = val1
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   0x10/disp8      .                 # copy *(EBP+16) to ECX
+    89/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EAX
+    # curr += 4
+    05/add-to-EAX  4/imm32
+    # if (curr >= max) abort
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # compare EAX with EDX
+    73/jump-if-greater-or-equal-unsigned  $stream-add4:abort/disp8
+    # *curr = val2
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   0x14/disp8      .                 # copy *(EBP+20) to ECX
+    89/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EAX
+    # curr += 4
+    05/add-to-EAX  4/imm32
+    # if (curr >= max) abort
+    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # compare EAX with EDX
+    73/jump-if-greater-or-equal-unsigned  $stream-add4:abort/disp8
+    # *curr = val3
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .                         1/r32/ECX   0x18/disp8      .                 # copy *(EBP+24) to ECX
+    89/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy ECX to *EAX
+    # in->write += 16
+    81          0/subop/add         0/mod/indirect  6/rm32/ESI    .           .             .           .           .               0x10/imm32        # add to *ESI
+$stream-add4:end:
+    # . restore registers
+    5e/pop-to-ESI
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+$stream-add4:abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "overflow in stream-add4\n"/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
+# some variants of 'trace' that take multiple arguments in different combinations of types:
+#   n: int
+#   c: character [4-bytes, will eventually be UTF-8]
+#   s: (address string)
+#   l: (address slice)
+# one gotcha: 's5' must not be empty
+
+trace-sssns:  # s1 : (address string), s2 : (address string), s3 : (address string), n4 : int, s5 : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # write(*Trace-stream, s1)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write(*Trace-stream, s2)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write(*Trace-stream, s3)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # print-int32(*Trace-stream, n4)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8      .                 # push *(EBP+20)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  print-int32/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # trace(s5)  # implicitly adds a newline and finalizes the trace line
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x18/disp8      .                 # push *(EBP+24)
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+$trace-sssns:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-trace-sssns:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . *Trace-stream->write = 0
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy *Trace-stream to EAX
+    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # clear *EAX
+    # trace-sssns("A" "b" "c " 3 " e")
+    # . . push args
+    68/push  " e"/imm32
+    68/push  3/imm32
+    68/push  "c "/imm32
+    68/push  "b"/imm32
+    68/push  "A"/imm32
+    # . . call
+    e8/call  trace-sssns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check-trace-contains("Abc 0x00000003 e")
+    # . . push args
+    68/push  "F - test-trace-sssns"/imm32
+    68/push  "Abc 0x00000003 e"/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+trace-snsns:  # s1 : (address string), n2 : int, s3 : (address string), n4 : int, s5 : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # write(*Trace-stream, s1)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # print-int32(*Trace-stream, n2)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  print-int32/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write(*Trace-stream, s3)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # print-int32(*Trace-stream, n4)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8      .                 # push *(EBP+20)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  print-int32/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # trace(s5)  # implicitly adds a newline and finalizes the trace line
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x18/disp8      .                 # push *(EBP+24)
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+$trace-snsns:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-trace-snsns:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . *Trace-stream->write = 0
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy *Trace-stream to EAX
+    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # clear *EAX
+    # trace-snsns("A " 2 " c " 3 " e")
+    # . . push args
+    68/push  " e"/imm32
+    68/push  3/imm32
+    68/push  " c "/imm32
+    68/push  2/imm32
+    68/push  "A "/imm32
+    # . . call
+    e8/call  trace-snsns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check-trace-contains("Abc 0x00000003 e")
+    # . . push args
+    68/push  "F - test-trace-snsns"/imm32
+    68/push  "A 0x00000002 c 0x00000003 e"/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+trace-slsls:  # s1 : (address string), l2 : (address slice), s3 : (address string), l4 : (address slice), s5 : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # write(*Trace-stream, s1)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write-slice(*Trace-stream, l2)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write(*Trace-stream, s3)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write-slice(*Trace-stream, l4)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8      .                 # push *(EBP+20)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # trace(s5)  # implicitly adds a newline and finalizes the trace line
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x18/disp8      .                 # push *(EBP+24)
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+$trace-slsls:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-trace-slsls:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . *Trace-stream->write = 0
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy *Trace-stream to EAX
+    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # clear *EAX
+    # (EAX..ECX) = "b"
+    b8/copy-to-EAX  "b"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var b/EBX : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBX
+    # (EAX..ECX) = "d"
+    b8/copy-to-EAX  "d"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var d/EDX : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EDX
+    # trace-slsls("A" b "c" d "e")
+    # . . push args
+    68/push  "e"/imm32
+    52/push-EDX
+    68/push  "c"/imm32
+    53/push-EBX
+    68/push  "A"/imm32
+    # . . call
+    e8/call  trace-slsls/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check-trace-contains("Abcde")
+    # . . push args
+    68/push  "F - test-trace-slsls"/imm32
+    68/push  "Abcde"/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+trace-slsns:  # s1 : (address string), l2 : (address slice), s3 : (address string), n4 : int, s5 : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # write(*Trace-stream, s1)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write-slice(*Trace-stream, l2)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write(*Trace-stream, s3)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # print-int32(*Trace-stream, n4)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8      .                 # push *(EBP+20)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  print-int32/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # trace(s5)  # implicitly adds a newline and finalizes the trace line
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x18/disp8      .                 # push *(EBP+24)
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+$trace-slsns:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-trace-slsns:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . *Trace-stream->write = 0
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy *Trace-stream to EAX
+    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # clear *EAX
+    # (EAX..ECX) = "b"
+    b8/copy-to-EAX  "b"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var b/EBX : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBX
+    # trace-slsls("A" b "c " 3 " e")
+    # . . push args
+    68/push  " e"/imm32
+    68/push  3/imm32
+    68/push  "c "/imm32
+    53/push-EBX
+    68/push  "A"/imm32
+    # . . call
+    e8/call  trace-slsns/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check-trace-contains("Abc 0x00000003 e")
+    # . . push args
+    68/push  "F - test-trace-slsls"/imm32
+    68/push  "Abc 0x00000003 e"/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+trace-slsss:  # s1 : (address string), l2 : (address slice), s3 : (address string), s4 : (address string), s5 : (address string)
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # write(*Trace-stream, s1)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write-slice(*Trace-stream, l2)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write(*Trace-stream, s3)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x10/disp8      .                 # push *(EBP+16)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # write(*Trace-stream, s4)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x14/disp8      .                 # push *(EBP+20)
+    ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # trace(s5)  # implicitly adds a newline and finalizes the trace line
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0x18/disp8      .                 # push *(EBP+24)
+    # . . call
+    e8/call  trace/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+$trace-slsss:end:
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-trace-slsss:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . *Trace-stream->write = 0
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           0/r32/EAX   Trace-stream/disp32               # copy *Trace-stream to EAX
+    c7          0/subop/copy        0/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # clear *EAX
+    # (EAX..ECX) = "b"
+    b8/copy-to-EAX  "b"/imm32
+    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           1/r32/ECX   .               .                 # copy *EAX to ECX
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/EAX  1/index/ECX   .           1/r32/ECX   4/disp8         .                 # copy EAX+ECX+4 to ECX
+    05/add-to-EAX  4/imm32
+    # var b/EBX : (address slice) = {EAX, ECX}
+    51/push-ECX
+    50/push-EAX
+    89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBX
+    # trace-slsss("A" b "c" "d" "e")
+    # . . push args
+    68/push  "e"/imm32
+    68/push  "d"/imm32
+    68/push  "c"/imm32
+    53/push-EBX
+    68/push  "A"/imm32
+    # . . call
+    e8/call  trace-slsss/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0x14/imm32        # add to ESP
+#?     # dump *Trace-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write-stream(2/stderr, *Trace-stream)
+#?     # . . push args
+#?     ff          6/subop/push        0/mod/indirect  5/rm32/.disp32            .             .           .           Trace-stream/disp32               # push *Trace-stream
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check-trace-contains("Abcde")
+    # . . push args
+    68/push  "F - test-trace-slsss"/imm32
+    68/push  "Abcde"/imm32
+    # . . call
+    e8/call  check-trace-contains/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+num-bytes:  # line : (address stream) -> EAX : int
+    # pseudocode:
+    #   result = 0
+    #   while true
+    #     var word-slice = next-word(line)
+    #     if slice-empty?(word-slice)             # end of line
+    #       break
+    #     if slice-starts-with?(word-slice, "#")  # comment
+    #       break
+    #     if is-label?(word-slice)                # no need for label declarations anymore
+    #       break
+    #     if slice-equal?(word-slice, "==")
+    #       break                                 # no need for segment header lines
+    #     result += compute-width(word-slice)
+    #   return result
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    51/push-ECX
+    52/push-EDX
+    53/push-EBX
+    # var result/EAX = 0
+    31/xor                          3/mod/direct    0/rm32/EAX    .           .             .           0/r32/EAX   .               .                 # clear EAX
+    # var word-slice/ECX = {0, 0}
+    68/push  0/imm32/end
+    68/push  0/imm32/start
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+#?     # dump line {{{
+#?     # . write(2/stderr, "LL: ")
+#?     # . . push args
+#?     68/push  "LL: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # write-stream(2/stderr, line)
+#?     # . . push args
+#?     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # . rewind-stream(line)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  rewind-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+$num-bytes:loop:
+    # next-word(line, word-slice)
+    # . . push args
+    51/push-ECX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  next-word/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # dump word-slice {{{
+#?     # . write(2/stderr, "AA: ")
+#?     # . . push args
+#?     68/push  "AA: "/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . clear-stream(Stderr+4)
+#?     # . . save EAX
+#?     50/push-EAX
+#?     # . . push args
+#?     b8/copy-to-EAX  Stderr/imm32
+#?     05/add-to-EAX  4/imm32
+#?     50/push-EAX
+#?     # . . call
+#?     e8/call  clear-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . . restore EAX
+#?     58/pop-to-EAX
+#?     # . write-slice-buffered(Stderr, word-slice)
+#?     # . . push args
+#?     51/push-ECX
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  write-slice-buffered/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . flush(Stderr)
+#?     # . . push args
+#?     68/push  Stderr/imm32
+#?     # . . call
+#?     e8/call  flush/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+$num-bytes:check0:
+    # if (slice-empty?(word-slice)) break
+    # . save result
+    50/push-EAX
+    # . EAX = slice-empty?(word-slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call  slice-empty?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . if (EAX != 0) break
+    3d/compare-EAX-and  0/imm32
+    # . restore result now that ZF is set
+    58/pop-to-EAX
+    75/jump-if-not-equal  $num-bytes:end/disp8
+$num-bytes:check-for-comment:
+    # if (slice-starts-with?(word-slice, "#")) break
+    # . start/EDX = word-slice->start
+    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # copy *ECX to EDX
+    # . c/EBX = *start
+    31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
+    8a/copy-byte                    0/mod/indirect  2/rm32/EDX    .           .             .           3/r32/BL    .               .                 # copy byte at *EDX to BL
+    # . if (EBX == '#') break
+    81          7/subop/compare     3/mod/direct    3/rm32/EBX    .           .             .           .           .               0x23/imm32/hash   # compare EBX
+    74/jump-if-equal  $num-bytes:end/disp8
+$num-bytes:check-for-label:
+    # if (slice-ends-with?(word-slice, ":")) break
+    # . end/EDX = word-slice->end
+    8b/copy                         1/mod/*+disp8   1/rm32/ECX    .           .             .           2/r32/EDX   4/disp8         .                 # copy *(ECX+4) to EDX
+    # . c/EBX = *(end-1)
+    31/xor                          3/mod/direct    3/rm32/EBX    .           .             .           3/r32/EBX   .               .                 # clear EBX
+    8a/copy-byte                    1/mod/*+disp8   2/rm32/EDX    .           .             .           3/r32/BL    -1/disp8        .                 # copy byte at *ECX to BL
+    # . if (EBX == ':') break
+    81          7/subop/compare     3/mod/direct    3/rm32/EBX    .           .             .           .           .               0x3a/imm32/colon  # compare EBX
+    74/jump-if-equal  $num-bytes:end/disp8
+$num-bytes:check-for-segment-header:
+    # if (slice-equal?(word-slice, "==")) break
+    # . push result
+    50/push-EAX
+    # . EAX = slice-equal?(word-slice, "==")
+    # . . push args
+    68/push  "=="/imm32
+    51/push-ECX
+    # . . call
+    e8/call  slice-equal?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . if (EAX != 0) break
+    3d/compare-EAX-and  0/imm32
+    # . restore result now that ZF is set
+    58/pop-to-EAX
+    75/jump-if-not-equal  $num-bytes:end/disp8
+$num-bytes:loop-body:
+    # result += compute-width-of-slice(word-slice)
+    # . copy result to EDX
+    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EDX
+    # . EAX = compute-width-of-slice(word-slice)
+    # . . push args
+    51/push-ECX
+    # . . call
+    e8/call compute-width-of-slice/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . EAX += result
+    01/add                          3/mod/direct    0/rm32/EAX    .           .             .           2/r32/EDX   .               .                 # add EDX to EAX
+    e9/jump  $num-bytes:loop/disp32
+$num-bytes:end:
+    # . rewind-stream(line)
+    # . . push args
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  rewind-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . reclaim locals
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . restore registers
+    5b/pop-to-EBX
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-num-bytes-handles-empty-string:
+    # if a line starts with '#', return 0
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # no contents in input
+    # EAX = num-bytes(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  num-bytes/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-num-bytes-handles-empty-string"/imm32
+    68/push  0/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-num-bytes-ignores-comments:
+    # if a line starts with '#', return 0
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # initialize input
+    # . write(_test-input-stream, "# abcd")
+    # . . push args
+    68/push  "# abcd"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EAX = num-bytes(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  num-bytes/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-num-bytes-ignores-comments"/imm32
+    68/push  0/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-num-bytes-ignores-labels:
+    # if the first word ends with ':', return 0
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # initialize input
+    # . write(_test-input-stream, "ab: # cd")
+    # . . push args
+    68/push  "ab: # cd"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EAX = num-bytes(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  num-bytes/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-num-bytes-ignores-labels"/imm32
+    68/push  0/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-num-bytes-ignores-segment-headers:
+    # if the first word is '==', return 0
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # initialize input
+    # . write(_test-input-stream, "== ab cd")
+    # . . push args
+    68/push  "== ab cd"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EAX = num-bytes(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  num-bytes/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 0, msg)
+    # . . push args
+    68/push  "F - test-num-bytes-ignores-segment-headers"/imm32
+    68/push  0/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-num-bytes-counts-words-by-default:
+    # without metadata, count words
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # initialize input
+    # . write(_test-input-stream, "ab cd ef")
+    # . . push args
+    68/push  "ab cd ef"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EAX = num-bytes(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  num-bytes/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 3, msg)
+    # . . push args
+    68/push  "F - test-num-bytes-counts-words-by-default"/imm32
+    68/push  3/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-num-bytes-ignores-trailing-comment:
+    # trailing comments appropriately ignored
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # initialize input
+    # . write(_test-input-stream, "ab cd # ef")
+    # . . push args
+    68/push  "ab cd # ef"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EAX = num-bytes(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  num-bytes/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 2, msg)
+    # . . push args
+    68/push  "F - test-num-bytes-ignores-trailing-comment"/imm32
+    68/push  2/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-num-bytes-handles-imm32:
+    # if a word has the /imm32 metadata, count it as 4 bytes
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # initialize input
+    # . write(_test-input-stream, "ab cd/imm32 ef")
+    # . . push args
+    68/push  "ab cd/imm32 ef"/imm32
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # EAX = num-bytes(_test-input-stream)
+    # . . push args
+    68/push  _test-input-stream/imm32
+    # . . call
+    e8/call  num-bytes/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-ints-equal(EAX, 6, msg)
+    # . . push args
+    68/push  "F - test-num-bytes-handles-imm32"/imm32
+    68/push  6/imm32/true
+    50/push-EAX
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+== data
+
+Segment-size:
+  0x1000/imm32/4KB
+
+# This block of bytes gets copied to the start of the output ELF file, with
+# some fields filled in.
+# http://www.sco.com/developers/gabi/latest/ch4.eheader.html
+Elf_header:
+  # - length
+  0x34/imm32
+  # - data
+$e_ident:
+  7f 45/E 4c/L 46/F
+  01/32-bit  01/little-endian  01/file-version  00/no-os-extensions
+  00 00 00 00 00 00 00 00  # 8 bytes of padding
+$e_type:
+  02 00
+$e_machine:
+  03 00
+$e_version:
+  1/imm32
+Elf_e_entry:
+  0x09000000/imm32  # approximate default; must be updated
+$e_phoff:
+  0x34/imm32  # offset for the 'program header table' containing segment headers
+$e_shoff:
+  0/imm32  # no sections
+$e_flags:
+  0/imm32  # unused
+$e_ehsize:
+  0x34 00
+$e_phentsize:
+  0x20 00
+Elf_e_phnum:
+  00 00  # number of segments; must be updated
+$e_shentsize:
+  00 00  # no sections
+$e_shnum:
+  00 00
+$e_shstrndx:
+  00 00
+
+# This block of bytes gets copied after the Elf_header once for each segment.
+# Some fields need filling in each time.
+# https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-83432/index.html
+Elf_program_header_entry:
+  # - length
+  0x20/imm32
+  # - data
+$p_type:
+  1/imm32/PT_LOAD
+Elf_p_offset:
+  0/imm32  # byte offset in the file at which a segment begins; must be updated
+Elf_p_vaddr:
+  0/imm32  # starting address to store the segment at before running the program
+Elf_p_paddr:
+  0/imm32  # should have same value as Elf_p_vaddr
+Elf_p_filesz:
+  0/imm32
+Elf_p_memsz:
+  0/imm32  # should have same value as Elf_p_filesz
+Elf_p_flags:
+  6/imm32/rw-  # read/write/execute permissions for the segment; must be updated for the code segment
+$p_align:
+  # we hold this constant; changing it will require adjusting the way we
+  # compute the starting address for each segment
+  0x1000/imm32
+
+# . . vim:nowrap:textwidth=0
diff --git a/subx/dgen b/subx/dgen
deleted file mode 100755
index 1a5a8366..00000000
--- a/subx/dgen
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/sh
-# Little helper to quickly build SubX programs in 'debug mode' from the commandline.
-# Only works for programs in some standard places the repo knows about.
-
-if [ $# -eq 0 ]
-then
-  echo "Usage: $0 <file root without subdirectory or .subx extension>"
-  echo
-  echo "Naming convention: Files starting with 'ex' will be assumed to live in examples/ and be self-contained."
-  echo "Other files will be assumed to live in apps/ and need the standard library."
-  exit 1
-fi
-
-# Build in debug mode since the common case at the moment is building small
-# files. To override, calling scripts should do their own builds to ensure
-# subx_bin is up to date.
-export CFLAGS=-g
-
-case $1 in
-  ex*)
-    ./subx --debug translate examples/$1.subx -o examples/`echo $1 |sed 's/\..*//'`
-    exit $?
-    ;;
-  *)
-    ./subx --debug translate *.subx apps/$1.subx  -o apps/`echo $1 |sed 's/\..*//'`
-    exit $?
-    ;;
-esac
diff --git a/subx/drun b/subx/drun
deleted file mode 100755
index 71b2f0e0..00000000
--- a/subx/drun
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env zsh
-# Run commonly-used SubX programs using the SubX VM in 'debug mode'.
-
-if [ $# -eq 0 ]
-then
-  echo "Usage: $0 <binary name without directory> <args>"
-  echo
-  echo "Naming convention: Binaries starting with 'ex' will be assumed to live in examples/"
-  echo "Other binaries will be assumed to live in apps/"
-  exit 1
-fi
-
-case $1 in
-  ex*)
-    ./subx --debug --trace run examples/$*
-    exit $?
-    ;;
-  *)
-    ./subx --debug --trace run apps/$*
-    exit $?
-    ;;
-esac
diff --git a/subx/gen b/subx/gen
deleted file mode 100755
index 0acbe3d8..00000000
--- a/subx/gen
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/sh
-# Little helper to quickly build SubX programs from the commandline.
-# Only works for programs in some standard places the repo knows about.
-
-if [ $# -eq 0 ]
-then
-  echo "Usage: $0 <file root without subdirectory or .subx extension>"
-  echo
-  echo "Naming convention: Files starting with 'ex' will be assumed to live in examples/ and be self-contained."
-  echo "Other files will be assumed to live in apps/ and need the standard library."
-  exit 1
-fi
-
-# Build in debug mode since the common case at the moment is building small
-# files. To override, calling scripts should do their own builds to ensure
-# subx_bin is up to date.
-export CFLAGS=-g
-
-case $1 in
-  ex*)
-    ./subx translate examples/$1.subx -o examples/`echo $1 |sed 's/\..*//'`
-    exit $?
-    ;;
-  *)
-    ./subx translate *.subx apps/$1.subx  -o apps/`echo $1 |sed 's/\..*//'`
-    exit $?
-    ;;
-esac
diff --git a/subx/run b/subx/run
deleted file mode 100755
index 99de0777..00000000
--- a/subx/run
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env zsh
-# Run commonly-used SubX programs using the SubX VM.
-
-if [ $# -eq 0 ]
-then
-  echo "Usage: $0 <binary name without directory> <args>"
-  echo
-  echo "Naming convention: Binaries starting with 'ex' will be assumed to live in examples/"
-  echo "Other binaries will be assumed to live in apps/"
-  exit 1
-fi
-
-case $1 in
-  ex*)
-    ./subx run examples/$*
-    exit $?
-    ;;
-  *)
-    ./subx run apps/$*
-    exit $?
-    ;;
-esac
diff --git a/subx/run_one_test.sh b/subx/run_one_test.sh
new file mode 100755
index 00000000..caecd63c
--- /dev/null
+++ b/subx/run_one_test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env zsh
+# Either run the test with the given name, or rerun the most recently run test.
+# Intended to be called from within Vim. Check out the vimrc.vim file.
+
+if [[ $2 == 'test-'* ]]
+then
+  TEST_NAME=$2 envsubst '$TEST_NAME' < run_one_test.subx > /tmp/run_one_test.subx
+  FILES=$(ls [0-9]*.subx apps/subx-common.subx $1 |sort |uniq)
+  echo $FILES > /tmp/last_run_files
+elif [[ -e /tmp/last_run_files ]]
+then
+  FILES=`cat /tmp/last_run_files`
+else
+  echo "no test found"
+  exit 0  # don't open trace
+fi
+
+set -e
+                                      # turn newlines into spaces
+CFLAGS=$CFLAGS ./subx --debug translate $(echo $FILES) /tmp/run_one_test.subx -o /tmp/a.elf
+
+./subx --debug --trace run /tmp/a.elf
diff --git a/subx/run_one_test.subx b/subx/run_one_test.subx
new file mode 100644
index 00000000..43c98fca
--- /dev/null
+++ b/subx/run_one_test.subx
@@ -0,0 +1,30 @@
+# run a single test
+
+== code
+#   instruction                     effective address                                                   register    displacement    immediate
+# . op          subop               mod             rm32          base        index         scale       r32
+# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
+
+Entry:
+    # Heap = new-segment(64KB)
+    # . . push args
+    68/push  Heap/imm32
+    68/push  0x10000/imm32/64KB
+    # . . call
+    e8/call  new-segment/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # initialize-trace-stream(256KB)
+    # . . push args
+    68/push  0x40000/imm32/256KB
+    # . . call
+    e8/call  initialize-trace-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # for debugging: run a single test
+    e8/call $TEST_NAME/disp32
+    8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+
+# . . vim:nowrap:textwidth=0
diff --git a/subx/test_apps b/subx/test_apps
index a6e78ee3..10b6869b 100755
--- a/subx/test_apps
+++ b/subx/test_apps
@@ -184,6 +184,16 @@ test `uname` = 'Linux'  &&  {
   echo
 }
 
+echo survey
+./subx translate 0*.subx apps/subx-common.subx apps/survey.subx  -o apps/survey
+[ "$1" != record ]  &&  git diff --exit-code apps/survey
+./subx run apps/survey test
+echo
+test `uname` = 'Linux'  &&  {
+  apps/survey test
+  echo
+}
+
 echo pack
 ./subx translate 0*.subx apps/subx-common.subx apps/pack.subx  -o apps/pack
 [ "$1" != record ]  &&  git diff --exit-code apps/pack
diff --git a/subx/vimrc.vim b/subx/vimrc.vim
index 89401ff3..57c450d6 120000..100644
--- a/subx/vimrc.vim
+++ b/subx/vimrc.vim
@@ -1 +1,95 @@
-../vimrc.vim
\ No newline at end of file
+" Highlighting literate directives in C++ sources.
+function! HighlightTangledFile()
+  " Tangled comments only make sense in the sources and are stripped out of
+  " the generated .cc file. They're highlighted same as regular comments.
+  syntax match tangledComment /\/\/:.*/ | highlight link tangledComment Comment
+  syntax match tangledSalientComment /\/\/::.*/ | highlight link tangledSalientComment SalientComment
+  set comments-=://
+  set comments-=n://
+  set comments+=n://:,n://
+
+  " Inside tangle scenarios.
+  syntax region tangleDirective start=+:(+ skip=+".*"+ end=+)+
+  highlight link tangleDirective Delimiter
+  syntax match traceContains /^+.*/
+  highlight traceContains ctermfg=22
+  syntax match traceAbsent /^-.*/
+  highlight traceAbsent ctermfg=darkred
+  syntax match tangleScenarioSetup /^\s*% .*/ | highlight link tangleScenarioSetup SpecialChar
+  highlight Special ctermfg=160
+
+  syntax match subxString %"[^"]*"% | highlight link subxString Constant
+  " match globals but not registers like 'EAX'
+  syntax match subxGlobal %\<[A-Z][a-z0-9_-]*\>% | highlight link subxGlobal SpecialChar
+endfunction
+augroup LocalVimrc
+  autocmd BufRead,BufNewFile *.cc call HighlightTangledFile()
+  autocmd BufRead,BufNewFile *.subx set ft=subx
+augroup END
+
+" Scenarios considered:
+"   opening or starting vim with a new or existing file without an extension (should interpret as C++)
+"   opening or starting vim with a new or existing file with a .mu extension
+"   starting vim or opening a buffer without a file name (ok to do nothing)
+"   opening a second file in a new or existing window (shouldn't mess up existing highlighting)
+"   reloading an existing file (shouldn't mess up existing highlighting)
+
+" assumes CWD is subx/
+command! -nargs=1 E call EditSubx("edit", <f-args>)
+if exists("&splitvertical")
+  command! -nargs=1 S call EditSubx("vert split", <f-args>)
+  command! -nargs=1 H call EditSubx("hor split", <f-args>)
+else
+  command! -nargs=1 S call EditSubx("vert split", <f-args>)
+  command! -nargs=1 H call EditSubx("split", <f-args>)
+endif
+
+function! EditSubx(cmd, arg)
+  exec "silent! " . a:cmd . " " . SubxPath(a:arg)
+endfunction
+
+function! SubxPath(arg)
+  if a:arg =~ "^ex"
+    return "examples/" . a:arg . ".subx"
+  else
+    return "apps/" . a:arg . ".subx"
+  endif
+endfunction
+
+" we often want to crib lines of machine code from other files
+function! GrepSubX(regex)
+  " https://github.com/mtth/scratch.vim
+  Scratch!
+  silent exec "r !grep -h '".a:regex."' *.subx */*.subx"
+endfunction
+command! -nargs=1 G call GrepSubX(<q-args>)
+
+if exists("&splitvertical")
+  command! -nargs=0 P hor split opcodes
+else
+  command! -nargs=0 P split opcodes
+endif
+
+" useful for inspecting just the control flow in a trace
+" see https://github.com/akkartik/mu/blob/master/subx/Readme.md#a-few-hints-for-debugging
+" the '-a' is because traces can sometimes contain unprintable characters that bother grep
+command! -nargs=0 L exec "%!grep -a label |grep -v clear-stream:loop"
+
+" run test cursor around cursor
+"   if test fails, open trace in split window
+"   if test passes, just show output and wait for <CR>
+"   don't move cursor in original window
+" this solution is unfortunate, but seems forced:
+"   can't put initial cursor movement inside function because we rely on <C-r><C-w> to grab word at cursor
+"   can't put final cursor movement out of function because that disables the wait for <CR> prompt; function must be final operation of map
+"   can't avoid the function because that disables the wait for <CR> prompt
+" known issue:
+"   cursor on '#' causes error
+noremap <Leader>t {j0:call RunTestMoveCursorAndMaybeOpenTrace("<C-r><C-w>")<CR>
+function RunTestMoveCursorAndMaybeOpenTrace(arg)
+  exec "!run_one_test.sh ".expand("%")." ".a:arg
+  exec "normal \<C-o>"
+  if v:shell_error
+    noautocmd vertical split last_run
+  endif
+endfunction
diff --git a/vimrc.vim b/vimrc.vim
index 0c297fef..c63a8941 100644
--- a/vimrc.vim
+++ b/vimrc.vim
@@ -16,16 +16,10 @@ function! HighlightTangledFile()
   syntax match traceAbsent /^-.*/
   highlight traceAbsent ctermfg=darkred
   syntax match tangleScenarioSetup /^\s*% .*/ | highlight link tangleScenarioSetup SpecialChar
-
   highlight Special ctermfg=160
-
-  syntax match subxString %"[^"]*"% | highlight link subxString Constant
-  " match globals but not registers like 'EAX'
-  syntax match subxGlobal %\<[A-Z][a-z0-9_-]*\>% | highlight link subxGlobal SpecialChar
 endfunction
 augroup LocalVimrc
   autocmd BufRead,BufNewFile *.cc call HighlightTangledFile()
-  autocmd BufRead,BufNewFile *.subx set ft=subx
   autocmd BufRead,BufNewFile *.mu set ft=mu
 augroup END
 
@@ -35,44 +29,3 @@ augroup END
 "   starting vim or opening a buffer without a file name (ok to do nothing)
 "   opening a second file in a new or existing window (shouldn't mess up existing highlighting)
 "   reloading an existing file (shouldn't mess up existing highlighting)
-
-" assumes CWD is subx/
-command! -nargs=1 E call EditSubx("edit", <f-args>)
-if exists("&splitvertical")
-  command! -nargs=1 S call EditSubx("vert split", <f-args>)
-  command! -nargs=1 H call EditSubx("hor split", <f-args>)
-else
-  command! -nargs=1 S call EditSubx("vert split", <f-args>)
-  command! -nargs=1 H call EditSubx("split", <f-args>)
-endif
-
-function! EditSubx(cmd, arg)
-  exec "silent! " . a:cmd . " " . SubxPath(a:arg)
-endfunction
-
-function! SubxPath(arg)
-  if a:arg =~ "^ex"
-    return "examples/" . a:arg . ".subx"
-  else
-    return "apps/" . a:arg . ".subx"
-  endif
-endfunction
-
-" we often want to crib lines of machine code from other files
-function! GrepSubX(regex)
-  " https://github.com/mtth/scratch.vim
-  Scratch!
-  silent exec "r !grep -h '".a:regex."' *.subx */*.subx"
-endfunction
-command! -nargs=1 G call GrepSubX(<q-args>)
-
-if exists("&splitvertical")
-  command! -nargs=0 P hor split opcodes
-else
-  command! -nargs=0 P split opcodes
-endif
-
-" useful for inspecting just the control flow in a trace
-" see https://github.com/akkartik/mu/blob/master/subx/Readme.md#a-few-hints-for-debugging
-" the '-a' is because traces can sometimes contain unprintable characters that bother grep
-command! -nargs=0 L exec "%!grep -a label |grep -v clear-stream:loop"