about summary refs log tree commit diff stats
path: root/subx
diff options
context:
space:
mode:
Diffstat (limited to 'subx')
-rw-r--r--subx/003trace.cc46
-rw-r--r--subx/003trace.test.cc4
-rw-r--r--subx/011run.cc183
-rw-r--r--subx/013direct_addressing.cc741
-rw-r--r--subx/014indirect_addressing.cc847
-rw-r--r--subx/015immediate_addressing.cc871
-rw-r--r--subx/016index_addressing.cc178
-rw-r--r--subx/017jump_disp8.cc424
-rw-r--r--subx/018jump_disp32.cc424
-rw-r--r--subx/019functions.cc116
-rw-r--r--subx/021byte_addressing.cc126
-rw-r--r--subx/030---operands.cc155
-rw-r--r--subx/031check_operands.cc300
-rw-r--r--subx/032check_operand_bounds.cc15
-rw-r--r--subx/034compute_segment_address.cc124
-rw-r--r--subx/035labels.cc230
-rw-r--r--subx/036global_variables.cc189
-rw-r--r--subx/038---literal_strings.cc192
-rw-r--r--subx/040---tests.cc31
19 files changed, 3144 insertions, 2052 deletions
diff --git a/subx/003trace.cc b/subx/003trace.cc
index bb614c66..9717fb80 100644
--- a/subx/003trace.cc
+++ b/subx/003trace.cc
@@ -16,28 +16,26 @@
 //: In response, this layer introduces the notion of domain-driven *white-box*
 //: testing. We focus on the domain of inputs the whole program needs to
 //: handle rather than the correctness of individual functions. All white-box
-//: tests (we call them 'scenarios') invoke the program in a single way: by
-//: calling run() with some input. As the program operates on the input, it
-//: traces out a list of _facts_ deduced about the domain:
+//: tests invoke the program in a single way: by calling run() with some
+//: input. As the program operates on the input, it traces out a list of
+//: _facts_ deduced about the domain:
 //:   trace("label") << "fact 1: " << val;
 //:
-//: Scenarios can now check these facts:
-//:   :(scenario foo)
-//:   34  # call run() with this input
-//:   +label: fact 1: 34  # 'run' should have deduced this fact
-//:   -label: fact 1: 35  # the trace should not contain such a fact
+//: Tests can now check for these facts in the trace:
+//:   CHECK_TRACE_CONTENTS("label", "fact 1: 34\n"
+//:                                 "fact 2: 35\n");
 //:
 //: Since we never call anything but the run() function directly, we never have
-//: to rewrite the scenarios when we reorganize the internals of the program. We
+//: to rewrite the tests when we reorganize the internals of the program. We
 //: just have to make sure our rewrite deduces the same facts about the domain,
 //: and that's something we're going to have to do anyway.
 //:
 //: To avoid the combinatorial explosion of integration tests, each layer
-//: mainly logs facts to the trace with a common *label*. All scenarios in a
-//: layer tend to check facts with this label. Validating the facts logged
-//: with a specific label is like calling functions of that layer directly.
+//: mainly logs facts to the trace with a common *label*. All tests in a layer
+//: tend to check facts with this label. Validating the facts logged with a
+//: specific label is like calling functions of that layer directly.
 //:
-//: To build robust scenarios, trace facts about your domain rather than details of
+//: To build robust tests, trace facts about your domain rather than details of
 //: how you computed them.
 //:
 //: More details: http://akkartik.name/blog/tracing-tests
@@ -50,10 +48,10 @@
 //: we allow programmers to engage with the a) deep, b) global structure of
 //: the c) domain. If you can systematically track discontinuities in the
 //: domain, you don't care if the code used gotos as long as it passed all
-//: scenarios. If scenarios become more robust to run, it becomes easier to
-//: try out radically different implementations for the same program. If code
-//: is super-easy to rewrite, it becomes less important what indentation style
-//: it uses, or that the objects are appropriately encapsulated, or that the
+//: tests. If tests become more robust to run, it becomes easier to try out
+//: radically different implementations for the same program. If code is
+//: super-easy to rewrite, it becomes less important what indentation style it
+//: uses, or that the objects are appropriately encapsulated, or that the
 //: functions are referentially transparent.
 //:
 //: Instead of plumbing, programming becomes building and gradually refining a
@@ -61,7 +59,7 @@
 //: is 'correct' at a given point in time is a red herring; what matters is
 //: avoiding regression by monotonically nailing down the more 'eventful'
 //: parts of the terrain. It helps readers new and old, and rewards curiosity,
-//: to organize large programs in self-similar hierarchies of example scenarios
+//: to organize large programs in self-similar hierarchies of example tests
 //: colocated with the code that makes them work.
 //:
 //:   "Programming properly should be regarded as an activity by which
@@ -178,7 +176,7 @@ void trace_stream::newline() {
   curr_depth = Max_depth;
 }
 
-//:: == Initializing the trace in scenarios
+//:: == Initializing the trace in tests
 
 :(before "End Includes")
 #define START_TRACING_UNTIL_END_OF_SCOPE  lease_tracer leased_tracer;
@@ -214,7 +212,7 @@ int Hide_warnings = false;  // if set, don't print warnings to screen
 :(before "End Reset")
 Hide_errors = false;
 Hide_warnings = false;
-//: Never dump warnings in scenarios
+//: Never dump warnings in tests
 :(before "End Test Setup")
 Hide_warnings = true;
 :(code)
@@ -230,7 +228,7 @@ bool should_incrementally_print_trace();
 :(before "End Globals")
 int Trace_errors = 0;  // used only when Trace_stream is NULL
 
-// Fail scenarios that displayed (unexpected) errors.
+// Fail tests that displayed (unexpected) errors.
 // Expected errors should always be hidden and silently checked for.
 :(before "End Test Teardown")
 if (Passed && !Hide_errors && trace_contains_errors()) {
@@ -287,14 +285,14 @@ bool trace_contains_errors() {
     return; \
   }
 
-// Allow scenarios to ignore trace lines generated during setup.
+// Allow tests to ignore trace lines generated during setup.
 #define CLEAR_TRACE  delete Trace_stream, Trace_stream = new trace_stream
 
 :(code)
 bool check_trace_contents(string FUNCTION, string FILE, int LINE, string expected) {
   if (!Passed) return false;
   if (!Trace_stream) return false;
-  vector<string> expected_lines = split(expected, "");
+  vector<string> expected_lines = split(expected, "\n");
   int curr_expected_line = 0;
   while (curr_expected_line < SIZE(expected_lines) && expected_lines.at(curr_expected_line).empty())
     ++curr_expected_line;
@@ -408,7 +406,7 @@ vector<string> split_first(string s, string delim) {
 //:: == Helpers for debugging using traces
 
 :(before "End Includes")
-// To debug why a scenario is failing, dump its trace using '?'.
+// To debug why a test is failing, dump its trace using '?'.
 #define DUMP(label)  if (Trace_stream) cerr << Trace_stream->readable_contents(label);
 
 // To add temporary prints to the trace, use 'dbg'.
diff --git a/subx/003trace.test.cc b/subx/003trace.test.cc
index 85751a4a..addc1f44 100644
--- a/subx/003trace.test.cc
+++ b/subx/003trace.test.cc
@@ -42,7 +42,9 @@ void test_trace_orders_across_layers() {
   trace("test layer 1") << "foo" << end();
   trace("test layer 2") << "bar" << end();
   trace("test layer 1") << "qux" << end();
-  CHECK_TRACE_CONTENTS("test layer 1: footest layer 2: bartest layer 1: qux");
+  CHECK_TRACE_CONTENTS("test layer 1: foo\n"
+                       "test layer 2: bar\n"
+                       "test layer 1: qux\n");
 }
 
 void test_trace_supports_count() {
diff --git a/subx/011run.cc b/subx/011run.cc
index 90af649b..da5af920 100644
--- a/subx/011run.cc
+++ b/subx/011run.cc
@@ -34,46 +34,53 @@ put_new(Help, "syntax",
 :(before "End Help Contents")
 cerr << "  syntax\n";
 
-:(scenario add_imm32_to_eax)
-# At the lowest level, SubX programs are a series of hex bytes, each
-# (variable-length) instruction on one line.
-#
-# Later we'll make things nicer using macros. But you'll always be able to
-# insert hex bytes out of instructions.
-#
-# As you can see, comments start with '#' and are ignored.
-
-# Segment headers start with '==', specifying the hex address where they
-# begin. There's usually one code segment and one data segment. We assume the
-# code segment always comes first. Later when we emit ELF binaries we'll add
-# directives for the operating system to ensure that the code segment can't be
-# written to, and the data segment can't be executed as code.
-== 0x1
-
-# We don't show it here, but all lines can have metadata after a ':'.
-# All words can have metadata after a '/'. No spaces allowed in word metadata, of course.
-# Metadata doesn't directly form instructions, but some macros may look at it.
-# Unrecognized metadata never causes errors, so you can also use it for
-# documentation.
-
-# Within the code segment, x86 instructions consist of the following parts (see cheatsheet.pdf):
-#   opcode        ModR/M                    SIB                   displacement    immediate
-#   instruction   mod, reg, Reg/Mem bits    scale, index, base
-#   1-3 bytes     0/1 byte                  0/1 byte              0/1/2/4 bytes   0/1/2/4 bytes
-    05            .                         .                     .               0a 0b 0c 0d  # add 0x0d0c0b0a to EAX
-# (The single periods are just to help the eye track long gaps between
-# columns, and are otherwise ignored.)
-
-# This program, when run, causes the following events in the trace:
-+load: 0x00000001 -> 05
-+load: 0x00000002 -> 0a
-+load: 0x00000003 -> 0b
-+load: 0x00000004 -> 0c
-+load: 0x00000005 -> 0d
-+run: add imm32 0x0d0c0b0a to reg EAX
-+run: storing 0x0d0c0b0a
-
 :(code)
+void test_add_imm32_to_eax() {
+  // At the lowest level, SubX programs are a series of hex bytes, each
+  // (variable-length) instruction on one line.
+  run(
+      // Comments start with '#' and are ignored.
+      "# comment\n"
+      // Segment headers start with '==' and a name or starting hex address.
+      // There's usually one code and one data segment. The code segment
+      // always comes first.
+      "== 0x1\n"  // code segment
+
+      // After the header, each segment consists of lines, and each line
+      // consists of words separated by whitespace.
+      //
+      // All words can have metadata after a '/'. No spaces allowed in
+      // metadata, of course.
+      // Unrecognized metadata never causes errors, so you can use it for
+      // documentation.
+      //
+      // Within the code segment in particular, x86 instructions consist of
+      // some number of the following parts and sub-parts (see the Readme and
+      // cheatsheet.pdf for details):
+      //   opcodes: 1-3 bytes
+      //   ModR/M byte
+      //   SIB byte
+      //   displacement: 0/1/2/4 bytes
+      //   immediate: 0/1/2/4 bytes
+      // opcode        ModR/M                    SIB                   displacement    immediate
+      // instruction   mod, reg, Reg/Mem bits    scale, index, base
+      // 1-3 bytes     0/1 byte                  0/1 byte              0/1/2/4 bytes   0/1/2/4 bytes
+      "  05            .                         .                     .               0a 0b 0c 0d\n"  // add 0x0d0c0b0a to EAX
+      // The periods are just to help the eye track long gaps between columns,
+      // and are otherwise ignored.
+  );
+  // This program, when run, causes the following events in the trace:
+  CHECK_TRACE_CONTENTS(
+      "load: 0x00000001 -> 05\n"
+      "load: 0x00000002 -> 0a\n"
+      "load: 0x00000003 -> 0b\n"
+      "load: 0x00000004 -> 0c\n"
+      "load: 0x00000005 -> 0d\n"
+      "run: add imm32 0x0d0c0b0a to reg EAX\n"
+      "run: storing 0x0d0c0b0a\n"
+  );
+}
+
 // top-level helper for scenarios: parse the input, transform any macros, load
 // the final hex bytes into memory, run it
 void run(const string& text_bytes) {
@@ -207,14 +214,18 @@ void parse(const string& text_bytes) {
   parse(in, p);
 }
 
-:(scenarios parse)
-:(scenario detect_duplicate_segments)
-% Hide_errors = true;
-== 0xee
-ab
-== 0xee
-cd
-+error: can't have multiple segments starting at address 0x000000ee
+void test_detect_duplicate_segments() {
+  Hide_errors = true;
+  parse(
+      "== 0xee\n"
+      "ab\n"
+      "== 0xee\n"
+      "cd\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: can't have multiple segments starting at address 0x000000ee\n"
+  );
+}
 
 //:: transform
 
@@ -278,37 +289,56 @@ uint8_t hex_byte(const string& s) {
   return static_cast<uint8_t>(result);
 }
 
-:(scenarios parse_and_load)
-:(scenario number_too_large)
-% Hide_errors = true;
-== 0x1
-05 cab
-+error: token 'cab' is not a hex byte
-
-:(scenario invalid_hex)
-% Hide_errors = true;
-== 0x1
-05 cx
-+error: token 'cx' is not a hex byte
-
-:(scenario negative_number)
-== 0x1
-05 -12
-$error: 0
-
-:(scenario negative_number_too_small)
-% Hide_errors = true;
-== 0x1
-05 -12345
-+error: token '-12345' is not a hex byte
-
-:(scenario hex_prefix)
-== 0x1
-0x05 -0x12
-$error: 0
+void test_number_too_large() {
+  Hide_errors = true;
+  parse_and_load(
+      "== 0x1\n"
+      "05 cab\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: token 'cab' is not a hex byte\n"
+  );
+}
+
+void test_invalid_hex() {
+  Hide_errors = true;
+  parse_and_load(
+      "== 0x1\n"
+      "05 cx\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: token 'cx' is not a hex byte\n"
+  );
+}
+
+void test_negative_number() {
+  parse_and_load(
+      "== 0x1\n"
+      "05 -12\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_negative_number_too_small() {
+  Hide_errors = true;
+  parse_and_load(
+      "== 0x1\n"
+      "05 -12345\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: token '-12345' is not a hex byte\n"
+  );
+}
+
+void test_hex_prefix() {
+  parse_and_load(
+      "== 0x1\n"
+      "0x05 -0x12\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 //: helper for tests
-:(code)
 void parse_and_load(const string& text_bytes) {
   program p;
   istringstream in(text_bytes);
@@ -343,7 +373,6 @@ int32_t next32() {
 
 //:: helpers
 
-:(code)
 string to_string(const word& w) {
   ostringstream out;
   out << w.data;
diff --git a/subx/013direct_addressing.cc b/subx/013direct_addressing.cc
index 40f9f52e..8f554772 100644
--- a/subx/013direct_addressing.cc
+++ b/subx/013direct_addressing.cc
@@ -3,16 +3,22 @@
 :(before "End Initialize Op Names")
 put_new(Name, "01", "add r32 to rm32 (add)");
 
-:(scenario add_r32_to_r32)
-% Reg[EAX].i = 0x10;
-% Reg[EBX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  01  d8                                      # add EBX to EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: add EBX to r/m32
-+run: r/m32 is EAX
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_r32() {
+  Reg[EAX].i = 0x10;
+  Reg[EBX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     d8                                    \n" // add EBX to EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x01: {  // add r32 to r/m32
@@ -79,16 +85,22 @@ string rname(uint8_t r) {
 :(before "End Initialize Op Names")
 put_new(Name, "29", "subtract r32 from rm32 (sub)");
 
-:(scenario subtract_r32_from_r32)
-% Reg[EAX].i = 10;
-% Reg[EBX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  29  d8                                      # subtract EBX from EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: subtract EBX from r/m32
-+run: r/m32 is EAX
-+run: storing 0x00000009
+:(code)
+void test_subtract_r32_from_r32() {
+  Reg[EAX].i = 10;
+  Reg[EBX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  29     d8                                    \n"  // subtract EBX from EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract EBX from r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing 0x00000009\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x29: {  // subtract r32 from r/m32
@@ -105,17 +117,23 @@ case 0x29: {  // subtract r32 from r/m32
 :(before "End Initialize Op Names")
 put_new(Name, "f7", "negate/multiply rm32 (with EAX if necessary) depending on subop (neg/mul)");
 
-:(scenario multiply_eax_by_r32)
-% Reg[EAX].i = 4;
-% Reg[ECX].i = 3;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  f7      e1                                      # multiply EAX by ECX
-# ModR/M in binary: 11 (direct mode) 100 (subop mul) 001 (src ECX)
-+run: operate on r/m32
-+run: r/m32 is ECX
-+run: subop: multiply EAX by r/m32
-+run: storing 0x0000000c
+:(code)
+void test_multiply_eax_by_r32() {
+  Reg[EAX].i = 4;
+  Reg[ECX].i = 3;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  f7     e1                                    \n"  // multiply EAX by ECX
+      // ModR/M in binary: 11 (direct mode) 100 (subop mul) 001 (src ECX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is ECX\n"
+      "run: subop: multiply EAX by r/m32\n"
+      "run: storing 0x0000000c\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xf7: {
@@ -146,16 +164,22 @@ case 0xf7: {
 :(before "End Initialize Op Names")
 put_new(Name_0f, "af", "multiply rm32 into r32 (imul)");
 
-:(scenario multiply_r32_into_r32)
-% Reg[EAX].i = 4;
-% Reg[EBX].i = 2;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f af   d8                                      # subtract EBX into EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: multiply r/m32 into EBX
-+run: r/m32 is EAX
-+run: storing 0x00000008
+:(code)
+void test_multiply_r32_into_r32() {
+  Reg[EAX].i = 4;
+  Reg[EBX].i = 2;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f af  d8                                    \n"  // subtract EBX into EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: multiply r/m32 into EBX\n"
+      "run: r/m32 is EAX\n"
+      "run: storing 0x00000008\n"
+  );
+}
 
 :(before "End Two-Byte Opcodes Starting With 0f")
 case 0xaf: {  // multiply r32 into r/m32
@@ -169,16 +193,22 @@ case 0xaf: {  // multiply r32 into r/m32
 
 //:: negate
 
-:(scenario negate_r32)
-% Reg[EBX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  f7  db                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 011 (subop negate) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: negate
-+run: storing 0xffffffff
+:(code)
+void test_negate_r32() {
+  Reg[EBX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  f7 db                                        \n"  // negate EBX
+      // ModR/M in binary: 11 (direct mode) 011 (subop negate) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: negate\n"
+      "run: storing 0xffffffff\n"
+  );
+}
 
 :(before "End Op f7 Subops")
 case 3: {  // negate r/m32
@@ -199,33 +229,46 @@ case 3: {  // negate r/m32
   break;
 }
 
-:(scenario negate_can_overflow)  // in exactly one situation
-% Reg[EBX].i = 0x80000000;  // INT_MIN
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  f7  db                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 011 (subop negate) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: negate
-+run: overflow
+:(code)
+// negate can overflow in exactly one situation
+void test_negate_can_overflow() {
+  Reg[EBX].i = 0x80000000;  // INT_MIN
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  f7 db                                        \n"  // negate EBX
+      // ModR/M in binary: 11 (direct mode) 011 (subop negate) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: negate\n"
+      "run: overflow\n"
+  );
+}
 
 //:: shift left
 
 :(before "End Initialize Op Names")
 put_new(Name, "d3", "shift rm32 by CL bits depending on subop (sal/sar/shl/shr)");
 
-:(scenario shift_left_r32_with_cl)
-% Reg[EBX].i = 13;
-% Reg[ECX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  d3  e3                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 100 (subop shift left) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift left by CL bits
-+run: storing 0x0000001a
+:(code)
+void test_shift_left_r32_with_cl() {
+  Reg[EBX].i = 13;
+  Reg[ECX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  d3     e3                                    \n"  // shift EBX left by CL bits
+      // ModR/M in binary: 11 (direct mode) 100 (subop shift left) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift left by CL bits\n"
+      "run: storing 0x0000001a\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xd3: {
@@ -259,17 +302,23 @@ case 0xd3: {
 
 //:: shift right arithmetic
 
-:(scenario shift_right_arithmetic_r32_with_cl)
-% Reg[EBX].i = 26;
-% Reg[ECX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  d3  fb                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while preserving sign
-+run: storing 0x0000000d
+:(code)
+void test_shift_right_arithmetic_r32_with_cl() {
+  Reg[EBX].i = 26;
+  Reg[ECX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  d3     fb                                    \n"  // shift EBX right by CL bits, while preserving sign
+      // ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while preserving sign\n"
+      "run: storing 0x0000000d\n"
+  );
+}
 
 :(before "End Op d3 Subops")
 case 7: {  // shift right r/m32 by CL, preserving sign
@@ -284,45 +333,63 @@ case 7: {  // shift right r/m32 by CL, preserving sign
   break;
 }
 
-:(scenario shift_right_arithmetic_odd_r32_with_cl)
-% Reg[EBX].i = 27;
-% Reg[ECX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  d3  fb                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while preserving sign
-# result: 13
-+run: storing 0x0000000d
-
-:(scenario shift_right_arithmetic_negative_r32_with_cl)
-% Reg[EBX].i = 0xfffffffd;  // -3
-% Reg[ECX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  d3  fb                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while preserving sign
-# result: -2
-+run: storing 0xfffffffe
+:(code)
+void test_shift_right_arithmetic_odd_r32_with_cl() {
+  Reg[EBX].i = 27;
+  Reg[ECX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  d3     fb                                    \n"  // shift EBX right by CL bits, while preserving sign
+      // ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while preserving sign\n"
+      // result: 13
+      "run: storing 0x0000000d\n"
+  );
+}
+
+void test_shift_right_arithmetic_negative_r32_with_cl() {
+  Reg[EBX].i = 0xfffffffd;  // -3
+  Reg[ECX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  d3     fb                                    \n"  // shift EBX right by CL bits, while preserving sign
+      // ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while preserving sign\n"
+      // result: -2
+      "run: storing 0xfffffffe\n"
+  );
+}
 
 //:: shift right logical
 
-:(scenario shift_right_logical_r32_with_cl)
-% Reg[EBX].i = 26;
-% Reg[ECX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  d3  eb                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while padding zeroes
-+run: storing 0x0000000d
+:(code)
+void test_shift_right_logical_r32_with_cl() {
+  Reg[EBX].i = 26;
+  Reg[ECX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  d3     eb                                    \n"  // shift EBX right by CL bits, while padding zeroes
+      // ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while padding zeroes\n"
+      // result: 13
+      "run: storing 0x0000000d\n"
+  );
+}
 
 :(before "End Op d3 Subops")
 case 5: {  // shift right r/m32 by CL, preserving sign
@@ -343,46 +410,63 @@ case 5: {  // shift right r/m32 by CL, preserving sign
   break;
 }
 
-:(scenario shift_right_logical_odd_r32_with_cl)
-% Reg[EBX].i = 27;
-% Reg[ECX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  d3  eb                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while padding zeroes
-# result: 13
-+run: storing 0x0000000d
-
-:(scenario shift_right_logical_negative_r32_with_cl)
-% Reg[EBX].i = 0xfffffffd;
-% Reg[ECX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  d3  eb                                      # negate EBX
-# ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while padding zeroes
-+run: storing 0x7ffffffe
+:(code)
+void test_shift_right_logical_odd_r32_with_cl() {
+  Reg[EBX].i = 27;
+  Reg[ECX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  d3     eb                                    \n"  // shift EBX right by CL bits, while padding zeroes
+      // ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while padding zeroes\n"
+      // result: 13
+      "run: storing 0x0000000d\n"
+  );
+}
+
+void test_shift_right_logical_negative_r32_with_cl() {
+  Reg[EBX].i = 0xfffffffd;
+  Reg[ECX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  d3     eb                                    \n"  // shift EBX right by CL bits, while padding zeroes
+      // ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while padding zeroes\n"
+      "run: storing 0x7ffffffe\n"
+  );
+}
 
 //:: and
 
 :(before "End Initialize Op Names")
 put_new(Name, "21", "rm32 = bitwise AND of r32 with rm32 (and)");
 
-:(scenario and_r32_with_r32)
-% Reg[EAX].i = 0x0a0b0c0d;
-% Reg[EBX].i = 0x000000ff;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  21  d8                                      # and EBX with destination EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: and EBX with r/m32
-+run: r/m32 is EAX
-+run: storing 0x0000000d
+:(code)
+void test_and_r32_with_r32() {
+  Reg[EAX].i = 0x0a0b0c0d;
+  Reg[EBX].i = 0x000000ff;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  21     d8                                    \n"  // and EBX with destination EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: and EBX with r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing 0x0000000d\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x21: {  // and r32 with r/m32
@@ -399,16 +483,22 @@ case 0x21: {  // and r32 with r/m32
 :(before "End Initialize Op Names")
 put_new(Name, "09", "rm32 = bitwise OR of r32 with rm32 (or)");
 
-:(scenario or_r32_with_r32)
-% Reg[EAX].i = 0x0a0b0c0d;
-% Reg[EBX].i = 0xa0b0c0d0;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  09  d8                                      # or EBX with destination EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: or EBX with r/m32
-+run: r/m32 is EAX
-+run: storing 0xaabbccdd
+:(code)
+void test_or_r32_with_r32() {
+  Reg[EAX].i = 0x0a0b0c0d;
+  Reg[EBX].i = 0xa0b0c0d0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  09     d8                                    \n"  // or EBX with destination EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: or EBX with r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing 0xaabbccdd\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x09: {  // or r32 with r/m32
@@ -425,16 +515,22 @@ case 0x09: {  // or r32 with r/m32
 :(before "End Initialize Op Names")
 put_new(Name, "31", "rm32 = bitwise XOR of r32 with rm32 (xor)");
 
-:(scenario xor_r32_with_r32)
-% Reg[EAX].i = 0x0a0b0c0d;
-% Reg[EBX].i = 0xaabbc0d0;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  31  d8                                      # xor EBX with destination EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: xor EBX with r/m32
-+run: r/m32 is EAX
-+run: storing 0xa0b0ccdd
+:(code)
+void test_xor_r32_with_r32() {
+  Reg[EAX].i = 0x0a0b0c0d;
+  Reg[EBX].i = 0xaabbc0d0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  31     d8                                    \n"  // xor EBX with destination EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: xor EBX with r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing 0xa0b0ccdd\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x31: {  // xor r32 with r/m32
@@ -448,16 +544,22 @@ case 0x31: {  // xor r32 with r/m32
 
 //:: not
 
-:(scenario not_r32)
-% Reg[EBX].i = 0x0f0f00ff;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  f7  d3                                      # not EBX
-# ModR/M in binary: 11 (direct mode) 010 (subop not) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: not
-+run: storing 0xf0f0ff00
+:(code)
+void test_not_r32() {
+  Reg[EBX].i = 0x0f0f00ff;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  f7     d3                                    \n"  // not EBX
+      // ModR/M in binary: 11 (direct mode) 010 (subop not) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: not\n"
+      "run: storing 0xf0f0ff00\n"
+  );
+}
 
 :(before "End Op f7 Subops")
 case 2: {  // not r/m32
@@ -475,16 +577,22 @@ case 2: {  // not r/m32
 :(before "End Initialize Op Names")
 put_new(Name, "39", "compare: set SF if rm32 < r32 (cmp)");
 
-:(scenario compare_r32_with_r32_greater)
-% Reg[EAX].i = 0x0a0b0c0d;
-% Reg[EBX].i = 0x0a0b0c07;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  39  d8                                      # compare EBX with EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: compare EBX with r/m32
-+run: r/m32 is EAX
-+run: SF=0; ZF=0; OF=0
+:(code)
+void test_compare_r32_with_r32_greater() {
+  Reg[EAX].i = 0x0a0b0c0d;
+  Reg[EBX].i = 0x0a0b0c07;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     d8                                    \n"  // compare EBX with EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=0; ZF=0; OF=0\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x39: {  // set SF if r/m32 < r32
@@ -502,42 +610,59 @@ case 0x39: {  // set SF if r/m32 < r32
   break;
 }
 
-:(scenario compare_r32_with_r32_lesser)
-% Reg[EAX].i = 0x0a0b0c07;
-% Reg[EBX].i = 0x0a0b0c0d;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  39  d8                                      # compare EBX with EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: compare EBX with r/m32
-+run: r/m32 is EAX
-+run: SF=1; ZF=0; OF=0
-
-:(scenario compare_r32_with_r32_equal)
-% Reg[EAX].i = 0x0a0b0c0d;
-% Reg[EBX].i = 0x0a0b0c0d;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  39  d8                                      # compare EBX with EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: compare EBX with r/m32
-+run: r/m32 is EAX
-+run: SF=0; ZF=1; OF=0
+:(code)
+void test_compare_r32_with_r32_lesser() {
+  Reg[EAX].i = 0x0a0b0c07;
+  Reg[EBX].i = 0x0a0b0c0d;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     d8                                    \n"  // compare EBX with EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=1; ZF=0; OF=0\n"
+  );
+}
+
+void test_compare_r32_with_r32_equal() {
+  Reg[EAX].i = 0x0a0b0c0d;
+  Reg[EBX].i = 0x0a0b0c0d;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     d8                                    \n"  // compare EBX with EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=0; ZF=1; OF=0\n"
+  );
+}
 
 //:: copy (mov)
 
 :(before "End Initialize Op Names")
 put_new(Name, "89", "copy r32 to rm32 (mov)");
 
-:(scenario copy_r32_to_r32)
-% Reg[EBX].i = 0xaf;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  89  d8                                      # copy EBX to EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: copy EBX to r/m32
-+run: r/m32 is EAX
-+run: storing 0x000000af
+:(code)
+void test_copy_r32_to_r32() {
+  Reg[EBX].i = 0xaf;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  89     d8                                    \n"  // copy EBX to EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy EBX to r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing 0x000000af\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x89: {  // copy r32 to r/m32
@@ -555,17 +680,23 @@ case 0x89: {  // copy r32 to r/m32
 :(before "End Initialize Op Names")
 put_new(Name, "87", "swap the contents of r32 and rm32 (xchg)");
 
-:(scenario xchg_r32_with_r32)
-% Reg[EBX].i = 0xaf;
-% Reg[EAX].i = 0x2e;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  87  d8                                      # exchange EBX with EAX
-# ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
-+run: exchange EBX with r/m32
-+run: r/m32 is EAX
-+run: storing 0x000000af in r/m32
-+run: storing 0x0000002e in EBX
+:(code)
+void test_xchg_r32_with_r32() {
+  Reg[EBX].i = 0xaf;
+  Reg[EAX].i = 0x2e;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  87     d8                                    \n"  // exchange EBX with EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: exchange EBX with r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing 0x000000af in r/m32\n"
+      "run: storing 0x0000002e in EBX\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x87: {  // exchange r32 with r/m32
@@ -593,13 +724,19 @@ put_new(Name, "45", "increment EBP (inc)");
 put_new(Name, "46", "increment ESI (inc)");
 put_new(Name, "47", "increment EDI (inc)");
 
-:(scenario increment_r32)
-% Reg[ECX].u = 0x1f;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  41                                          # increment ECX
-+run: increment ECX
-+run: storing value 0x00000020
+:(code)
+void test_increment_r32() {
+  Reg[ECX].u = 0x1f;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  41                                           \n"  // increment ECX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: increment ECX\n"
+      "run: storing value 0x00000020\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x40:
@@ -620,15 +757,21 @@ case 0x47: {  // increment r32
 :(before "End Initialize Op Names")
 put_new(Name, "ff", "increment/decrement/jump/push/call rm32 based on subop (inc/dec/jmp/push/call)");
 
-:(scenario increment_rm32)
-% Reg[EAX].u = 0x20;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  ff  c0                                      # increment EAX
-# ModR/M in binary: 11 (direct mode) 000 (subop inc) 000 (EAX)
-+run: increment r/m32
-+run: r/m32 is EAX
-+run: storing value 0x00000021
+:(code)
+void test_increment_rm32() {
+  Reg[EAX].u = 0x20;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  ff     c0                                    \n"  // increment EAX
+      // ModR/M in binary: 11 (direct mode) 000 (subop inc) 000 (EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: increment r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing value 0x00000021\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xff: {
@@ -663,13 +806,19 @@ put_new(Name, "4d", "decrement EBP (dec)");
 put_new(Name, "4e", "decrement ESI (dec)");
 put_new(Name, "4f", "decrement EDI (dec)");
 
-:(scenario decrement_r32)
-% Reg[ECX].u = 0x1f;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  49                                          # decrement ECX
-+run: decrement ECX
-+run: storing value 0x0000001e
+:(code)
+void test_decrement_r32() {
+  Reg[ECX].u = 0x1f;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  49                                           \n"  // decrement ECX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: decrement ECX\n"
+      "run: storing value 0x0000001e\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x48:
@@ -687,15 +836,21 @@ case 0x4f: {  // decrement r32
   break;
 }
 
-:(scenario decrement_rm32)
-% Reg[EAX].u = 0x20;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  ff  c8                                      # decrement EAX
-# ModR/M in binary: 11 (direct mode) 001 (subop inc) 000 (EAX)
-+run: decrement r/m32
-+run: r/m32 is EAX
-+run: storing value 0x0000001f
+:(code)
+void test_decrement_rm32() {
+  Reg[EAX].u = 0x20;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  ff     c8                                    \n"  // decrement EAX
+      // ModR/M in binary: 11 (direct mode) 001 (subop inc) 000 (EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: decrement r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: storing value 0x0000001f\n"
+  );
+}
 
 :(before "End Op ff Subops")
 case 1: {  // decrement r/m32
@@ -718,15 +873,21 @@ put_new(Name, "55", "push EBP to stack (push)");
 put_new(Name, "56", "push ESI to stack (push)");
 put_new(Name, "57", "push EDI to stack (push)");
 
-:(scenario push_r32)
-% Reg[ESP].u = 0x64;
-% Reg[EBX].i = 0x0000000a;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  53                                          # push EBX to stack
-+run: push EBX
-+run: decrementing ESP to 0x00000060
-+run: pushing value 0x0000000a
+:(code)
+void test_push_r32() {
+  Reg[ESP].u = 0x64;
+  Reg[EBX].i = 0x0000000a;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  53                                           \n"  // push EBX to stack
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: push EBX\n"
+      "run: decrementing ESP to 0x00000060\n"
+      "run: pushing value 0x0000000a\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x50:
@@ -756,18 +917,24 @@ put_new(Name, "5d", "pop top of stack to EBP (pop)");
 put_new(Name, "5e", "pop top of stack to ESI (pop)");
 put_new(Name, "5f", "pop top of stack to EDI (pop)");
 
-:(scenario pop_r32)
-% Reg[ESP].u = 0x02000000;
-% Mem.push_back(vma(0x02000000));  // manually allocate memory
-% write_mem_i32(0x02000000, 0x0000000a);  // ..before this write
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  5b                                          # pop stack to EBX
-== 0x2000  # data segment
-0a 00 00 00  # 0x0a
-+run: pop into EBX
-+run: popping value 0x0000000a
-+run: incrementing ESP to 0x02000004
+:(code)
+void test_pop_r32() {
+  Reg[ESP].u = 0x02000000;
+  Mem.push_back(vma(0x02000000));  // manually allocate memory
+  write_mem_i32(0x02000000, 0x0000000a);  // ..before this write
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  5b                                           \n"  // pop stack to EBX
+      "== 0x2000\n"  // data segment
+      "0a 00 00 00\n"  // 0x0000000a
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: pop into EBX\n"
+      "run: popping value 0x0000000a\n"
+      "run: incrementing ESP to 0x02000004\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x58:
diff --git a/subx/014indirect_addressing.cc b/subx/014indirect_addressing.cc
index 448f4231..f591f0cf 100644
--- a/subx/014indirect_addressing.cc
+++ b/subx/014indirect_addressing.cc
@@ -1,18 +1,23 @@
 //: operating on memory at the address provided by some register
 //: we'll now start providing data in a separate segment
 
-:(scenario add_r32_to_mem_at_r32)
-% Reg[EBX].i = 0x10;
-% Reg[EAX].i = 0x2000;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  18                                     # add EBX to *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0x00000011
+void test_add_r32_to_mem_at_r32() {
+  Reg[EBX].i = 0x10;
+  Reg[EAX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01  18                                       \n"  // add EBX to *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Mod Special-cases(addr)")
 case 0:  // indirect addressing
@@ -30,18 +35,24 @@ case 0:  // indirect addressing
 :(before "End Initialize Op Names")
 put_new(Name, "03", "add rm32 to r32 (add)");
 
-:(scenario add_mem_at_r32_to_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0x10;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  03  18                                      # add *EAX to EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add r/m32 to EBX
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0x00000011
+:(code)
+void test_add_mem_at_r32_to_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x10;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  03  18                                       \n"  // add *EAX to EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add r/m32 to EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x03: {  // add r/m32 to r32
@@ -55,36 +66,48 @@ case 0x03: {  // add r/m32 to r32
 
 //:: subtract
 
-:(scenario subtract_r32_from_mem_at_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 1;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  29  18                                      # subtract EBX from *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0a 00 00 00  # 10
-+run: subtract EBX from r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0x00000009
+:(code)
+void test_subtract_r32_from_mem_at_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  29  18                                       \n"  // subtract EBX from *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0a 00 00 00\n"  // 0x0000000a
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract EBX from r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0x00000009\n"
+  );
+}
 
 //:
 
 :(before "End Initialize Op Names")
 put_new(Name, "2b", "subtract rm32 from r32 (sub)");
 
-:(scenario subtract_mem_at_r32_from_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 10;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  2b  18                                      # subtract *EAX from EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: subtract r/m32 from EBX
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0x00000009
+:(code)
+void test_subtract_mem_at_r32_from_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 10;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  2b     18                                    \n"  // subtract *EAX from EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract r/m32 from EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0x00000009\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x2b: {  // subtract r/m32 from r32
@@ -97,37 +120,48 @@ case 0x2b: {  // subtract r/m32 from r32
 }
 
 //:: and
-
-:(scenario and_r32_with_mem_at_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0xff;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  21  18                                      # and EBX with *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c 0b 0a  # 0x0a0b0c0d
-+run: and EBX with r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0x0000000d
+:(code)
+void test_and_r32_with_mem_at_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0xff;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  21     18                                    \n"  // and EBX with *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0d 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: and EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0x0000000d\n"
+  );
+}
 
 //:
 
 :(before "End Initialize Op Names")
 put_new(Name, "23", "r32 = bitwise AND of r32 with rm32 (and)");
 
-:(scenario and_mem_at_r32_with_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0x0a0b0c0d;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  23  18                                      # and *EAX with EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-ff 00 00 00  # 0xff
-+run: and r/m32 with EBX
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0x0000000d
+:(code)
+void test_and_mem_at_r32_with_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x0a0b0c0d;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  23     18                                    \n"  // and *EAX with EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "ff 00 00 00\n"  // 0x000000ff
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: and r/m32 with EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0x0000000d\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x23: {  // and r/m32 with r32
@@ -141,36 +175,48 @@ case 0x23: {  // and r/m32 with r32
 
 //:: or
 
-:(scenario or_r32_with_mem_at_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0xa0b0c0d0;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  09  18                                      # or EBX with *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c 0b 0a  # 0x0a0b0c0d
-+run: or EBX with r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0xaabbccdd
+:(code)
+void test_or_r32_with_mem_at_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0xa0b0c0d0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  09  18                                      #\n"  // EBX with *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0d 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: or EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0xaabbccdd\n"
+  );
+}
 
 //:
 
 :(before "End Initialize Op Names")
 put_new(Name, "0b", "r32 = bitwise OR of r32 with rm32 (or)");
 
-:(scenario or_mem_at_r32_with_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0xa0b0c0d0;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  0b  18                                      # or *EAX with EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c 0b 0a  # 0x0a0b0c0d
-+run: or r/m32 with EBX
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0xaabbccdd
+:(code)
+void test_or_mem_at_r32_with_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0xa0b0c0d0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0b     18                                    \n"  // or *EAX with EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0d 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: or r/m32 with EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0xaabbccdd\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x0b: {  // or r/m32 with r32
@@ -184,36 +230,47 @@ case 0x0b: {  // or r/m32 with r32
 
 //:: xor
 
-:(scenario xor_r32_with_mem_at_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0xa0b0c0d0;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  31  18                                      # xor EBX with *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c bb aa  # 0xaabb0c0d
-+run: xor EBX with r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0x0a0bccdd
+:(code)
+void test_xor_r32_with_mem_at_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0xa0b0c0d0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  31     18                                    \n"  // xor EBX with *EAX
+      "== 0x2000\n"  // data segment
+      "0d 0c bb aa\n"  // 0xaabb0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: xor EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0x0a0bccdd\n"
+  );
+}
 
 //:
 
 :(before "End Initialize Op Names")
 put_new(Name, "33", "r32 = bitwise XOR of r32 with rm32 (xor)");
 
-:(scenario xor_mem_at_r32_with_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0xa0b0c0d0;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  33  18                                      # xor *EAX with EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c 0b 0a  # 0x0a0b0c0d
-+run: xor r/m32 with EBX
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0xaabbccdd
+:(code)
+void test_xor_mem_at_r32_with_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0xa0b0c0d0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  33     18                                    \n"  // xor *EAX with EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0d 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: xor r/m32 with EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0xaabbccdd\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x33: {  // xor r/m32 with r32
@@ -227,77 +284,107 @@ case 0x33: {  // xor r/m32 with r32
 
 //:: not
 
-:(scenario not_of_mem_at_r32)
-% Reg[EBX].i = 0x2000;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  f7  13                                      # not *EBX
-# ModR/M in binary: 00 (indirect mode) 010 (subop not) 011 (dest EBX)
-== 0x2000  # data segment
-ff 00 0f 0f  # 0x0f0f00ff
-+run: operate on r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: subop: not
-+run: storing 0xf0f0ff00
+:(code)
+void test_not_of_mem_at_r32() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  f7     13                                    \n"  // not *EBX
+      // ModR/M in binary: 00 (indirect mode) 010 (subop not) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "ff 00 0f 0f\n"  // 0x0f0f00ff
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: subop: not\n"
+      "run: storing 0xf0f0ff00\n"
+  );
+}
 
 //:: compare (cmp)
 
-:(scenario compare_mem_at_r32_with_r32_greater)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0x0a0b0c07;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  39  18                                      # compare EBX with *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c 0b 0a  # 0x0a0b0c0d
-+run: compare EBX with r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: SF=0; ZF=0; OF=0
-
-:(scenario compare_mem_at_r32_with_r32_lesser)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0x0a0b0c0d;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  39  18                                      # compare EBX with *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-07 0c 0b 0a  # 0x0a0b0c0d
-+run: compare EBX with r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: SF=1; ZF=0; OF=0
-
-:(scenario compare_mem_at_r32_with_r32_equal)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0x0a0b0c0d;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  39  18                                      # compare EBX with *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c 0b 0a  # 0x0a0b0c0d
-+run: compare EBX with r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: SF=0; ZF=1; OF=0
+:(code)
+void test_compare_mem_at_r32_with_r32_greater() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x0a0b0c07;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     18                                    \n"  // compare EBX with *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0d 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: SF=0; ZF=0; OF=0\n"
+  );
+}
+
+:(code)
+void test_compare_mem_at_r32_with_r32_lesser() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x0a0b0c0d;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     18                                    \n"  // compare EBX with *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "07 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: SF=1; ZF=0; OF=0\n"
+  );
+}
+
+:(code)
+void test_compare_mem_at_r32_with_r32_equal() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x0a0b0c0d;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     18                                    \n"  // compare EBX with *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0d 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: SF=0; ZF=1; OF=0\n"
+  );
+}
 
 //:
 
 :(before "End Initialize Op Names")
 put_new(Name, "3b", "compare: set SF if r32 < rm32 (cmp)");
 
-:(scenario compare_r32_with_mem_at_r32_greater)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0x0a0b0c0d;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  3b  18                                      # compare *EAX with EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-07 0c 0b 0a  # 0x0a0b0c0d
-+run: compare r/m32 with EBX
-+run: effective address is 0x00002000 (EAX)
-+run: SF=0; ZF=0; OF=0
+:(code)
+void test_compare_r32_with_mem_at_r32_greater() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x0a0b0c0d;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3b     18                                    \n"  // compare *EAX with EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "07 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare r/m32 with EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: SF=0; ZF=0; OF=0\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x3b: {  // set SF if r32 < r/m32
@@ -315,61 +402,84 @@ case 0x3b: {  // set SF if r32 < r/m32
   break;
 }
 
-:(scenario compare_r32_with_mem_at_r32_lesser)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0x0a0b0c07;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  3b  18                                      # compare *EAX with EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c 0b 0a  # 0x0a0b0c0d
-+run: compare r/m32 with EBX
-+run: effective address is 0x00002000 (EAX)
-+run: SF=1; ZF=0; OF=0
-
-:(scenario compare_r32_with_mem_at_r32_equal)
-% Reg[EAX].i = 0x2000;
-% Reg[EBX].i = 0x0a0b0c0d;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  3b  18                                      # compare *EAX with EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-== 0x2000  # data segment
-0d 0c 0b 0a  # 0x0a0b0c0d
-+run: compare r/m32 with EBX
-+run: effective address is 0x00002000 (EAX)
-+run: SF=0; ZF=1; OF=0
+:(code)
+void test_compare_r32_with_mem_at_r32_lesser() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x0a0b0c07;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3b     18                                    \n"  // compare *EAX with EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0d 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare r/m32 with EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: SF=1; ZF=0; OF=0\n"
+  );
+}
+
+:(code)
+void test_compare_r32_with_mem_at_r32_equal() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x0a0b0c0d;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3b     18                                    \n"  // compare *EAX with EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "0d 0c 0b 0a\n"  // 0x0a0b0c0d
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare r/m32 with EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: SF=0; ZF=1; OF=0\n"
+  );
+}
 
 //:: copy (mov)
 
-:(scenario copy_r32_to_mem_at_r32)
-% Reg[EBX].i = 0xaf;
-% Reg[EAX].i = 0x60;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  89  18                                      # copy EBX to *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
-+run: copy EBX to r/m32
-+run: effective address is 0x00000060 (EAX)
-+run: storing 0x000000af
+:(code)
+void test_copy_r32_to_mem_at_r32() {
+  Reg[EBX].i = 0xaf;
+  Reg[EAX].i = 0x60;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  89     18                                    \n"  // copy EBX to *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy EBX to r/m32\n"
+      "run: effective address is 0x00000060 (EAX)\n"
+      "run: storing 0x000000af\n"
+  );
+}
 
 //:
 
 :(before "End Initialize Op Names")
 put_new(Name, "8b", "copy rm32 to r32 (mov)");
 
-:(scenario copy_mem_at_r32_to_r32)
-% Reg[EAX].i = 0x2000;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  8b  18                                      # copy *EAX to EBX
-# ModR/M in binary: 00 (indirect mode) 011 (src EBX) 000 (dest EAX)
-== 0x2000  # data segment
-af 00 00 00  # 0xaf
-+run: copy r/m32 to EBX
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0x000000af
+:(code)
+void test_copy_mem_at_r32_to_r32() {
+  Reg[EAX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  8b     18                                    \n"  // copy *EAX to EBX
+      "== 0x2000\n"  // data segment
+      "af 00 00 00\n"  // 0x000000af
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy r/m32 to EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0x000000af\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x8b: {  // copy r32 to r/m32
@@ -384,22 +494,28 @@ case 0x8b: {  // copy r32 to r/m32
 
 //:: jump
 
-:(scenario jump_mem_at_r32)
-% Reg[EAX].i = 0x2000;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  ff  20                                      # jump to *EAX
-# ModR/M in binary: 00 (indirect mode) 100 (jump to r/m32) 000 (src EAX)
-  05                              00 00 00 01
-  05                              00 00 00 02
-== 0x2000  # data segment
-08 00 00 00  # 8
-+run: 0x00000001 opcode: ff
-+run: jump to r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: jumping to 0x00000008
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
+:(code)
+void test_jump_mem_at_r32() {
+  Reg[EAX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  ff     20                                    \n"  // jump to *EAX
+      // ModR/M in binary: 00 (indirect mode) 100 (jump to r/m32) 000 (src EAX)
+      "  05                                 00 00 00 01\n"
+      "  05                                 00 00 00 02\n"
+      "== 0x2000\n"  // data segment
+      "08 00 00 00\n"  // 0x00000008
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: ff\n"
+      "run: jump to r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: jumping to 0x00000008\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
 
 :(before "End Op ff Subops")
 case 4: {  // jump to r/m32
@@ -412,19 +528,24 @@ case 4: {  // jump to r/m32
 
 //:: push
 
-:(scenario push_mem_at_r32)
-% Reg[EAX].i = 0x2000;
-% Reg[ESP].u = 0x14;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  ff  30                                      # push *EAX to stack
-# ModR/M in binary: 00 (indirect mode) 110 (push r/m32) 000 (src EAX)
-== 0x2000  # data segment
-af 00 00 00  # 0xaf
-+run: push r/m32
-+run: effective address is 0x00002000 (EAX)
-+run: decrementing ESP to 0x00000010
-+run: pushing value 0x000000af
+:(code)
+void test_push_mem_at_r32() {
+  Reg[EAX].i = 0x2000;
+  Reg[ESP].u = 0x14;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  ff     30                                    \n"  // push *EAX to stack
+      "== 0x2000\n"  // data segment
+      "af 00 00 00\n"  // 0x000000af
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: push r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: decrementing ESP to 0x00000010\n"
+      "run: pushing value 0x000000af\n"
+  );
+}
 
 :(before "End Op ff Subops")
 case 6: {  // push r/m32 to stack
@@ -439,19 +560,25 @@ case 6: {  // push r/m32 to stack
 :(before "End Initialize Op Names")
 put_new(Name, "8f", "pop top of stack to rm32 (pop)");
 
-:(scenario pop_mem_at_r32)
-% Reg[EAX].i = 0x60;
-% Reg[ESP].u = 0x2000;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  8f  00                                      # pop stack into *EAX
-# ModR/M in binary: 00 (indirect mode) 000 (pop r/m32) 000 (dest EAX)
-== 0x2000  # data segment
-30 00 00 00  # 0x30
-+run: pop into r/m32
-+run: effective address is 0x00000060 (EAX)
-+run: popping value 0x00000030
-+run: incrementing ESP to 0x00002004
+:(code)
+void test_pop_mem_at_r32() {
+  Reg[EAX].i = 0x60;
+  Reg[ESP].u = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  8f     00                                    \n"  // pop stack into *EAX
+      // ModR/M in binary: 00 (indirect mode) 000 (pop r/m32) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "30 00 00 00\n"  // 0x00000030
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: pop into r/m32\n"
+      "run: effective address is 0x00000060 (EAX)\n"
+      "run: popping value 0x00000030\n"
+      "run: incrementing ESP to 0x00002004\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x8f: {  // pop stack into r/m32
@@ -470,17 +597,23 @@ case 0x8f: {  // pop stack into r/m32
 
 //:: special-case for loading address from disp32 rather than register
 
-:(scenario add_r32_to_mem_at_displacement)
-% Reg[EBX].i = 0x10;  // source
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  1d            00 20 00 00              # add EBX to *0x2000
-# ModR/M in binary: 00 (indirect mode) 011 (src EBX) 101 (dest in disp32)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is 0x00002000 (disp32)
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_displacement() {
+  Reg[EBX].i = 0x10;  // source
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     1d            00 20 00 00             \n"  // add EBX to *0x2000
+      // ModR/M in binary: 00 (indirect mode) 011 (src EBX) 101 (dest in disp32)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is 0x00002000 (disp32)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Mod 0 Special-cases(addr)")
 case 5:  // exception: mod 0b00 rm 0b101 => incoming disp32
@@ -490,19 +623,25 @@ case 5:  // exception: mod 0b00 rm 0b101 => incoming disp32
 
 //:
 
-:(scenario add_r32_to_mem_at_r32_plus_disp8)
-% Reg[EBX].i = 0x10;  // source
-% Reg[EAX].i = 0x1ffe;  // dest
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  58            02                       # add EBX to *(EAX+2)
-# ModR/M in binary: 01 (indirect+disp8 mode) 011 (src EBX) 000 (dest EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00001ffe (EAX)
-+run: effective address is 0x00002000 (after adding disp8)
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_r32_plus_disp8() {
+  Reg[EBX].i = 0x10;  // source
+  Reg[EAX].i = 0x1ffe;  // dest
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     58            02                      \n"  // add EBX to *(EAX+2)
+      // ModR/M in binary: 01 (indirect+disp8 mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00001ffe (EAX)\n"
+      "run: effective address is 0x00002000 (after adding disp8)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Mod Special-cases(addr)")
 case 1:  // indirect + disp8 addressing
@@ -519,35 +658,47 @@ case 1:  // indirect + disp8 addressing
   }
   break;
 
-:(scenario add_r32_to_mem_at_r32_plus_negative_disp8)
-% Reg[EBX].i = 0x10;  // source
-% Reg[EAX].i = 0x2001;  // dest
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  58            ff                       # add EBX to *(EAX-1)
-# ModR/M in binary: 01 (indirect+disp8 mode) 011 (src EBX) 000 (dest EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00002001 (EAX)
-+run: effective address is 0x00002000 (after adding disp8)
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_r32_plus_negative_disp8() {
+  Reg[EBX].i = 0x10;  // source
+  Reg[EAX].i = 0x2001;  // dest
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     58            ff                      \n"  // add EBX to *(EAX-1)
+      // ModR/M in binary: 01 (indirect+disp8 mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00002001 (EAX)\n"
+      "run: effective address is 0x00002000 (after adding disp8)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 //:
 
-:(scenario add_r32_to_mem_at_r32_plus_disp32)
-% Reg[EBX].i = 0x10;  // source
-% Reg[EAX].i = 0x1ffe;  // dest
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  98            02 00 00 00              # add EBX to *(EAX+2)
-# ModR/M in binary: 10 (indirect+disp32 mode) 011 (src EBX) 000 (dest EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00001ffe (EAX)
-+run: effective address is 0x00002000 (after adding disp32)
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_r32_plus_disp32() {
+  Reg[EBX].i = 0x10;  // source
+  Reg[EAX].i = 0x1ffe;  // dest
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     98            02 00 00 00             \n"  // add EBX to *(EAX+2)
+      // ModR/M in binary: 10 (indirect+disp32 mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00001ffe (EAX)\n"
+      "run: effective address is 0x00002000 (after adding disp32)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Mod Special-cases(addr)")
 case 2:  // indirect + disp32 addressing
@@ -564,33 +715,45 @@ case 2:  // indirect + disp32 addressing
   }
   break;
 
-:(scenario add_r32_to_mem_at_r32_plus_negative_disp32)
-% Reg[EBX].i = 0x10;  // source
-% Reg[EAX].i = 0x2001;  // dest
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  98            ff ff ff ff              # add EBX to *(EAX-1)
-# ModR/M in binary: 10 (indirect+disp32 mode) 011 (src EBX) 000 (dest EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00002001 (EAX)
-+run: effective address is 0x00002000 (after adding disp32)
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_r32_plus_negative_disp32() {
+  Reg[EBX].i = 0x10;  // source
+  Reg[EAX].i = 0x2001;  // dest
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     98            ff ff ff ff             \n"  // add EBX to *(EAX-1)
+      // ModR/M in binary: 10 (indirect+disp32 mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00002001 (EAX)\n"
+      "run: effective address is 0x00002000 (after adding disp32)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 //:: copy address (lea)
 
 :(before "End Initialize Op Names")
 put_new(Name, "8d", "copy address in rm32 into r32 (lea)");
 
-:(scenario copy_address)
-% Reg[EAX].u = 0x2000;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  8d  18
-# ModR/M in binary: 00 (indirect mode) 011 (dest EBX) 000 (src EAX)
-+run: copy address into EBX
-+run: effective address is 0x00002000 (EAX)
+:(code)
+void test_copy_address() {
+  Reg[EAX].u = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  8d     18                                    \n"  // copy address in EAX into EBX
+      // ModR/M in binary: 00 (indirect mode) 011 (dest EBX) 000 (src EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy address into EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x8d: {  // copy address of m32 to r32
diff --git a/subx/015immediate_addressing.cc b/subx/015immediate_addressing.cc
index 93dd699b..18cd5334 100644
--- a/subx/015immediate_addressing.cc
+++ b/subx/015immediate_addressing.cc
@@ -3,17 +3,23 @@
 :(before "End Initialize Op Names")
 put_new(Name, "81", "combine rm32 with imm32 based on subop (add/sub/and/or/xor/cmp)");
 
-:(scenario add_imm32_to_r32)
-% Reg[EBX].i = 1;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  81  c3                          0a 0b 0c 0d  # add 0x0d0c0b0a to EBX
-# ModR/M in binary: 11 (direct mode) 000 (add imm32) 011 (dest EBX)
-+run: combine imm32 with r/m32
-+run: r/m32 is EBX
-+run: imm32 is 0x0d0c0b0a
-+run: subop add
-+run: storing 0x0d0c0b0b
+:(code)
+void test_add_imm32_to_r32() {
+  Reg[EBX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     c3                          0a 0b 0c 0d\n"  // add 0x0d0c0b0a to EBX
+      // ModR/M in binary: 11 (direct mode) 000 (add imm32) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: subop add\n"
+      "run: storing 0x0d0c0b0b\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x81: {  // combine imm32 with r/m32
@@ -38,32 +44,44 @@ case 0x81: {  // combine imm32 with r/m32
 
 //:
 
-:(scenario add_imm32_to_mem_at_r32)
-% Reg[EBX].i = 0x2000;
-== 0x01  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  81  03                          0a 0b 0c 0d  # add 0x0d0c0b0a to *EBX
-# ModR/M in binary: 00 (indirect mode) 000 (add imm32) 011 (dest EBX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: combine imm32 with r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: imm32 is 0x0d0c0b0a
-+run: subop add
-+run: storing 0x0d0c0b0b
+:(code)
+void test_add_imm32_to_mem_at_r32() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     03                          0a 0b 0c 0d \n"  // add 0x0d0c0b0a to *EBX
+      // ModR/M in binary: 00 (indirect mode) 000 (add imm32) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: subop add\n"
+      "run: storing 0x0d0c0b0b\n"
+  );
+}
 
 //:: subtract
 
 :(before "End Initialize Op Names")
 put_new(Name, "2d", "subtract imm32 from EAX (sub)");
 
-:(scenario subtract_imm32_from_eax)
-% Reg[EAX].i = 0x0d0c0baa;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  2d                              0a 0b 0c 0d  # subtract 0x0d0c0b0a from EAX
-+run: subtract imm32 0x0d0c0b0a from EAX
-+run: storing 0x000000a0
+:(code)
+void test_subtract_imm32_from_eax() {
+  Reg[EAX].i = 0x0d0c0baa;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  2d                                 0a 0b 0c 0d \n"  // subtract 0x0d0c0b0a from EAX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract imm32 0x0d0c0b0a from EAX\n"
+      "run: storing 0x000000a0\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x2d: {  // subtract imm32 from EAX
@@ -75,19 +93,25 @@ case 0x2d: {  // subtract imm32 from EAX
 
 //:
 
-:(scenario subtract_imm32_from_mem_at_r32)
-% Reg[EBX].i = 0x2000;
-== 0x01  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  81  2b                          01 00 00 00  # subtract 1 from *EBX
-# ModR/M in binary: 00 (indirect mode) 101 (subtract imm32) 011 (dest EBX)
-== 0x2000  # data segment
-0a 00 00 00  # 10
-+run: combine imm32 with r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: imm32 is 0x00000001
-+run: subop subtract
-+run: storing 0x00000009
+:(code)
+void test_subtract_imm32_from_mem_at_r32() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     2b                          01 00 00 00 \n"  // subtract 1 from *EBX
+      // ModR/M in binary: 00 (indirect mode) 101 (subtract imm32) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "0a 00 00 00\n"  // 0x0000000a
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: imm32 is 0x00000001\n"
+      "run: subop subtract\n"
+      "run: storing 0x00000009\n"
+  );
+}
 
 :(before "End Op 81 Subops")
 case 5: {
@@ -98,33 +122,45 @@ case 5: {
 
 //:
 
-:(scenario subtract_imm32_from_r32)
-% Reg[EBX].i = 10;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  81  eb                          01 00 00 00  # subtract 1 from EBX
-# ModR/M in binary: 11 (direct mode) 101 (subtract imm32) 011 (dest EBX)
-+run: combine imm32 with r/m32
-+run: r/m32 is EBX
-+run: imm32 is 0x00000001
-+run: subop subtract
-+run: storing 0x00000009
+:(code)
+void test_subtract_imm32_from_r32() {
+  Reg[EBX].i = 10;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     eb                          01 00 00 00 \n"  // subtract 1 from EBX
+      // ModR/M in binary: 11 (direct mode) 101 (subtract imm32) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x00000001\n"
+      "run: subop subtract\n"
+      "run: storing 0x00000009\n"
+  );
+}
 
 //:: shift left
 
 :(before "End Initialize Op Names")
 put_new(Name, "c1", "shift rm32 by imm8 bits depending on subop (sal/sar/shl/shr)");
 
-:(scenario shift_left_r32_with_imm8)
-% Reg[EBX].i = 13;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c1  e3                          01          # negate EBX
-# ModR/M in binary: 11 (direct mode) 100 (subop shift left) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift left by CL bits
-+run: storing 0x0000001a
+:(code)
+void test_shift_left_r32_with_imm8() {
+  Reg[EBX].i = 13;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c1     e3                          01          \n"  // shift EBX left by 1 bit
+      // ModR/M in binary: 11 (direct mode) 100 (subop shift left) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift left by CL bits\n"
+      "run: storing 0x0000001a\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xc1: {
@@ -158,16 +194,22 @@ case 0xc1: {
 
 //:: shift right arithmetic
 
-:(scenario shift_right_arithmetic_r32_with_imm8)
-% Reg[EBX].i = 26;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c1  fb                          01          # negate EBX
-# ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while preserving sign
-+run: storing 0x0000000d
+:(code)
+void test_shift_right_arithmetic_r32_with_imm8() {
+  Reg[EBX].i = 26;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c1     fb                          01          \n"  // shift EBX right by 1 bit
+      // ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while preserving sign\n"
+      "run: storing 0x0000000d\n"
+  );
+}
 
 :(before "End Op c1 Subops")
 case 7: {  // shift right r/m32 by CL, preserving sign
@@ -182,42 +224,60 @@ case 7: {  // shift right r/m32 by CL, preserving sign
   break;
 }
 
-:(scenario shift_right_arithmetic_odd_r32_with_imm8)
-% Reg[EBX].i = 27;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c1  fb                          01          # negate EBX
-# ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while preserving sign
-# result: 13
-+run: storing 0x0000000d
-
-:(scenario shift_right_arithmetic_negative_r32_with_imm8)
-% Reg[EBX].i = 0xfffffffd;  // -3
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c1  fb                          01          # negate EBX
-# ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while preserving sign
-# result: -2
-+run: storing 0xfffffffe
+:(code)
+void test_shift_right_arithmetic_odd_r32_with_imm8() {
+  Reg[EBX].i = 27;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c1     fb                          01          \n"  // shift EBX right by 1 bit
+      // ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while preserving sign\n"
+      // result: 13
+      "run: storing 0x0000000d\n"
+  );
+}
+
+:(code)
+void test_shift_right_arithmetic_negative_r32_with_imm8() {
+  Reg[EBX].i = 0xfffffffd;  // -3
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c1     fb                          01          \n"  // shift EBX right by 1 bit, while preserving sign
+      // ModR/M in binary: 11 (direct mode) 111 (subop shift right arithmetic) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while preserving sign\n"
+      // result: -2
+      "run: storing 0xfffffffe\n"
+  );
+}
 
 //:: shift right logical
 
-:(scenario shift_right_logical_r32_with_imm8)
-% Reg[EBX].i = 26;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c1  eb                          01          # negate EBX
-# ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while padding zeroes
-+run: storing 0x0000000d
+:(code)
+void test_shift_right_logical_r32_with_imm8() {
+  Reg[EBX].i = 26;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c1     eb                          01          \n"  // shift EBX right by 1 bit, while padding zeroes
+      // ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while padding zeroes\n"
+      "run: storing 0x0000000d\n"
+  );
+}
 
 :(before "End Op c1 Subops")
 case 5: {  // shift right r/m32 by CL, preserving sign
@@ -238,41 +298,58 @@ case 5: {  // shift right r/m32 by CL, preserving sign
   break;
 }
 
-:(scenario shift_right_logical_odd_r32_with_imm8)
-% Reg[EBX].i = 27;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c1  eb                          01          # negate EBX
-# ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while padding zeroes
-# result: 13
-+run: storing 0x0000000d
-
-:(scenario shift_right_logical_negative_r32_with_imm8)
-% Reg[EBX].i = 0xfffffffd;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c1  eb                          01          # negate EBX
-# ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
-+run: operate on r/m32
-+run: r/m32 is EBX
-+run: subop: shift right by CL bits, while padding zeroes
-+run: storing 0x7ffffffe
+:(code)
+void test_shift_right_logical_odd_r32_with_imm8() {
+  Reg[EBX].i = 27;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c1     eb                          01          \n"  // shift EBX right by 1 bit, while padding zeroes
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while padding zeroes\n"
+      // result: 13
+      "run: storing 0x0000000d\n"
+  );
+}
+
+:(code)
+void test_shift_right_logical_negative_r32_with_imm8() {
+  Reg[EBX].i = 0xfffffffd;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c1     eb                          01          \n"  // shift EBX right by 1 bit, while padding zeroes
+      // ModR/M in binary: 11 (direct mode) 101 (subop shift right logical) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: operate on r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: subop: shift right by CL bits, while padding zeroes\n"
+      "run: storing 0x7ffffffe\n"
+  );
+}
 
 //:: and
 
 :(before "End Initialize Op Names")
 put_new(Name, "25", "EAX = bitwise AND of imm32 with EAX (and)");
 
-:(scenario and_imm32_with_eax)
-% Reg[EAX].i = 0xff;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  25                              0a 0b 0c 0d  # and 0x0d0c0b0a with EAX
-+run: and imm32 0x0d0c0b0a with EAX
-+run: storing 0x0000000a
+:(code)
+void test_and_imm32_with_eax() {
+  Reg[EAX].i = 0xff;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  25                                 0a 0b 0c 0d \n"  // and 0x0d0c0b0a with EAX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: and imm32 0x0d0c0b0a with EAX\n"
+      "run: storing 0x0000000a\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x25: {  // and imm32 with EAX
@@ -284,19 +361,25 @@ case 0x25: {  // and imm32 with EAX
 
 //:
 
-:(scenario and_imm32_with_mem_at_r32)
-% Reg[EBX].i = 0x2000;
-== 0x01  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  81  23                          0a 0b 0c 0d  # and 0x0d0c0b0a with *EBX
-# ModR/M in binary: 00 (indirect mode) 100 (and imm32) 011 (dest EBX)
-== 0x2000  # data segment
-ff 00 00 00  # 0xff
-+run: combine imm32 with r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: imm32 is 0x0d0c0b0a
-+run: subop and
-+run: storing 0x0000000a
+:(code)
+void test_and_imm32_with_mem_at_r32() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     23                          0a 0b 0c 0d \n"  // and 0x0d0c0b0a with *EBX
+      // ModR/M in binary: 00 (indirect mode) 100 (and imm32) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "ff 00 00 00\n"  // 0x000000ff
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: subop and\n"
+      "run: storing 0x0000000a\n"
+  );
+}
 
 :(before "End Op 81 Subops")
 case 4: {
@@ -307,30 +390,42 @@ case 4: {
 
 //:
 
-:(scenario and_imm32_with_r32)
-% Reg[EBX].i = 0xff;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  81  e3                          0a 0b 0c 0d  # and 0x0d0c0b0a with EBX
-# ModR/M in binary: 11 (direct mode) 100 (and imm32) 011 (dest EBX)
-+run: combine imm32 with r/m32
-+run: r/m32 is EBX
-+run: imm32 is 0x0d0c0b0a
-+run: subop and
-+run: storing 0x0000000a
+:(code)
+void test_and_imm32_with_r32() {
+  Reg[EBX].i = 0xff;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     e3                          0a 0b 0c 0d \n"  // and 0x0d0c0b0a with EBX
+      // ModR/M in binary: 11 (direct mode) 100 (and imm32) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: subop and\n"
+      "run: storing 0x0000000a\n"
+  );
+}
 
 //:: or
 
 :(before "End Initialize Op Names")
 put_new(Name, "0d", "EAX = bitwise OR of imm32 with EAX (or)");
 
-:(scenario or_imm32_with_eax)
-% Reg[EAX].i = 0xd0c0b0a0;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  0d                              0a 0b 0c 0d  # or 0x0d0c0b0a with EAX
-+run: or imm32 0x0d0c0b0a with EAX
-+run: storing 0xddccbbaa
+:(code)
+void test_or_imm32_with_eax() {
+  Reg[EAX].i = 0xd0c0b0a0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0d                                 0a 0b 0c 0d \n"  // or 0x0d0c0b0a with EAX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: or imm32 0x0d0c0b0a with EAX\n"
+      "run: storing 0xddccbbaa\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x0d: {  // or imm32 with EAX
@@ -342,19 +437,25 @@ case 0x0d: {  // or imm32 with EAX
 
 //:
 
-:(scenario or_imm32_with_mem_at_r32)
-% Reg[EBX].i = 0x2000;
-== 0x01  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  81  0b                          0a 0b 0c 0d  # or 0x0d0c0b0a with *EBX
-# ModR/M in binary: 00 (indirect mode) 001 (or imm32) 011 (dest EBX)
-== 0x2000  # data segment
-a0 b0 c0 d0  # 0xd0c0b0a0
-+run: combine imm32 with r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: imm32 is 0x0d0c0b0a
-+run: subop or
-+run: storing 0xddccbbaa
+:(code)
+void test_or_imm32_with_mem_at_r32() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     0b                          0a 0b 0c 0d \n"  // or 0x0d0c0b0a with *EBX
+      // ModR/M in binary: 00 (indirect mode) 001 (or imm32) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "a0 b0 c0 d0\n"  // 0xd0c0b0a0
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: subop or\n"
+      "run: storing 0xddccbbaa\n"
+  );
+}
 
 :(before "End Op 81 Subops")
 case 1: {
@@ -363,30 +464,42 @@ case 1: {
   break;
 }
 
-:(scenario or_imm32_with_r32)
-% Reg[EBX].i = 0xd0c0b0a0;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  81  cb                          0a 0b 0c 0d  # or 0x0d0c0b0a with EBX
-# ModR/M in binary: 11 (direct mode) 001 (or imm32) 011 (dest EBX)
-+run: combine imm32 with r/m32
-+run: r/m32 is EBX
-+run: imm32 is 0x0d0c0b0a
-+run: subop or
-+run: storing 0xddccbbaa
+:(code)
+void test_or_imm32_with_r32() {
+  Reg[EBX].i = 0xd0c0b0a0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     cb                          0a 0b 0c 0d \n"  // or 0x0d0c0b0a with EBX
+      // ModR/M in binary: 11 (direct mode) 001 (or imm32) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: subop or\n"
+      "run: storing 0xddccbbaa\n"
+  );
+}
 
 //:: xor
 
 :(before "End Initialize Op Names")
 put_new(Name, "35", "EAX = bitwise XOR of imm32 with EAX (xor)");
 
-:(scenario xor_imm32_with_eax)
-% Reg[EAX].i = 0xddccb0a0;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  35                              0a 0b 0c 0d  # xor 0x0d0c0b0a with EAX
-+run: xor imm32 0x0d0c0b0a with EAX
-+run: storing 0xd0c0bbaa
+:(code)
+void test_xor_imm32_with_eax() {
+  Reg[EAX].i = 0xddccb0a0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  35                                 0a 0b 0c 0d \n"  // xor 0x0d0c0b0a with EAX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: xor imm32 0x0d0c0b0a with EAX\n"
+      "run: storing 0xd0c0bbaa\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x35: {  // xor imm32 with EAX
@@ -398,19 +511,25 @@ case 0x35: {  // xor imm32 with EAX
 
 //:
 
-:(scenario xor_imm32_with_mem_at_r32)
-% Reg[EBX].i = 0x2000;
-== 0x01  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  81  33                          0a 0b 0c 0d  # xor 0x0d0c0b0a with *EBX
-# ModR/M in binary: 00 (indirect mode) 110 (xor imm32) 011 (dest EBX)
-== 0x2000  # data segment
-a0 b0 c0 d0  # 0xd0c0b0a0
-+run: combine imm32 with r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: imm32 is 0x0d0c0b0a
-+run: subop xor
-+run: storing 0xddccbbaa
+:(code)
+void test_xor_imm32_with_mem_at_r32() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     33                          0a 0b 0c 0d \n"  // xor 0x0d0c0b0a with *EBX
+      // ModR/M in binary: 00 (indirect mode) 110 (xor imm32) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "a0 b0 c0 d0\n"  // 0xd0c0b0a0
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: subop xor\n"
+      "run: storing 0xddccbbaa\n"
+  );
+}
 
 :(before "End Op 81 Subops")
 case 6: {
@@ -419,30 +538,42 @@ case 6: {
   break;
 }
 
-:(scenario xor_imm32_with_r32)
-% Reg[EBX].i = 0xd0c0b0a0;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  81  f3                          0a 0b 0c 0d  # xor 0x0d0c0b0a with EBX
-# ModR/M in binary: 11 (direct mode) 110 (xor imm32) 011 (dest EBX)
-+run: combine imm32 with r/m32
-+run: r/m32 is EBX
-+run: imm32 is 0x0d0c0b0a
-+run: subop xor
-+run: storing 0xddccbbaa
+:(code)
+void test_xor_imm32_with_r32() {
+  Reg[EBX].i = 0xd0c0b0a0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     f3                          0a 0b 0c 0d \n"  // xor 0x0d0c0b0a with EBX
+      // ModR/M in binary: 11 (direct mode) 110 (xor imm32) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: subop xor\n"
+      "run: storing 0xddccbbaa\n"
+  );
+}
 
 //:: compare (cmp)
 
 :(before "End Initialize Op Names")
 put_new(Name, "3d", "compare: set SF if EAX < imm32 (cmp)");
 
-:(scenario compare_imm32_with_eax_greater)
-% Reg[EAX].i = 0x0d0c0b0a;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  3d                              07 0b 0c 0d  # compare 0x0d0c0b07 with EAX
-+run: compare EAX and imm32 0x0d0c0b07
-+run: SF=0; ZF=0; OF=0
+:(code)
+void test_compare_imm32_with_eax_greater() {
+  Reg[EAX].i = 0x0d0c0b0a;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3d                                 07 0b 0c 0d \n"  // compare 0x0d0c0b07 with EAX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EAX and imm32 0x0d0c0b07\n"
+      "run: SF=0; ZF=0; OF=0\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x3d: {  // compare EAX with imm32
@@ -458,34 +589,52 @@ case 0x3d: {  // compare EAX with imm32
   break;
 }
 
-:(scenario compare_imm32_with_eax_lesser)
-% Reg[EAX].i = 0x0d0c0b07;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  3d                              0a 0b 0c 0d  # compare 0x0d0c0b0a with EAX
-+run: compare EAX and imm32 0x0d0c0b0a
-+run: SF=1; ZF=0; OF=0
+:(code)
+void test_compare_imm32_with_eax_lesser() {
+  Reg[EAX].i = 0x0d0c0b07;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3d                                 0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with EAX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EAX and imm32 0x0d0c0b0a\n"
+      "run: SF=1; ZF=0; OF=0\n"
+  );
+}
 
-:(scenario compare_imm32_with_eax_equal)
-% Reg[EAX].i = 0x0d0c0b0a;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  3d                              0a 0b 0c 0d  # compare 0x0d0c0b0a with EAX
-+run: compare EAX and imm32 0x0d0c0b0a
-+run: SF=0; ZF=1; OF=0
+:(code)
+void test_compare_imm32_with_eax_equal() {
+  Reg[EAX].i = 0x0d0c0b0a;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3d                                 0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with EAX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EAX and imm32 0x0d0c0b0a\n"
+      "run: SF=0; ZF=1; OF=0\n"
+  );
+}
 
 //:
 
-:(scenario compare_imm32_with_r32_greater)
-% Reg[EBX].i = 0x0d0c0b0a;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  81  fb                          07 0b 0c 0d  # compare 0x0d0c0b07 with EBX
-# ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
-+run: combine imm32 with r/m32
-+run: r/m32 is EBX
-+run: imm32 is 0x0d0c0b07
-+run: SF=0; ZF=0; OF=0
+:(code)
+void test_compare_imm32_with_r32_greater() {
+  Reg[EBX].i = 0x0d0c0b0a;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     fb                          07 0b 0c 0d \n"  // compare 0x0d0c0b07 with EBX
+      // ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x0d0c0b07\n"
+      "run: SF=0; ZF=0; OF=0\n"
+  );
+}
 
 :(before "End Op 81 Subops")
 case 7: {
@@ -499,67 +648,97 @@ case 7: {
   break;
 }
 
-:(scenario compare_imm32_with_r32_lesser)
-% Reg[EBX].i = 0x0d0c0b07;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  81  fb                          0a 0b 0c 0d  # compare 0x0d0c0b0a with EBX
-# ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
-+run: combine imm32 with r/m32
-+run: r/m32 is EBX
-+run: imm32 is 0x0d0c0b0a
-+run: SF=1; ZF=0; OF=0
-
-:(scenario compare_imm32_with_r32_equal)
-% Reg[EBX].i = 0x0d0c0b0a;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  81  fb                          0a 0b 0c 0d  # compare 0x0d0c0b0a with EBX
-# ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
-+run: combine imm32 with r/m32
-+run: r/m32 is EBX
-+run: imm32 is 0x0d0c0b0a
-+run: SF=0; ZF=1; OF=0
-
-:(scenario compare_imm32_with_mem_at_r32_greater)
-% Reg[EBX].i = 0x2000;
-== 0x01  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  81  3b                          07 0b 0c 0d  # compare 0x0d0c0b07 with *EBX
-# ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
-== 0x2000  # data segment
-0a 0b 0c 0d  # 0x0d0c0b0a
-+run: combine imm32 with r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: imm32 is 0x0d0c0b07
-+run: SF=0; ZF=0; OF=0
-
-:(scenario compare_imm32_with_mem_at_r32_lesser)
-% Reg[EBX].i = 0x2000;
-== 0x01  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  81  3b                          0a 0b 0c 0d  # compare 0x0d0c0b0a with *EBX
-# ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
-== 0x2000  # data segment
-07 0b 0c 0d  # 0x0d0c0b07
-+run: combine imm32 with r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: imm32 is 0x0d0c0b0a
-+run: SF=1; ZF=0; OF=0
-
-:(scenario compare_imm32_with_mem_at_r32_equal)
-% Reg[EBX].i = 0x0d0c0b0a;
-% Reg[EBX].i = 0x2000;
-== 0x01  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  81  3b                          0a 0b 0c 0d  # compare 0x0d0c0b0a with *EBX
-# ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
-== 0x2000  # data segment
-0a 0b 0c 0d  # 0x0d0c0b0a
-+run: combine imm32 with r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: imm32 is 0x0d0c0b0a
-+run: SF=0; ZF=1; OF=0
+:(code)
+void test_compare_imm32_with_r32_lesser() {
+  Reg[EBX].i = 0x0d0c0b07;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     fb                          0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with EBX
+      // ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: SF=1; ZF=0; OF=0\n"
+  );
+}
+
+:(code)
+void test_compare_imm32_with_r32_equal() {
+  Reg[EBX].i = 0x0d0c0b0a;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     fb                          0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with EBX
+      // ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: SF=0; ZF=1; OF=0\n"
+  );
+}
+
+:(code)
+void test_compare_imm32_with_mem_at_r32_greater() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     3b                          07 0b 0c 0d \n"  // compare 0x0d0c0b07 with *EBX
+      // ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "0a 0b 0c 0d\n"  // 0x0d0c0b0a
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: imm32 is 0x0d0c0b07\n"
+      "run: SF=0; ZF=0; OF=0\n"
+  );
+}
+
+:(code)
+void test_compare_imm32_with_mem_at_r32_lesser() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     3b                          0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with *EBX
+      // ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "07 0b 0c 0d\n"  // 0x0d0c0b07
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: SF=1; ZF=0; OF=0\n"
+  );
+}
+
+:(code)
+void test_compare_imm32_with_mem_at_r32_equal() {
+  Reg[EBX].i = 0x0d0c0b0a;
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     3b                          0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with *EBX
+      // ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "0a 0b 0c 0d\n"  // 0x0d0c0b0a
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+      "run: SF=0; ZF=1; OF=0\n"
+  );
+}
 
 //:: copy (mov)
 
@@ -573,11 +752,17 @@ put_new(Name, "bd", "copy imm32 to EBP (mov)");
 put_new(Name, "be", "copy imm32 to ESI (mov)");
 put_new(Name, "bf", "copy imm32 to EDI (mov)");
 
-:(scenario copy_imm32_to_r32)
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  bb                              0a 0b 0c 0d  # copy 0x0d0c0b0a to EBX
-+run: copy imm32 0x0d0c0b0a to EBX
+:(code)
+void test_copy_imm32_to_r32() {
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  bb                                 0a 0b 0c 0d \n"  // copy 0x0d0c0b0a to EBX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy imm32 0x0d0c0b0a to EBX\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xb8:
@@ -600,15 +785,21 @@ case 0xbf: {  // copy imm32 to r32
 :(before "End Initialize Op Names")
 put_new(Name, "c7", "copy imm32 to rm32 (mov)");
 
-:(scenario copy_imm32_to_mem_at_r32)
-% Reg[EBX].i = 0x60;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c7  03                          0a 0b 0c 0d  # copy 0x0d0c0b0a to *EBX
-# ModR/M in binary: 00 (indirect mode) 000 (unused) 011 (dest EBX)
-+run: copy imm32 to r/m32
-+run: effective address is 0x00000060 (EBX)
-+run: imm32 is 0x0d0c0b0a
+:(code)
+void test_copy_imm32_to_mem_at_r32() {
+  Reg[EBX].i = 0x60;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c7     03                          0a 0b 0c 0d \n"  // copy 0x0d0c0b0a to *EBX
+      // ModR/M in binary: 00 (indirect mode) 000 (unused) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy imm32 to r/m32\n"
+      "run: effective address is 0x00000060 (EBX)\n"
+      "run: imm32 is 0x0d0c0b0a\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xc7: {  // copy imm32 to r32
@@ -631,14 +822,20 @@ case 0xc7: {  // copy imm32 to r32
 :(before "End Initialize Op Names")
 put_new(Name, "68", "push imm32 to stack (push)");
 
-:(scenario push_imm32)
-% Reg[ESP].u = 0x14;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  68                              af 00 00 00  # push *EAX to stack
-+run: push imm32 0x000000af
-+run: ESP is now 0x00000010
-+run: contents at ESP: 0x000000af
+:(code)
+void test_push_imm32() {
+  Reg[ESP].u = 0x14;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  68                                 af 00 00 00 \n"  // push *EAX to stack
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: push imm32 0x000000af\n"
+      "run: ESP is now 0x00000010\n"
+      "run: contents at ESP: 0x000000af\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x68: {
diff --git a/subx/016index_addressing.cc b/subx/016index_addressing.cc
index 9fb3e9bb..ef72f710 100644
--- a/subx/016index_addressing.cc
+++ b/subx/016index_addressing.cc
@@ -1,19 +1,25 @@
 //: operating on memory at the address provided by some register plus optional scale and offset
 
-:(scenario add_r32_to_mem_at_r32_with_sib)
-% Reg[EBX].i = 0x10;
-% Reg[EAX].i = 0x2000;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  1c      20                             # add EBX to *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src EBX) 100 (dest in SIB)
-# SIB in binary: 00 (scale 1) 100 (no index) 000 (base EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00002000 (EAX)
-+run: effective address is 0x00002000
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_r32_with_sib() {
+  Reg[EBX].i = 0x10;
+  Reg[EAX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     1c      20                              \n"  // add EBX to *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src EBX) 100 (dest in SIB)
+      // SIB in binary: 00 (scale 1) 100 (no index) 000 (base EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00002000 (EAX)\n"
+      "run: effective address is 0x00002000\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Mod 0 Special-cases(addr)")
 case 4:  // exception: mod 0b00 rm 0b100 => incoming SIB (scale-index-base) byte
@@ -46,54 +52,72 @@ uint32_t effective_address_from_sib(uint8_t mod) {
   return addr;
 }
 
-:(scenario add_r32_to_mem_at_base_r32_index_r32)
-% Reg[EBX].i = 0x10;  // source
-% Reg[EAX].i = 0x1ffe;  // dest base
-% Reg[ECX].i = 0x2;  // dest index
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  1c      08                             # add EBX to *(EAX+ECX)
-# ModR/M in binary: 00 (indirect mode) 011 (src EBX) 100 (dest in SIB)
-# SIB in binary: 00 (scale 1) 001 (index ECX) 000 (base EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00001ffe (EAX)
-+run: effective address is 0x00002000 (after adding ECX*1)
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_base_r32_index_r32() {
+  Reg[EBX].i = 0x10;  // source
+  Reg[EAX].i = 0x1ffe;  // dest base
+  Reg[ECX].i = 0x2;  // dest index
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     1c      08                              \n"  // add EBX to *(EAX+ECX)
+      // ModR/M in binary: 00 (indirect mode) 011 (src EBX) 100 (dest in SIB)
+      // SIB in binary: 00 (scale 1) 001 (index ECX) 000 (base EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00001ffe (EAX)\n"
+      "run: effective address is 0x00002000 (after adding ECX*1)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
-:(scenario add_r32_to_mem_at_displacement_using_sib)
-% Reg[EBX].i = 0x10;  // source
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  1c      25    00 20 00 00              # add EBX to *0x2000
-# ModR/M in binary: 00 (indirect mode) 011 (src EBX) 100 (dest in SIB)
-# SIB in binary: 00 (scale 1) 100 (no index) 101 (not EBP but disp32)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00002000 (disp32)
-+run: effective address is 0x00002000
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_displacement_using_sib() {
+  Reg[EBX].i = 0x10;  // source
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     1c      25    00 20 00 00               \n"  // add EBX to *0x2000
+      // ModR/M in binary: 00 (indirect mode) 011 (src EBX) 100 (dest in SIB)
+      // SIB in binary: 00 (scale 1) 100 (no index) 101 (not EBP but disp32)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00002000 (disp32)\n"
+      "run: effective address is 0x00002000\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 //:
 
-:(scenario add_r32_to_mem_at_base_r32_index_r32_plus_disp8)
-% Reg[EBX].i = 0x10;  // source
-% Reg[EAX].i = 0x1ff9;  // dest base
-% Reg[ECX].i = 0x5;  // dest index
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  5c      08    02                       # add EBX to *(EAX+ECX+2)
-# ModR/M in binary: 01 (indirect+disp8 mode) 011 (src EBX) 100 (dest in SIB)
-# SIB in binary: 00 (scale 1) 001 (index ECX) 000 (base EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00001ff9 (EAX)
-+run: effective address is 0x00001ffe (after adding ECX*1)
-+run: effective address is 0x00002000 (after adding disp8)
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_base_r32_index_r32_plus_disp8() {
+  Reg[EBX].i = 0x10;  // source
+  Reg[EAX].i = 0x1ff9;  // dest base
+  Reg[ECX].i = 0x5;  // dest index
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     5c      08    02                        \n"  // add EBX to *(EAX+ECX+2)
+      // ModR/M in binary: 01 (indirect+disp8 mode) 011 (src EBX) 100 (dest in SIB)
+      // SIB in binary: 00 (scale 1) 001 (index ECX) 000 (base EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00001ff9 (EAX)\n"
+      "run: effective address is 0x00001ffe (after adding ECX*1)\n"
+      "run: effective address is 0x00002000 (after adding disp8)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Mod 1 Special-cases(addr)")
 case 4:  // exception: mod 0b01 rm 0b100 => incoming SIB (scale-index-base) byte
@@ -102,22 +126,28 @@ case 4:  // exception: mod 0b01 rm 0b100 => incoming SIB (scale-index-base) byte
 
 //:
 
-:(scenario add_r32_to_mem_at_base_r32_index_r32_plus_disp32)
-% Reg[EBX].i = 0x10;  // source
-% Reg[EAX].i = 0x1ff9;  // dest base
-% Reg[ECX].i = 0x5;  // dest index
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  01  9c      08    02 00 00 00              # add EBX to *(EAX+ECX+2)
-# ModR/M in binary: 10 (indirect+disp32 mode) 011 (src EBX) 100 (dest in SIB)
-# SIB in binary: 00 (scale 1) 001 (index ECX) 000 (base EAX)
-== 0x2000  # data segment
-01 00 00 00  # 1
-+run: add EBX to r/m32
-+run: effective address is initially 0x00001ff9 (EAX)
-+run: effective address is 0x00001ffe (after adding ECX*1)
-+run: effective address is 0x00002000 (after adding disp32)
-+run: storing 0x00000011
+:(code)
+void test_add_r32_to_mem_at_base_r32_index_r32_plus_disp32() {
+  Reg[EBX].i = 0x10;  // source
+  Reg[EAX].i = 0x1ff9;  // dest base
+  Reg[ECX].i = 0x5;  // dest index
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     9c      08    02 00 00 00               \n"  // add EBX to *(EAX+ECX+2)
+      // ModR/M in binary: 10 (indirect+disp32 mode) 011 (src EBX) 100 (dest in SIB)
+      // SIB in binary: 00 (scale 1) 001 (index ECX) 000 (base EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 0x00000001
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: effective address is initially 0x00001ff9 (EAX)\n"
+      "run: effective address is 0x00001ffe (after adding ECX*1)\n"
+      "run: effective address is 0x00002000 (after adding disp32)\n"
+      "run: storing 0x00000011\n"
+  );
+}
 
 :(before "End Mod 2 Special-cases(addr)")
 case 4:  // exception: mod 0b10 rm 0b100 => incoming SIB (scale-index-base) byte
diff --git a/subx/017jump_disp8.cc b/subx/017jump_disp8.cc
index 24467f5c..22ae6567 100644
--- a/subx/017jump_disp8.cc
+++ b/subx/017jump_disp8.cc
@@ -5,16 +5,22 @@
 :(before "End Initialize Op Names")
 put_new(Name, "eb", "jump disp8 bytes away (jmp)");
 
-:(scenario jump_rel8)
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  eb                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: eb
-+run: jump 5
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
+:(code)
+void test_jump_rel8() {
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  eb                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: eb\n"
+      "run: jump 5\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xeb: {  // jump rel8
@@ -29,17 +35,23 @@ case 0xeb: {  // jump rel8
 :(before "End Initialize Op Names")
 put_new(Name, "74", "jump disp8 bytes away if equal, if ZF is set (jcc/jz/je)");
 
-:(scenario je_rel8_success)
-% ZF = true;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  74                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 74
-+run: jump 5
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
+:(code)
+void test_je_rel8_success() {
+  ZF = true;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  74                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 74\n"
+      "run: jump 5\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x74: {  // jump rel8 if ZF
@@ -51,34 +63,46 @@ case 0x74: {  // jump rel8 if ZF
   break;
 }
 
-:(scenario je_rel8_fail)
-% ZF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  74                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 74
-+run: 0x00000003 opcode: 05
-+run: 0x00000008 opcode: 05
--run: jump 5
+:(code)
+void test_je_rel8_fail() {
+  ZF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  74                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 74\n"
+      "run: 0x00000003 opcode: 05\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: jump if not equal/not zero
 
 :(before "End Initialize Op Names")
 put_new(Name, "75", "jump disp8 bytes away if not equal, if ZF is not set (jcc/jnz/jne)");
 
-:(scenario jne_rel8_success)
-% ZF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  75                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 75
-+run: jump 5
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
+:(code)
+void test_jne_rel8_success() {
+  ZF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  75                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 75\n"
+      "run: jump 5\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x75: {  // jump rel8 unless ZF
@@ -90,36 +114,48 @@ case 0x75: {  // jump rel8 unless ZF
   break;
 }
 
-:(scenario jne_rel8_fail)
-% ZF = true;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  75                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 75
-+run: 0x00000003 opcode: 05
-+run: 0x00000008 opcode: 05
--run: jump 5
+:(code)
+void test_jne_rel8_fail() {
+  ZF = true;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  75                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 75\n"
+      "run: 0x00000003 opcode: 05\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: jump if greater
 
 :(before "End Initialize Op Names")
 put_new(Name, "7f", "jump disp8 bytes away if greater, if ZF is unset and SF == OF (jcc/jg/jnle)");
 
-:(scenario jg_rel8_success)
-% ZF = false;
-% SF = false;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7f                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7f
-+run: jump 5
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
+:(code)
+void test_jg_rel8_success() {
+  ZF = false;
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7f                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7f\n"
+      "run: jump 5\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x7f: {  // jump rel8 if !SF and !ZF
@@ -131,37 +167,49 @@ case 0x7f: {  // jump rel8 if !SF and !ZF
   break;
 }
 
-:(scenario jg_rel8_fail)
-% ZF = false;
-% SF = true;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7f                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7f
-+run: 0x00000003 opcode: 05
-+run: 0x00000008 opcode: 05
--run: jump 5
+:(code)
+void test_jg_rel8_fail() {
+  ZF = false;
+  SF = true;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7f                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7f\n"
+      "run: 0x00000003 opcode: 05\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: jump if greater or equal
 
 :(before "End Initialize Op Names")
 put_new(Name, "7d", "jump disp8 bytes away if greater or equal, if SF == OF (jcc/jge/jnl)");
 
-:(scenario jge_rel8_success)
-% SF = false;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7d                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7d
-+run: jump 5
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
+:(code)
+void test_jge_rel8_success() {
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7d                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7d\n"
+      "run: jump 5\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x7d: {  // jump rel8 if !SF
@@ -173,37 +221,49 @@ case 0x7d: {  // jump rel8 if !SF
   break;
 }
 
-:(scenario jge_rel8_fail)
-% SF = true;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7d                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7d
-+run: 0x00000003 opcode: 05
-+run: 0x00000008 opcode: 05
--run: jump 5
+:(code)
+void test_jge_rel8_fail() {
+  SF = true;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7d                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7d\n"
+      "run: 0x00000003 opcode: 05\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: jump if lesser
 
 :(before "End Initialize Op Names")
 put_new(Name, "7c", "jump disp8 bytes away if lesser, if SF != OF (jcc/jl/jnge)");
 
-:(scenario jl_rel8_success)
-% ZF = false;
-% SF = true;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7c                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7c
-+run: jump 5
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
+:(code)
+void test_jl_rel8_success() {
+  ZF = false;
+  SF = true;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7c                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7c\n"
+      "run: jump 5\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x7c: {  // jump rel8 if SF and !ZF
@@ -215,52 +275,70 @@ case 0x7c: {  // jump rel8 if SF and !ZF
   break;
 }
 
-:(scenario jl_rel8_fail)
-% ZF = false;
-% SF = false;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7c                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7c
-+run: 0x00000003 opcode: 05
-+run: 0x00000008 opcode: 05
--run: jump 5
+:(code)
+void test_jl_rel8_fail() {
+  ZF = false;
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7c                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7c\n"
+      "run: 0x00000003 opcode: 05\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: jump if lesser or equal
 
 :(before "End Initialize Op Names")
 put_new(Name, "7e", "jump disp8 bytes away if lesser or equal, if ZF is set or SF != OF (jcc/jle/jng)");
 
-:(scenario jle_rel8_equal)
-% ZF = true;
-% SF = false;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7e                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7e
-+run: jump 5
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
-
-:(scenario jle_rel8_lesser)
-% ZF = false;
-% SF = true;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7e                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7e
-+run: jump 5
-+run: 0x00000008 opcode: 05
--run: 0x00000003 opcode: 05
+:(code)
+void test_jle_rel8_equal() {
+  ZF = true;
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7e                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7e\n"
+      "run: jump 5\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
+
+:(code)
+void test_jle_rel8_lesser() {
+  ZF = false;
+  SF = true;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7e                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7e\n"
+      "run: jump 5\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x7e: {  // jump rel8 if SF or ZF
@@ -272,16 +350,22 @@ case 0x7e: {  // jump rel8 if SF or ZF
   break;
 }
 
-:(scenario jle_rel8_greater)
-% ZF = false;
-% SF = false;
-% OF = false;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  7e                05                        # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: 7e
-+run: 0x00000003 opcode: 05
-+run: 0x00000008 opcode: 05
--run: jump 5
+:(code)
+void test_jle_rel8_greater() {
+  ZF = false;
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  7e                   05                        \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 7e\n"
+      "run: 0x00000003 opcode: 05\n"
+      "run: 0x00000008 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
diff --git a/subx/018jump_disp32.cc b/subx/018jump_disp32.cc
index c86cd2df..836146ee 100644
--- a/subx/018jump_disp32.cc
+++ b/subx/018jump_disp32.cc
@@ -5,16 +5,22 @@
 :(before "End Initialize Op Names")
 put_new(Name, "e9", "jump disp32 bytes away (jmp)");
 
-:(scenario jump_disp32)
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  e9                05 00 00 00               # skip 1 instruction
-  05                              00 00 00 01
-  05                              00 00 00 02
-+run: 0x00000001 opcode: e9
-+run: jump 5
-+run: 0x0000000b opcode: 05
--run: 0x00000006 opcode: 05
+:(code)
+void test_jump_disp32() {
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  e9                   05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: e9\n"
+      "run: jump 5\n"
+      "run: 0x0000000b opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000006 opcode: 05");
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xe9: {  // jump disp32
@@ -29,17 +35,23 @@ case 0xe9: {  // jump disp32
 :(before "End Initialize Op Names")
 put_new(Name_0f, "84", "jump disp32 bytes away if equal, if ZF is set (jcc/jz/je)");
 
-:(scenario je_disp32_success)
-% ZF = true;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 84                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: jump 5
-+run: 0x0000000c opcode: 05
--run: 0x00000007 opcode: 05
+:(code)
+void test_je_disp32_success() {
+  ZF = true;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 84                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: jump 5\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000007 opcode: 05");
+}
 
 :(before "End Two-Byte Opcodes Starting With 0f")
 case 0x84: {  // jump disp32 if ZF
@@ -51,34 +63,46 @@ case 0x84: {  // jump disp32 if ZF
   break;
 }
 
-:(scenario je_disp32_fail)
-% ZF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 84                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: 0x00000007 opcode: 05
-+run: 0x0000000c opcode: 05
--run: jump 5
+:(code)
+void test_je_disp32_fail() {
+  ZF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 84                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: 0x00000007 opcode: 05\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: jump if not equal/not zero
 
 :(before "End Initialize Op Names")
 put_new(Name_0f, "85", "jump disp32 bytes away if not equal, if ZF is not set (jcc/jnz/jne)");
 
-:(scenario jne_disp32_success)
-% ZF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 85                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: jump 5
-+run: 0x0000000c opcode: 05
--run: 0x00000007 opcode: 05
+:(code)
+void test_jne_disp32_success() {
+  ZF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 85                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: jump 5\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000007 opcode: 05");
+}
 
 :(before "End Two-Byte Opcodes Starting With 0f")
 case 0x85: {  // jump disp32 unless ZF
@@ -90,36 +114,48 @@ case 0x85: {  // jump disp32 unless ZF
   break;
 }
 
-:(scenario jne_disp32_fail)
-% ZF = true;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 85                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: 0x00000007 opcode: 05
-+run: 0x0000000c opcode: 05
--run: jump 5
+:(code)
+void test_jne_disp32_fail() {
+  ZF = true;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 85                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: 0x00000007 opcode: 05\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: 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)");
 
-:(scenario jg_disp32_success)
-% ZF = false;
-% SF = false;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8f                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: jump 5
-+run: 0x0000000c opcode: 05
--run: 0x00000007 opcode: 05
+:(code)
+void test_jg_disp32_success() {
+  ZF = false;
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8f                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: jump 5\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000007 opcode: 05");
+}
 
 :(before "End Two-Byte Opcodes Starting With 0f")
 case 0x8f: {  // jump disp32 if !SF and !ZF
@@ -131,37 +167,49 @@ case 0x8f: {  // jump disp32 if !SF and !ZF
   break;
 }
 
-:(scenario jg_disp32_fail)
-% ZF = false;
-% SF = true;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8f                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: 0x00000007 opcode: 05
-+run: 0x0000000c opcode: 05
--run: jump 5
+:(code)
+void test_jg_disp32_fail() {
+  ZF = false;
+  SF = true;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8f                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: 0x00000007 opcode: 05\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: 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)");
 
-:(scenario jge_disp32_success)
-% SF = false;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8d                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: jump 5
-+run: 0x0000000c opcode: 05
--run: 0x00000007 opcode: 05
+:(code)
+void test_jge_disp32_success() {
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8d                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: jump 5\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000007 opcode: 05");
+}
 
 :(before "End Two-Byte Opcodes Starting With 0f")
 case 0x8d: {  // jump disp32 if !SF
@@ -173,37 +221,49 @@ case 0x8d: {  // jump disp32 if !SF
   break;
 }
 
-:(scenario jge_disp32_fail)
-% SF = true;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8d                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: 0x00000007 opcode: 05
-+run: 0x0000000c opcode: 05
--run: jump 5
+:(code)
+void test_jge_disp32_fail() {
+  SF = true;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8d                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: 0x00000007 opcode: 05\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: 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)");
 
-:(scenario jl_disp32_success)
-% ZF = false;
-% SF = true;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8c                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: jump 5
-+run: 0x0000000c opcode: 05
--run: 0x00000007 opcode: 05
+:(code)
+void test_jl_disp32_success() {
+  ZF = false;
+  SF = true;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8c                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: jump 5\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000007 opcode: 05");
+}
 
 :(before "End Two-Byte Opcodes Starting With 0f")
 case 0x8c: {  // jump disp32 if SF and !ZF
@@ -215,52 +275,70 @@ case 0x8c: {  // jump disp32 if SF and !ZF
   break;
 }
 
-:(scenario jl_disp32_fail)
-% ZF = false;
-% SF = false;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8c                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: 0x00000007 opcode: 05
-+run: 0x0000000c opcode: 05
--run: jump 5
+:(code)
+void test_jl_disp32_fail() {
+  ZF = false;
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8c                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: 0x00000007 opcode: 05\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
 
 //:: 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)");
 
-:(scenario jle_disp32_equal)
-% ZF = true;
-% SF = false;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8e                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: jump 5
-+run: 0x0000000c opcode: 05
--run: 0x00000007 opcode: 05
-
-:(scenario jle_disp32_lesser)
-% ZF = false;
-% SF = true;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8e                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: jump 5
-+run: 0x0000000c opcode: 05
--run: 0x00000007 opcode: 05
+:(code)
+void test_jle_disp32_equal() {
+  ZF = true;
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8e                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: jump 5\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000007 opcode: 05");
+}
+
+:(code)
+void test_jle_disp32_lesser() {
+  ZF = false;
+  SF = true;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8e                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: jump 5\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000007 opcode: 05");
+}
 
 :(before "End Two-Byte Opcodes Starting With 0f")
 case 0x8e: {  // jump disp32 if SF or ZF
@@ -272,16 +350,22 @@ case 0x8e: {  // jump disp32 if SF or ZF
   break;
 }
 
-:(scenario jle_disp32_greater)
-% ZF = false;
-% SF = false;
-% OF = false;
-== 0x1
-# op      ModR/M  SIB   displacement  immediate
-  0f 8e                 05 00 00 00               # skip 1 instruction
-  05                                  00 00 00 01
-  05                                  00 00 00 02
-+run: 0x00000001 opcode: 0f
-+run: 0x00000007 opcode: 05
-+run: 0x0000000c opcode: 05
--run: jump 5
+:(code)
+void test_jle_disp32_greater() {
+  ZF = false;
+  SF = false;
+  OF = false;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  0f 8e                05 00 00 00               \n"  // skip 1 instruction
+      "  05                                 00 00 00 01 \n"
+      "  05                                 00 00 00 02 \n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000001 opcode: 0f\n"
+      "run: 0x00000007 opcode: 05\n"
+      "run: 0x0000000c opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: jump 5");
+}
diff --git a/subx/019functions.cc b/subx/019functions.cc
index 66cfe384..7f45167b 100644
--- a/subx/019functions.cc
+++ b/subx/019functions.cc
@@ -3,16 +3,22 @@
 :(before "End Initialize Op Names")
 put_new(Name, "e8", "call disp32 (call)");
 
-:(scenario call_disp32)
-% Reg[ESP].u = 0x64;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  e8                              a0 00 00 00  # call function offset at 0x000000a0
-  # next EIP is 6
-+run: call imm32 0x000000a0
-+run: decrementing ESP to 0x00000060
-+run: pushing value 0x00000006
-+run: jumping to 0x000000a6
+:(code)
+void test_call_disp32() {
+  Reg[ESP].u = 0x64;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  e8                                 a0 00 00 00 \n"  // call function offset at 0x000000a0
+      // next EIP is 6
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: call imm32 0x000000a0\n"
+      "run: decrementing ESP to 0x00000060\n"
+      "run: pushing value 0x00000006\n"
+      "run: jumping to 0x000000a6\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xe8: {  // call disp32 relative to next EIP
@@ -28,18 +34,24 @@ case 0xe8: {  // call disp32 relative to next EIP
 
 //:
 
-:(scenario call_r32)
-% Reg[ESP].u = 0x64;
-% Reg[EBX].u = 0x000000a0;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  ff  d3                                       # call function offset at EBX
-  # next EIP is 3
-+run: call to r/m32
-+run: r/m32 is EBX
-+run: decrementing ESP to 0x00000060
-+run: pushing value 0x00000003
-+run: jumping to 0x000000a3
+:(code)
+void test_call_r32() {
+  Reg[ESP].u = 0x64;
+  Reg[EBX].u = 0x000000a0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  ff     d3                                      \n"  // call function offset at EBX
+      // next EIP is 3
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: call to r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: decrementing ESP to 0x00000060\n"
+      "run: pushing value 0x00000003\n"
+      "run: jumping to 0x000000a3\n"
+  );
+}
 
 :(before "End Op ff Subops")
 case 2: {  // call function pointer at r/m32
@@ -52,36 +64,48 @@ case 2: {  // call function pointer at r/m32
   break;
 }
 
-:(scenario call_mem_at_r32)
-% Reg[ESP].u = 0x64;
-% Reg[EBX].u = 0x2000;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  ff  13                                       # call function offset at *EBX
-  # next EIP is 3
-== 0x2000  # data segment
-a0 00 00 00  # 0xa0
-+run: call to r/m32
-+run: effective address is 0x00002000 (EBX)
-+run: decrementing ESP to 0x00000060
-+run: pushing value 0x00000003
-+run: jumping to 0x000000a3
+:(code)
+void test_call_mem_at_r32() {
+  Reg[ESP].u = 0x64;
+  Reg[EBX].u = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  ff     13                                      \n"  // call function offset at *EBX
+      // next EIP is 3
+      "== 0x2000\n"  // data segment
+      "a0 00 00 00\n"  // 0x000000a0
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: call to r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: decrementing ESP to 0x00000060\n"
+      "run: pushing value 0x00000003\n"
+      "run: jumping to 0x000000a3\n"
+  );
+}
 
 //:: ret
 
 :(before "End Initialize Op Names")
 put_new(Name, "c3", "return from most recent unfinished call (ret)");
 
-:(scenario ret)
-% Reg[ESP].u = 0x2000;
-== 0x1  # code segment
-# op  ModR/M  SIB   displacement  immediate
-  c3
-== 0x2000  # data segment
-10 00 00 00  # 0x10
-+run: return
-+run: popping value 0x00000010
-+run: jumping to 0x00000010
+:(code)
+void test_ret() {
+  Reg[ESP].u = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c3                                           \n"  // return
+      "== 0x2000\n"  // data segment
+      "10 00 00 00\n"  // 0x00000010
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: return\n"
+      "run: popping value 0x00000010\n"
+      "run: jumping to 0x00000010\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xc3: {  // return from a call
diff --git a/subx/021byte_addressing.cc b/subx/021byte_addressing.cc
index c96bc9c1..106cd426 100644
--- a/subx/021byte_addressing.cc
+++ b/subx/021byte_addressing.cc
@@ -40,19 +40,25 @@ uint8_t* reg_8bit(uint8_t rm) {
 :(before "End Initialize Op Names")
 put_new(Name, "88", "copy r8 to r8/m8-at-r32");
 
-:(scenario copy_r8_to_mem_at_r32)
-% Reg[EBX].i = 0x224488ab;
-% Reg[EAX].i = 0x2000;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  88  18                                      # copy BL to the byte at *EAX
-# ModR/M in binary: 00 (indirect mode) 011 (src BL) 000 (dest EAX)
-== 0x2000
-f0 cc bb aa
-+run: copy BL to r8/m8-at-r32
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0xab
-% CHECK_EQ(0xaabbccab, read_mem_u32(0x2000));
+:(code)
+void test_copy_r8_to_mem_at_r32() {
+  Reg[EBX].i = 0x224488ab;
+  Reg[EAX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  88     18                                      \n"  // copy BL to the byte at *EAX
+      // ModR/M in binary: 00 (indirect mode) 011 (src BL) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "f0 cc bb aa\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy BL to r8/m8-at-r32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0xab\n"
+  );
+  CHECK_EQ(0xaabbccab, read_mem_u32(0x2000));
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x88: {  // copy r8 to r/m8
@@ -72,20 +78,26 @@ case 0x88: {  // copy r8 to r/m8
 :(before "End Initialize Op Names")
 put_new(Name, "8a", "copy r8/m8-at-r32 to r8");
 
-:(scenario copy_mem_at_r32_to_r8)
-% Reg[EBX].i = 0xaabbcc0f;  // one nibble each of lowest byte set to all 0s and all 1s, to maximize value of this test
-% Reg[EAX].i = 0x2000;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  8a  18                                      # copy just the byte at *EAX to BL
-# ModR/M in binary: 00 (indirect mode) 011 (dest EBX) 000 (src EAX)
-== 0x2000  # data segment
-ab ff ff ff  # 0xab with more data in following bytes
-+run: copy r8/m8-at-r32 to BL
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0xab
-# remaining bytes of EBX are *not* cleared
-+run: EBX now contains 0xaabbccab
+:(code)
+void test_copy_mem_at_r32_to_r8() {
+  Reg[EBX].i = 0xaabbcc0f;  // one nibble each of lowest byte set to all 0s and all 1s, to maximize value of this test
+  Reg[EAX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  8a     18                                      \n"  // copy just the byte at *EAX to BL
+      // ModR/M in binary: 00 (indirect mode) 011 (dest EBX) 000 (src EAX)
+      "== 0x2000\n"  // data segment
+      "ab ff ff ff\n"  // 0xab with more data in following bytes
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy r8/m8-at-r32 to BL\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0xab\n"
+      // remaining bytes of EBX are *not* cleared
+      "run: EBX now contains 0xaabbccab\n"
+  );
+}
 
 :(before "End Single-Byte Opcodes")
 case 0x8a: {  // copy r/m8 to r8
@@ -102,36 +114,48 @@ case 0x8a: {  // copy r/m8 to r8
   break;
 }
 
-:(scenario cannot_copy_byte_to_ESP_EBP_ESI_EDI)
-% Reg[ESI].u = 0xaabbccdd;
-% Reg[EBX].u = 0x11223344;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  8a  f3                                      # copy just the byte at *EBX to 8-bit register '6'
-# ModR/M in binary: 11 (direct mode) 110 (dest 8-bit 'register 6') 011 (src EBX)
-# ensure 8-bit register '6' is DH, not ESI
-+run: copy r8/m8-at-r32 to DH
-+run: storing 0x44
-# ensure ESI is unchanged
-% CHECK_EQ(Reg[ESI].u, 0xaabbccdd);
+:(code)
+void test_cannot_copy_byte_to_ESP_EBP_ESI_EDI() {
+  Reg[ESI].u = 0xaabbccdd;
+  Reg[EBX].u = 0x11223344;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  8a     f3                                      \n"  // copy just the byte at *EBX to 8-bit register '6'
+      // ModR/M in binary: 11 (direct mode) 110 (dest 8-bit 'register 6') 011 (src EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      // ensure 8-bit register '6' is DH, not ESI
+      "run: copy r8/m8-at-r32 to DH\n"
+      "run: storing 0x44\n"
+  );
+  // ensure ESI is unchanged
+  CHECK_EQ(Reg[ESI].u, 0xaabbccdd);
+}
 
 //:
 
 :(before "End Initialize Op Names")
 put_new(Name, "c6", "copy imm8 to r8/m8-at-r32 (mov)");
 
-:(scenario copy_imm8_to_mem_at_r32)
-% Reg[EAX].i = 0x2000;
-== 0x1
-# op  ModR/M  SIB   displacement  immediate
-  c6  00                          dd          # copy to the byte at *EAX
-# ModR/M in binary: 00 (indirect mode) 000 (unused) 000 (dest EAX)
-== 0x2000
-f0 cc bb aa
-+run: copy imm8 to r8/m8-at-r32
-+run: effective address is 0x00002000 (EAX)
-+run: storing 0xdd
-% CHECK_EQ(0xaabbccdd, read_mem_u32(0x2000));
+:(code)
+void test_copy_imm8_to_mem_at_r32() {
+  Reg[EAX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  c6     00                          dd          \n"  // copy to the byte at *EAX
+      // ModR/M in binary: 00 (indirect mode) 000 (unused) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "f0 cc bb aa\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy imm8 to r8/m8-at-r32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: storing 0xdd\n"
+  );
+  CHECK_EQ(0xaabbccdd, read_mem_u32(0x2000));
+}
 
 :(before "End Single-Byte Opcodes")
 case 0xc6: {  // copy imm8 to r/m8
diff --git a/subx/030---operands.cc b/subx/030---operands.cc
index 085cf1b2..c8e2942d 100644
--- a/subx/030---operands.cc
+++ b/subx/030---operands.cc
@@ -27,12 +27,18 @@ put_new(Help, "instructions",
 :(before "End Help Contents")
 cerr << "  instructions\n";
 
-:(scenario pack_immediate_constants)
-== 0x1
-bb  0x2a/imm32
-+transform: packing instruction 'bb 0x2a/imm32'
-+transform: instruction after packing: 'bb 2a 00 00 00'
-+run: copy imm32 0x0000002a to EBX
+:(code)
+void test_pack_immediate_constants() {
+  run(
+      "== 0x1\n"  // code segment
+      "bb  0x2a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction 'bb 0x2a/imm32'\n"
+      "transform: instruction after packing: 'bb 2a 00 00 00'\n"
+      "run: copy imm32 0x0000002a to EBX\n"
+  );
+}
 
 //: complete set of valid operand types
 
@@ -310,23 +316,31 @@ void test_preserve_metadata_when_emitting_single_byte() {
   CHECK_EQ(out.words.at(0).original, "f0/foo");
 }
 
-:(scenario pack_disp8)
-== 0x1
-74 2/disp8  # jump 2 bytes away if ZF is set
-+transform: packing instruction '74 2/disp8'
-+transform: instruction after packing: '74 02'
-
-:(scenarios transform)
-:(scenario pack_disp8_negative)
-== 0x1
-# running this will cause an infinite loop
-74 -1/disp8  # jump 1 byte before if ZF is set
-+transform: packing instruction '74 -1/disp8'
-+transform: instruction after packing: '74 ff'
-:(scenarios run)
+:(code)
+void test_pack_disp8() {
+  run(
+      "== 0x1\n"  // code segment
+      "74 2/disp8\n"  // jump 2 bytes away if ZF is set
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction '74 2/disp8'\n"
+      "transform: instruction after packing: '74 02'\n"
+  );
+}
+
+void test_pack_disp8_negative() {
+  transform(
+      "== 0x1\n"  // code segment
+      // running this will cause an infinite loop
+      "74 -1/disp8\n"  // jump 1 byte before if ZF is set
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction '74 -1/disp8'\n"
+      "transform: instruction after packing: '74 ff'\n"
+  );
+}
 
 //: helper for scenario
-:(code)
 void transform(const string& text_bytes) {
   program p;
   istringstream in(text_bytes);
@@ -335,47 +349,69 @@ void transform(const string& text_bytes) {
   transform(p);
 }
 
-:(scenario pack_modrm_imm32)
-== 0x1
-# instruction                     effective address                                                   operand     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
-  81          0/add/subop         3/mod/direct    3/ebx/rm32                                                                      1/imm32           # add 1 to EBX
-+transform: packing instruction '81 0/add/subop 3/mod/direct 3/ebx/rm32 1/imm32'
-+transform: instruction after packing: '81 c3 01 00 00 00'
-
-:(scenario pack_imm32_large)
-== 0x1
-b9  0x080490a7/imm32
-+transform: packing instruction 'b9 0x080490a7/imm32'
-+transform: instruction after packing: 'b9 a7 90 04 08'
-
-:(scenario pack_immediate_constants_hex)
-== 0x1
-b9  0x2a/imm32
-+transform: packing instruction 'b9 0x2a/imm32'
-+transform: instruction after packing: 'b9 2a 00 00 00'
-+run: copy imm32 0x0000002a to ECX
-
-:(scenarios transform)
-:(scenario pack_silently_ignores_non_hex)
-% Hide_errors = true;
-== 0x1
-b9  foo/imm32
-+transform: packing instruction 'b9 foo/imm32'
-# no change (we're just not printing metadata to the trace)
-+transform: instruction after packing: 'b9 foo'
-:(scenarios run)
-
-:(scenario pack_flags_bad_hex)
-% Hide_errors = true;
-== 0x1
-b9  0xfoo/imm32
-+error: not a number: 0xfoo
+void test_pack_modrm_imm32() {
+  run(
+      "== 0x1\n"  // code segment
+      // instruction                     effective address                                                   operand     displacement    immediate\n"
+      // op          subop               mod             rm32          base        index         scale       r32\n"
+      // 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\n"
+      "  81          0/add/subop         3/mod/direct    3/ebx/rm32                                                                      1/imm32      \n"  // add 1 to EBX
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction '81 0/add/subop 3/mod/direct 3/ebx/rm32 1/imm32'\n"
+      "transform: instruction after packing: '81 c3 01 00 00 00'\n"
+  );
+}
+
+void test_pack_imm32_large() {
+  run(
+      "== 0x1\n"  // code segment
+      "b9  0x080490a7/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction 'b9 0x080490a7/imm32'\n"
+      "transform: instruction after packing: 'b9 a7 90 04 08'\n"
+  );
+}
+
+void test_pack_immediate_constants_hex() {
+  run(
+      "== 0x1\n"  // code segment
+      "b9  0x2a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction 'b9 0x2a/imm32'\n"
+      "transform: instruction after packing: 'b9 2a 00 00 00'\n"
+      "run: copy imm32 0x0000002a to ECX\n"
+  );
+}
+
+void test_pack_silently_ignores_non_hex() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"  // code segment
+      "b9  foo/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction 'b9 foo/imm32'\n"
+      // no change (we're just not printing metadata to the trace)
+      "transform: instruction after packing: 'b9 foo'\n"
+  );
+}
+
+void test_pack_flags_bad_hex() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "b9  0xfoo/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: not a number: 0xfoo\n"
+  );
+}
 
 //:: helpers
 
-:(code)
 bool all_hex_bytes(const line& inst) {
   for (int i = 0;  i < SIZE(inst.words);  ++i)
     if (!is_hex_byte(inst.words.at(i)))
@@ -444,7 +480,6 @@ bool looks_like_hex_int(const string& s) {
   return false;
 }
 
-:(code)
 string to_string(const line& inst) {
   ostringstream out;
   for (int i = 0;  i < SIZE(inst.words);  ++i) {
diff --git a/subx/031check_operands.cc b/subx/031check_operands.cc
index bc76fca5..475cda5c 100644
--- a/subx/031check_operands.cc
+++ b/subx/031check_operands.cc
@@ -1,11 +1,16 @@
 //: Since we're tagging operands with their types, let's start checking these
 //: operand types for each instruction.
 
-:(scenario check_missing_imm8_operand)
-% Hide_errors = true;
-== 0x1
-cd  # int ??
-+error: 'cd' (software interrupt): missing imm8 operand
+void test_check_missing_imm8_operand() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "cd\n"  // interrupt ??
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: 'cd' (software interrupt): missing imm8 operand\n"
+  );
+}
 
 :(before "Pack Operands(segment code)")
 check_operands(code);
@@ -239,7 +244,6 @@ void init_permitted_operands() {
   // End Init Permitted Operands
 }
 
-:(code)
 #define HAS(bitvector, bit)  ((bitvector) & (1 << (bit)))
 #define SET(bitvector, bit)  ((bitvector) | (1 << (bit)))
 #define CLEAR(bitvector, bit)  ((bitvector) & (~(1 << (bit))))
@@ -335,22 +339,31 @@ uint32_t expected_bit_for_received_operand(const word& w, set<string>& instructi
   return bv;
 }
 
-:(scenario conflicting_operand_type)
-% Hide_errors = true;
-== 0x1
-cd/software-interrupt 80/imm8/imm32
-+error: '80/imm8/imm32' has conflicting operand types; it should have only one
+void test_conflicting_operand_type() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "cd/software-interrupt 80/imm8/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '80/imm8/imm32' has conflicting operand types; it should have only one\n"
+  );
+}
 
 //: Instructions computing effective addresses have more complex rules, so
 //: we'll hard-code a common set of instruction-decoding rules.
 
-:(scenario check_missing_mod_operand)
-% Hide_errors = true;
-== 0x1
-81 0/add/subop       3/rm32/ebx 1/imm32
-+error: '81 0/add/subop 3/rm32/ebx 1/imm32' (combine rm32 with imm32 based on subop): missing mod operand
+void test_check_missing_mod_operand() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "81 0/add/subop       3/rm32/ebx 1/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '81 0/add/subop 3/rm32/ebx 1/imm32' (combine rm32 with imm32 based on subop): missing mod operand\n"
+  );
+}
 
-:(code)
 void check_operands_modrm(const line& inst, const word& op) {
   if (all_hex_bytes(inst)) return;  // deliberately programming in raw hex; we'll raise a warning elsewhere
   check_operand_metadata_present(inst, "mod", op);
@@ -421,95 +434,158 @@ void check_operand_metadata_absent(const line& inst, const string& type, const w
     raise << "'" << to_string(inst) << "'" << maybe_name(op) << ": unexpected " << type << " operand (" << msg << ")\n" << end();
 }
 
-:(scenarios transform)
-:(scenario modrm_with_displacement)
-% Reg[EAX].u = 0x1;
-== 0x1
-# just avoid null pointer
-8b/copy 1/mod/lookup+disp8 0/rm32/EAX 2/r32/EDX 4/disp8  # copy *(EAX+4) to EDX
-$error: 0
-
-:(scenario check_missing_disp8)
-% Hide_errors = true;
-== 0x1
-89/copy 1/mod/lookup+disp8 0/rm32/EAX 1/r32/ECX  # missing disp8
-+error: '89/copy 1/mod/lookup+disp8 0/rm32/EAX 1/r32/ECX' (copy r32 to rm32): missing disp8 operand
-
-:(scenario check_missing_disp32)
-% Hide_errors = true;
-== 0x1
-8b/copy 0/mod/indirect 5/rm32/.disp32 2/r32/EDX  # missing disp32
-+error: '8b/copy 0/mod/indirect 5/rm32/.disp32 2/r32/EDX' (copy rm32 to r32): missing disp32 operand
-:(scenarios run)
-
-:(scenario conflicting_operands_in_modrm_instruction)
-% Hide_errors = true;
-== 0x1
-01/add 0/mod 3/mod
-+error: '01/add 0/mod 3/mod' has conflicting mod operands
-
-:(scenario conflicting_operand_type_modrm)
-% Hide_errors = true;
-== 0x1
-01/add 0/mod 3/rm32/r32
-+error: '3/rm32/r32' has conflicting operand types; it should have only one
-
-:(scenario check_missing_rm32_operand)
-% Hide_errors = true;
-== 0x1
-81 0/add/subop 0/mod            1/imm32
-+error: '81 0/add/subop 0/mod 1/imm32' (combine rm32 with imm32 based on subop): missing rm32 operand
-
-:(scenario check_missing_subop_operand)
-% Hide_errors = true;
-== 0x1
-81             0/mod 3/rm32/ebx 1/imm32
-+error: '81 0/mod 3/rm32/ebx 1/imm32' (combine rm32 with imm32 based on subop): missing subop operand
-
-:(scenario check_missing_base_operand)
-% Hide_errors = true;
-== 0x1
-81 0/add/subop 0/mod/indirect 4/rm32/use-sib 1/imm32
-+error: '81 0/add/subop 0/mod/indirect 4/rm32/use-sib 1/imm32' (combine rm32 with imm32 based on subop): missing base operand
-
-:(scenario check_missing_index_operand)
-% Hide_errors = true;
-== 0x1
-81 0/add/subop 0/mod/indirect 4/rm32/use-sib 0/base 1/imm32
-+error: '81 0/add/subop 0/mod/indirect 4/rm32/use-sib 0/base 1/imm32' (combine rm32 with imm32 based on subop): missing index operand
-
-:(scenario check_missing_base_operand_2)
-% Hide_errors = true;
-== 0x1
-81 0/add/subop 0/mod/indirect 4/rm32/use-sib 2/index 3/scale 1/imm32
-+error: '81 0/add/subop 0/mod/indirect 4/rm32/use-sib 2/index 3/scale 1/imm32' (combine rm32 with imm32 based on subop): missing base operand
-
-:(scenario check_extra_displacement)
-% Hide_errors = true;
-== 0x1
-89/copy 0/mod/indirect 0/rm32/EAX 1/r32/ECX 4/disp8
-+error: '89/copy 0/mod/indirect 0/rm32/EAX 1/r32/ECX 4/disp8' (copy r32 to rm32): unexpected disp8 operand
-
-:(scenario check_duplicate_operand)
-% Hide_errors = true;
-== 0x1
-89/copy 0/mod/indirect 0/rm32/EAX 1/r32/ECX 1/r32
-+error: '89/copy 0/mod/indirect 0/rm32/EAX 1/r32/ECX 1/r32': duplicate r32 operand
-
-:(scenario check_base_operand_not_needed_in_direct_mode)
-== 0x1
-81 0/add/subop 3/mod/indirect 4/rm32/use-sib 1/imm32
-$error: 0
-
-:(scenario extra_modrm)
-% Hide_errors = true;
-== 0x1
-59/pop-to-ECX  3/mod/direct 1/rm32/ECX 4/r32/ESP
-+error: '59/pop-to-ECX 3/mod/direct 1/rm32/ECX 4/r32/ESP' (pop top of stack to ECX): unexpected modrm operand
+void test_modrm_with_displacement() {
+  Reg[EAX].u = 0x1;
+  transform(
+      "== 0x1\n"
+      // just avoid null pointer
+      "8b/copy 1/mod/lookup+disp8 0/rm32/EAX 2/r32/EDX 4/disp8\n"  // copy *(EAX+4) to EDX
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_check_missing_disp8() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"  // code segment
+      "89/copy 1/mod/lookup+disp8 0/rm32/EAX 1/r32/ECX\n"  // missing disp8
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '89/copy 1/mod/lookup+disp8 0/rm32/EAX 1/r32/ECX' (copy r32 to rm32): missing disp8 operand\n"
+  );
+}
+
+void test_check_missing_disp32() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"  // code segment
+      "8b/copy 0/mod/indirect 5/rm32/.disp32 2/r32/EDX\n"  // missing disp32
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '8b/copy 0/mod/indirect 5/rm32/.disp32 2/r32/EDX' (copy rm32 to r32): missing disp32 operand\n"
+  );
+}
+
+void test_conflicting_operands_in_modrm_instruction() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "01/add 0/mod 3/mod\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '01/add 0/mod 3/mod' has conflicting mod operands\n"
+  );
+}
+
+void test_conflicting_operand_type_modrm() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "01/add 0/mod 3/rm32/r32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '3/rm32/r32' has conflicting operand types; it should have only one\n"
+  );
+}
+
+void test_check_missing_rm32_operand() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "81 0/add/subop 0/mod            1/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '81 0/add/subop 0/mod 1/imm32' (combine rm32 with imm32 based on subop): missing rm32 operand\n"
+  );
+}
+
+void test_check_missing_subop_operand() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "81             0/mod 3/rm32/ebx 1/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '81 0/mod 3/rm32/ebx 1/imm32' (combine rm32 with imm32 based on subop): missing subop operand\n"
+  );
+}
+
+void test_check_missing_base_operand() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "81 0/add/subop 0/mod/indirect 4/rm32/use-sib 1/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '81 0/add/subop 0/mod/indirect 4/rm32/use-sib 1/imm32' (combine rm32 with imm32 based on subop): missing base operand\n"
+  );
+}
+
+void test_check_missing_index_operand() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "81 0/add/subop 0/mod/indirect 4/rm32/use-sib 0/base 1/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '81 0/add/subop 0/mod/indirect 4/rm32/use-sib 0/base 1/imm32' (combine rm32 with imm32 based on subop): missing index operand\n"
+  );
+}
+
+void test_check_missing_base_operand_2() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "81 0/add/subop 0/mod/indirect 4/rm32/use-sib 2/index 3/scale 1/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '81 0/add/subop 0/mod/indirect 4/rm32/use-sib 2/index 3/scale 1/imm32' (combine rm32 with imm32 based on subop): missing base operand\n"
+  );
+}
+
+void test_check_extra_displacement() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "89/copy 0/mod/indirect 0/rm32/EAX 1/r32/ECX 4/disp8\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '89/copy 0/mod/indirect 0/rm32/EAX 1/r32/ECX 4/disp8' (copy r32 to rm32): unexpected disp8 operand\n"
+  );
+}
+
+void test_check_duplicate_operand() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "89/copy 0/mod/indirect 0/rm32/EAX 1/r32/ECX 1/r32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '89/copy 0/mod/indirect 0/rm32/EAX 1/r32/ECX 1/r32': duplicate r32 operand\n"
+  );
+}
+
+void test_check_base_operand_not_needed_in_direct_mode() {
+  run(
+      "== 0x1\n"  // code segment
+      "81 0/add/subop 3/mod/indirect 4/rm32/use-sib 1/imm32\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_extra_modrm() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "59/pop-to-ECX  3/mod/direct 1/rm32/ECX 4/r32/ESP\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '59/pop-to-ECX 3/mod/direct 1/rm32/ECX 4/r32/ESP' (pop top of stack to ECX): unexpected modrm operand\n"
+  );
+}
 
 //:: similarly handle multi-byte opcodes
 
-:(code)
 void check_operands_0f(const line& inst) {
   assert(inst.words.at(0).data == "0f");
   if (SIZE(inst.words) == 1) {
@@ -528,14 +604,16 @@ void check_operands_f3(const line& /*unused*/) {
   raise << "no supported opcodes starting with f3\n" << end();
 }
 
-:(scenario check_missing_disp32_operand)
-% Hide_errors = true;
-== 0x1
-# instruction                     effective address                                                   operand     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
-  0f 84                                                                                                                                             # jmp if ZF to ??
-+error: '0f 84' (jump disp32 bytes away if equal, if ZF is set): missing disp32 operand
+void test_check_missing_disp32_operand() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "  0f 84                                                                                                                                             # jmp if ZF to ??\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '0f 84' (jump disp32 bytes away if equal, if ZF is set): missing disp32 operand\n"
+  );
+}
 
 :(before "End Globals")
 map</*op*/string, /*bitvector*/uint8_t> Permitted_operands_0f;
diff --git a/subx/032check_operand_bounds.cc b/subx/032check_operand_bounds.cc
index 58b7c7b9..ca114e22 100644
--- a/subx/032check_operand_bounds.cc
+++ b/subx/032check_operand_bounds.cc
@@ -1,10 +1,15 @@
 //:: Check that the different operands of an instruction aren't too large for their bitfields.
 
-:(scenario check_bitfield_sizes)
-% Hide_errors = true;
-== 0x1
-01/add 4/mod 3/rm32 1/r32  # add ECX to EBX
-+error: '4/mod' too large to fit in bitfield mod
+void test_check_bitfield_sizes() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"  // code segment
+      "01/add 4/mod 3/rm32 1/r32\n"  // add ECX to EBX
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '4/mod' too large to fit in bitfield mod\n"
+  );
+}
 
 :(before "End Globals")
 map<string, uint32_t> Operand_bound;
diff --git a/subx/034compute_segment_address.cc b/subx/034compute_segment_address.cc
index 79c6e45d..a1b7482d 100644
--- a/subx/034compute_segment_address.cc
+++ b/subx/034compute_segment_address.cc
@@ -2,17 +2,22 @@
 //: segment.
 //: This gives up a measure of control in placing code and data.
 
-:(scenario segment_name)
-== code
-05/add-to-EAX  0x0d0c0b0a/imm32
-# code starts at 0x08048000 + p_offset, which is 0x54 for a single-segment binary
-+load: 0x09000054 -> 05
-+load: 0x09000055 -> 0a
-+load: 0x09000056 -> 0b
-+load: 0x09000057 -> 0c
-+load: 0x09000058 -> 0d
-+run: add imm32 0x0d0c0b0a to reg EAX
-+run: storing 0x0d0c0b0a
+void test_segment_name() {
+  run(
+      "== code\n"
+      "05/add-to-EAX  0x0d0c0b0a/imm32\n"
+      // code starts at 0x08048000 + p_offset, which is 0x54 for a single-segment binary
+  );
+  CHECK_TRACE_CONTENTS(
+      "load: 0x09000054 -> 05\n"
+      "load: 0x09000055 -> 0a\n"
+      "load: 0x09000056 -> 0b\n"
+      "load: 0x09000057 -> 0c\n"
+      "load: 0x09000058 -> 0d\n"
+      "run: add imm32 0x0d0c0b0a to reg EAX\n"
+      "run: storing 0x0d0c0b0a\n"
+  );
+}
 
 //: Update the parser to handle non-numeric segment name.
 //:
@@ -61,44 +66,65 @@ if (Currently_parsing_named_segment) {
   return;
 }
 
-:(scenario repeated_segment_merges_data)
-== code
-05/add-to-EAX  0x0d0c0b0a/imm32
-== code
-2d/subtract-from-EAX  0xddccbbaa/imm32
-+parse: new segment 'code'
-+parse: appending to segment 'code'
-# first segment
-+load: 0x09000054 -> 05
-+load: 0x09000055 -> 0a
-+load: 0x09000056 -> 0b
-+load: 0x09000057 -> 0c
-+load: 0x09000058 -> 0d
-# second segment
-+load: 0x09000059 -> 2d
-+load: 0x0900005a -> aa
-+load: 0x0900005b -> bb
-+load: 0x0900005c -> cc
-+load: 0x0900005d -> dd
-
-:(scenario error_on_missing_segment_header)
-% Hide_errors = true;
-05/add-to-EAX 0/imm32
-+error: input does not start with a '==' section header
-
-:(scenario error_on_first_segment_not_code)
-% Hide_errors = true;
-== data
-05 00 00 00 00
-+error: first segment must be 'code' but is 'data'
-
-:(scenario error_on_second_segment_not_data)
-% Hide_errors = true;
-== code
-05/add-to-EAX 0/imm32
-== bss
-05 00 00 00 00
-+error: second segment must be 'data' but is 'bss'
+:(code)
+void test_repeated_segment_merges_data() {
+  run(
+      "== code\n"
+      "05/add-to-EAX  0x0d0c0b0a/imm32\n"
+      "== code\n"  // again
+      "2d/subtract-from-EAX  0xddccbbaa/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: new segment 'code'\n"
+      "parse: appending to segment 'code'\n"
+      // first segment
+      "load: 0x09000054 -> 05\n"
+      "load: 0x09000055 -> 0a\n"
+      "load: 0x09000056 -> 0b\n"
+      "load: 0x09000057 -> 0c\n"
+      "load: 0x09000058 -> 0d\n"
+      // second segment
+      "load: 0x09000059 -> 2d\n"
+      "load: 0x0900005a -> aa\n"
+      "load: 0x0900005b -> bb\n"
+      "load: 0x0900005c -> cc\n"
+      "load: 0x0900005d -> dd\n"
+  );
+}
+
+void test_error_on_missing_segment_header() {
+  Hide_errors = true;
+  run(
+      "05/add-to-EAX 0/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: input does not start with a '==' section header\n"
+  );
+}
+
+void test_error_on_first_segment_not_code() {
+  Hide_errors = true;
+  run(
+      "== data\n"
+      "05 00 00 00 00\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: first segment must be 'code' but is 'data'\n"
+  );
+}
+
+void test_error_on_second_segment_not_data() {
+  Hide_errors = true;
+  run(
+      "== code\n"
+      "05/add-to-EAX 0/imm32\n"
+      "== bss\n"
+      "05 00 00 00 00\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: second segment must be 'data' but is 'bss'\n"
+  );
+}
 
 //: compute segment address
 
diff --git a/subx/035labels.cc b/subx/035labels.cc
index 4c5ea641..322d1952 100644
--- a/subx/035labels.cc
+++ b/subx/035labels.cc
@@ -21,13 +21,18 @@
 
 //: One special label: the address to start running the program at.
 
-:(scenario entry_label)
-== 0x1
-05 0x0d0c0b0a/imm32
-Entry:
-05 0x0d0c0b0a/imm32
-+run: 0x00000006 opcode: 05
--run: 0x00000001 opcode: 05
+void test_entry_label() {
+  run(
+      "== 0x1\n"  // code segment
+      "05 0x0d0c0b0a/imm32\n"
+      "Entry:\n"
+      "05 0x0d0c0b0a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000006 opcode: 05\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000001 opcode: 05");
+}
 
 :(before "End Globals")
 uint32_t Entry_address = 0;
@@ -41,33 +46,47 @@ if (Entry_address) e_entry = Entry_address;
 :(before "End looks_like_hex_int(s) Detectors")
 if (SIZE(s) == 2) return true;
 
-:(scenarios transform)
-:(scenario pack_immediate_ignores_single_byte_nondigit_operand)
-% Hide_errors = true;
-== 0x1
-b9/copy  a/imm32
-+transform: packing instruction 'b9/copy a/imm32'
-# no change (we're just not printing metadata to the trace)
-+transform: instruction after packing: 'b9 a'
+:(code)
+void test_pack_immediate_ignores_single_byte_nondigit_operand() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"  // code segment
+      "b9/copy  a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction 'b9/copy a/imm32'\n"
+      // no change (we're just not printing metadata to the trace)
+      "transform: instruction after packing: 'b9 a'\n"
+  );
+}
 
-:(scenario pack_immediate_ignores_3_hex_digit_operand)
-% Hide_errors = true;
-== 0x1
-b9/copy  aaa/imm32
-+transform: packing instruction 'b9/copy aaa/imm32'
-# no change (we're just not printing metadata to the trace)
-+transform: instruction after packing: 'b9 aaa'
+void test_pack_immediate_ignores_3_hex_digit_operand() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"  // code segment
+      "b9/copy  aaa/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction 'b9/copy aaa/imm32'\n"
+      // no change (we're just not printing metadata to the trace)
+      "transform: instruction after packing: 'b9 aaa'\n"
+  );
+}
 
-:(scenario pack_immediate_ignores_non_hex_operand)
-% Hide_errors = true;
-== 0x1
-b9/copy xxx/imm32
-+transform: packing instruction 'b9/copy xxx/imm32'
-# no change (we're just not printing metadata to the trace)
-+transform: instruction after packing: 'b9 xxx'
+void test_pack_immediate_ignores_non_hex_operand() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"  // code segment
+      "b9/copy xxx/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: packing instruction 'b9/copy xxx/imm32'\n"
+      // no change (we're just not printing metadata to the trace)
+      "transform: instruction after packing: 'b9 xxx'\n"
+  );
+}
 
 //: a helper we'll find handy later
-:(code)
 void check_valid_name(const string& s) {
   if (s.empty()) {
     raise << "empty name!\n" << end();
@@ -87,11 +106,16 @@ void check_valid_name(const string& s) {
 
 //: Now that that's done, let's start using names as labels.
 
-:(scenario map_label)
-== 0x1
-loop:
-  05  0x0d0c0b0a/imm32
-+transform: label 'loop' is at address 1
+void test_map_label() {
+  transform(
+      "== 0x1\n"  // code segment
+      "loop:\n"
+      "  05  0x0d0c0b0a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: label 'loop' is at address 1\n"
+  );
+}
 
 :(before "End Level-2 Transforms")
 Transform.push_back(rewrite_labels);
@@ -241,67 +265,97 @@ string drop_last(const string& s) {
 //: However, you can absolutely have multiple labels map to the same address,
 //: as long as they're on separate lines.
 
-:(scenario multiple_labels_at)
-== 0x1
-# address 1
-loop:
- $loop2:
-# address 1 (labels take up no space)
-    05  0x0d0c0b0a/imm32
-# address 6
-    eb  $loop2/disp8
-# address 8
-    eb  $loop3/disp8
-# address 0xa
- $loop3:
-+transform: label 'loop' is at address 1
-+transform: label '$loop2' is at address 1
-+transform: label '$loop3' is at address a
-# first jump is to -7
-+transform: instruction after transform: 'eb f9'
-# second jump is to 0 (fall through)
-+transform: instruction after transform: 'eb 00'
+void test_multiple_labels_at() {
+  transform(
+      "== 0x1\n"  // code segment
+      // address 1
+      "loop:\n"
+      " $loop2:\n"
+      // address 1 (labels take up no space)
+      "    05  0x0d0c0b0a/imm32\n"
+      // address 6
+      "    eb  $loop2/disp8\n"
+      // address 8
+      "    eb  $loop3/disp8\n"
+      // address 0xa
+      " $loop3:\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: label 'loop' is at address 1\n"
+      "transform: label '$loop2' is at address 1\n"
+      "transform: label '$loop3' is at address a\n"
+      // first jump is to -7
+      "transform: instruction after transform: 'eb f9'\n"
+      // second jump is to 0 (fall through)
+      "transform: instruction after transform: 'eb 00'\n"
+  );
+}
 
-:(scenario duplicate_label)
-% Hide_errors = true;
-== 0x1
-loop:
-loop:
-    05  0x0d0c0b0a/imm32
-+error: duplicate label 'loop'
+void test_duplicate_label() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"
+      "loop:\n"
+      "loop:\n"
+      "    05  0x0d0c0b0a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: duplicate label 'loop'\n"
+  );
+}
 
-:(scenario label_too_short)
-% Hide_errors = true;
-== 0x1
-xz:
-  05  0x0d0c0b0a/imm32
-+error: 'xz' is two characters long which can look like raw hex bytes at a glance; use a different name
+void test_label_too_short() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"
+      "xz:\n"
+      "  05  0x0d0c0b0a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: 'xz' is two characters long which can look like raw hex bytes at a glance; use a different name\n"
+  );
+}
 
-:(scenario label_hex)
-% Hide_errors = true;
-== 0x1
-0xab:
-  05  0x0d0c0b0a/imm32
-+error: '0xab' looks like a hex number; use a different name
+void test_label_hex() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"
+      "0xab:\n"
+      "  05  0x0d0c0b0a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '0xab' looks like a hex number; use a different name\n"
+  );
+}
 
-:(scenario label_negative_hex)
-% Hide_errors = true;
-== 0x1
- -a:  # indent to avoid looking like a trace_should_not_contain command for this scenario
-    05  0x0d0c0b0a/imm32
-+error: '-a' starts with '-', which can be confused with a negative number; use a different name
+void test_label_negative_hex() {
+  Hide_errors = true;
+  transform(
+      "== 0x1\n"
+      "-a:\n"
+      "    05  0x0d0c0b0a/imm32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: '-a' starts with '-', which can be confused with a negative number; use a different name\n"
+  );
+}
 
 //: now that we have labels, we need to adjust segment size computation to
 //: ignore them.
 
-:(scenario segment_size_ignores_labels)
-== code  # 0x09000074
-  05/add  0x0d0c0b0a/imm32  # 5 bytes
-foo:                      # 0 bytes
-== data  # 0x0a000079
-bar:
-  00
-+transform: segment 1 begins at address 0x0a000079
+void test_segment_size_ignores_labels() {
+  transform(
+      "== code\n"  // 0x09000074
+      "  05/add  0x0d0c0b0a/imm32\n"  // 5 bytes
+      "foo:\n"                        // 0 bytes
+      "== data\n"  // 0x0a000079
+      "bar:\n"
+      "  00\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: segment 1 begins at address 0x0a000079\n"
+  );
+}
 
 :(before "End size_of(word w) Special-cases")
 else if (is_label(w))
diff --git a/subx/036global_variables.cc b/subx/036global_variables.cc
index 2d20ca43..846cd291 100644
--- a/subx/036global_variables.cc
+++ b/subx/036global_variables.cc
@@ -6,13 +6,19 @@
 //:
 //: This layer has much the same structure as rewriting labels.
 
-:(scenario global_variable)
-== code
-b9  x/imm32
-== data
-x:
-  00 00 00 00
-+transform: global variable 'x' is at address 0x0a000079
+:(code)
+void test_global_variable() {
+  run(
+      "== code\n"
+      "b9  x/imm32\n"
+      "== data\n"
+      "x:\n"
+      "  00 00 00 00\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: global variable 'x' is at address 0x0a000079\n"
+  );
+}
 
 :(before "End Level-2 Transforms")
 Transform.push_back(rewrite_global_variables);
@@ -173,83 +179,112 @@ bool has_metadata(const word& w, const string& m) {
   return false;
 }
 
-:(scenario global_variable_disallowed_in_jump)
-% Hide_errors = true;
-== code
-eb/jump  x/disp8
-== data
-x:
-  00 00 00 00
-+error: 'eb/jump x/disp8': can't refer to global variable 'x'
-# sub-optimal error message; should be
-#? +error: can't jump to data (variable 'x')
+void test_global_variable_disallowed_in_jump() {
+  Hide_errors = true;
+  run(
+      "== code\n"
+      "eb/jump  x/disp8\n"
+      "== data\n"
+      "x:\n"
+      "  00 00 00 00\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: 'eb/jump x/disp8': can't refer to global variable 'x'\n"
+      // sub-optimal error message; should be
+//?       "error: can't jump to data (variable 'x')\n"
+  );
+}
 
-:(scenario global_variable_disallowed_in_call)
-% Hide_errors = true;
-== code
-e8/call  x/disp32
-== data
-x:
-  00 00 00 00
-+error: 'e8/call x/disp32': can't refer to global variable 'x'
-# sub-optimal error message; should be
-#? +error: can't call to the data segment ('x')
+void test_global_variable_disallowed_in_call() {
+  Hide_errors = true;
+  run(
+      "== code\n"
+      "e8/call  x/disp32\n"
+      "== data\n"
+      "x:\n"
+      "  00 00 00 00\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: 'e8/call x/disp32': can't refer to global variable 'x'\n"
+      // sub-optimal error message; should be
+//?       "error: can't call to the data segment ('x')\n"
+  );
+}
 
-:(scenario global_variable_in_data_segment)
-== 0x1
-b9  x/imm32
-== 0x0a000000
-x:
-  y/imm32
-y:
-  00 00 00 00
-# check that we loaded 'x' with the address of 'y'
-+load: 0x0a000000 -> 04
-+load: 0x0a000001 -> 00
-+load: 0x0a000002 -> 00
-+load: 0x0a000003 -> 0a
-$error: 0
+void test_global_variable_in_data_segment() {
+  run(
+      "== 0x1\n"
+      "b9  x/imm32\n"
+      "== 0x0a000000\n"
+      "x:\n"
+      "  y/imm32\n"
+      "y:\n"
+      "  00 00 00 00\n"
+  );
+  // check that we loaded 'x' with the address of 'y'
+  CHECK_TRACE_CONTENTS(
+      "load: 0x0a000000 -> 04\n"
+      "load: 0x0a000001 -> 00\n"
+      "load: 0x0a000002 -> 00\n"
+      "load: 0x0a000003 -> 0a\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(scenario raw_number_with_imm32_in_data_segment)
-== 0x1
-b9  x/imm32
-== 0x0a000000
-x:
-  1/imm32
-# check that we loaded 'x' with the address of 1
-+load: 0x0a000000 -> 01
-+load: 0x0a000001 -> 00
-+load: 0x0a000002 -> 00
-+load: 0x0a000003 -> 00
-$error: 0
+void test_raw_number_with_imm32_in_data_segment() {
+  run(
+      "== 0x1\n"
+      "b9  x/imm32\n"
+      "== 0x0a000000\n"
+      "x:\n"
+      "  1/imm32\n"
+  );
+  // check that we loaded 'x' with the address of 1
+  CHECK_TRACE_CONTENTS(
+      "load: 0x0a000000 -> 01\n"
+      "load: 0x0a000001 -> 00\n"
+      "load: 0x0a000002 -> 00\n"
+      "load: 0x0a000003 -> 00\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(scenario duplicate_global_variable)
-% Hide_errors = true;
-== 0x1
-40/increment-EAX
-== 0x0a000000
-x:
-x:
-  00
-+error: duplicate global 'x'
+void test_duplicate_global_variable() {
+  Hide_errors = true;
+  run(
+      "== 0x1\n"
+      "40/increment-EAX\n"
+      "== 0x0a000000\n"
+      "x:\n"
+      "x:\n"
+      "  00\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: duplicate global 'x'\n"
+  );
+}
 
-:(scenario global_variable_disp32_with_modrm)
-== code
-8b/copy 0/mod/indirect 5/rm32/.disp32 2/r32/EDX x/disp32
-== data
-x:
-  00 00 00 00
-$error: 0
+void test_global_variable_disp32_with_modrm() {
+  run(
+      "== code\n"
+      "8b/copy 0/mod/indirect 5/rm32/.disp32 2/r32/EDX x/disp32\n"
+      "== data\n"
+      "x:\n"
+      "  00 00 00 00\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(scenarios transform)
-:(scenario global_variable_disp32_with_call)
-== code
-foo:
-  e8/call bar/disp32
-bar:
-$error: 0
+void test_global_variable_disp32_with_call() {
+  transform(
+      "== code\n"
+      "foo:\n"
+      "  e8/call bar/disp32\n"
+      "bar:\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(code)
 string to_full_string(const line& in) {
   ostringstream out;
   for (int i = 0;  i < SIZE(in.words);  ++i) {
diff --git a/subx/038---literal_strings.cc b/subx/038---literal_strings.cc
index ce46a119..b7cb0aa4 100644
--- a/subx/038---literal_strings.cc
+++ b/subx/038---literal_strings.cc
@@ -3,13 +3,18 @@
 //: This layer will transparently move them to the global segment (assumed to
 //: always be the second segment).
 
-:(scenario transform_literal_string)
-== code
-b8/copy  "test"/imm32
-== data  # need to manually create this for now
-+transform: -- move literal strings to data segment
-+transform: adding global variable '__subx_global_1' containing "test"
-+transform: instruction after transform: 'b8 __subx_global_1'
+void test_transform_literal_string() {
+  run(
+      "== code\n"
+      "b8/copy  \"test\"/imm32\n"
+      "== data\n"  // need to manually create the segment for now
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: -- move literal strings to data segment\n"
+      "transform: adding global variable '__subx_global_1' containing \"test\"\n"
+      "transform: instruction after transform: 'b8 __subx_global_1'\n"
+  );
+}
 
 //: We don't rely on any transforms running in previous layers, but this layer
 //: knows about labels and global variables and will emit them for previous
@@ -70,14 +75,18 @@ void add_global_to_data_segment(const string& name, const word& value, segment&
 //: Within strings, whitespace is significant. So we need to redo our instruction
 //: parsing.
 
-:(scenarios parse_instruction_character_by_character)
-:(scenario instruction_with_string_literal)
-a "abc  def" z  # two spaces inside string
-+parse2: word: a
-+parse2: word: "abc  def"
-+parse2: word: z
-# no other words
-$parse2: 3
+void test_instruction_with_string_literal() {
+  parse_instruction_character_by_character(
+      "a \"abc  def\" z\n"  // two spaces inside string
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: a\n"
+      "parse2: word: \"abc  def\"\n"
+      "parse2: word: z\n"
+  );
+  // no other words
+  CHECK_TRACE_COUNT("parse2", 3);
+}
 
 :(before "End Line Parsing Special-cases(line_data -> l)")
 if (line_data.find('"') != string::npos) {  // can cause false-positives, but we can handle them
@@ -168,68 +177,113 @@ void parse_instruction_character_by_character(const string& line_data) {
   parse_instruction_character_by_character(line_data, out);
 }
 
-:(scenario parse2_comment_token_in_middle)
-a . z
-+parse2: word: a
-+parse2: word: z
--parse2: word: .
-# no other words
-$parse2: 2
-
-:(scenario parse2_word_starting_with_dot)
-a .b c
-+parse2: word: a
-+parse2: word: .b
-+parse2: word: c
-
-:(scenario parse2_comment_token_at_start)
-. a b
-+parse2: word: a
-+parse2: word: b
--parse2: word: .
-
-:(scenario parse2_comment_token_at_end)
-a b .
-+parse2: word: a
-+parse2: word: b
--parse2: word: .
-
-:(scenario parse2_word_starting_with_dot_at_start)
-.a b c
-+parse2: word: .a
-+parse2: word: b
-+parse2: word: c
-
-:(scenario parse2_metadata)
-.a b/c d
-+parse2: word: .a
-+parse2: word: b /c
-+parse2: word: d
-
-:(scenario parse2_string_with_metadata)
-a "bc  def"/disp32 g
-+parse2: word: a
-+parse2: word: "bc  def" /disp32
-+parse2: word: g
-
-:(scenario parse2_string_with_metadata_at_end)
-a "bc  def"/disp32
-+parse2: word: a
-+parse2: word: "bc  def" /disp32
+void test_parse2_comment_token_in_middle() {
+  parse_instruction_character_by_character(
+      "a . z\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: a\n"
+      "parse2: word: z\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("parse2: word: .");
+  // no other words
+  CHECK_TRACE_COUNT("parse2", 2);
+}
+
+void test_parse2_word_starting_with_dot() {
+  parse_instruction_character_by_character(
+      "a .b c\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: a\n"
+      "parse2: word: .b\n"
+      "parse2: word: c\n"
+  );
+}
+
+void test_parse2_comment_token_at_start() {
+  parse_instruction_character_by_character(
+      ". a b\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: a\n"
+      "parse2: word: b\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("parse2: word: .");
+}
+
+void test_parse2_comment_token_at_end() {
+  parse_instruction_character_by_character(
+      "a b .\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: a\n"
+      "parse2: word: b\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("parse2: word: .");
+}
+
+void test_parse2_word_starting_with_dot_at_start() {
+  parse_instruction_character_by_character(
+      ".a b c\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: .a\n"
+      "parse2: word: b\n"
+      "parse2: word: c\n"
+  );
+}
+
+void test_parse2_metadata() {
+  parse_instruction_character_by_character(
+      ".a b/c d\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: .a\n"
+      "parse2: word: b /c\n"
+      "parse2: word: d\n"
+  );
+}
+
+void test_parse2_string_with_metadata() {
+  parse_instruction_character_by_character(
+      "a \"bc  def\"/disp32 g\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: a\n"
+      "parse2: word: \"bc  def\" /disp32\n"
+      "parse2: word: g\n"
+  );
+}
+
+void test_parse2_string_with_metadata_at_end() {
+  parse_instruction_character_by_character(
+      "a \"bc  def\"/disp32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: a\n"
+      "parse2: word: \"bc  def\" /disp32\n"
+  );
+}
 
-:(code)
 void test_parse2_string_with_metadata_at_end_of_line_without_newline() {
   parse_instruction_character_by_character(
       "68/push \"test\"/f"  // no newline, which is how calls from parse() will look
   );
   CHECK_TRACE_CONTENTS(
-      "parse2: word: 68 /push"
-      "parse2: word: \"test\" /f"
+      "parse2: word: 68 /push\n"
+      "parse2: word: \"test\" /f\n"
   );
 }
 
 //: Make sure slashes inside strings don't trigger adding stuff from inside the
 //: string to metadata.
-:(scenario parse2_string_containing_slashes)
-a "bc/def"/disp32
-+parse2: word: "bc/def" /disp32
+
+void test_parse2_string_containing_slashes() {
+  parse_instruction_character_by_character(
+      "a \"bc/def\"/disp32\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse2: word: \"bc/def\" /disp32\n"
+  );
+}
diff --git a/subx/040---tests.cc b/subx/040---tests.cc
index e6f2f489..d35cc711 100644
--- a/subx/040---tests.cc
+++ b/subx/040---tests.cc
@@ -14,21 +14,24 @@
 Transform.push_back(create_test_function);
 // End Level-4 Transforms
 
-:(scenario run_test)
-% Reg[ESP].u = 0x100;
-== 0x1
-main:
-  e8/call run-tests/disp32  # 5 bytes
-  f4/halt                   # 1 byte
-
-test-foo:  # offset 7
-  01 d8  # just some unique instruction: add EBX to EAX
-  c3/return
-
-# check that code in test-foo ran (implicitly called by run-tests)
-+run: 0x00000007 opcode: 01
-
 :(code)
+void test_run_test() {
+  Reg[ESP].u = 0x100;
+  run(
+      "== 0x1\n"  // code segment
+      "main:\n"
+      "  e8/call run-tests/disp32\n"  // 5 bytes
+      "  f4/halt\n"                   // 1 byte
+      "test-foo:\n"  // offset 7
+      "  01 d8\n"  // just some unique instruction: add EBX to EAX
+      "  c3/return\n"
+  );
+  // check that code in test-foo ran (implicitly called by run-tests)
+  CHECK_TRACE_CONTENTS(
+      "run: 0x00000007 opcode: 01\n"
+  );
+}
+
 void create_test_function(program& p) {
   if (p.segments.empty()) return;
   segment& code = p.segments.at(0);