about summary refs log tree commit diff stats
path: root/subx
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2019-03-12 18:56:55 -0700
committerKartik Agaram <vc@akkartik.com>2019-03-12 19:14:12 -0700
commit4a943d4ed313eff001504c2b5c472266e86a38af (patch)
treea5757233a8c81b303a808f251180c7344071ed51 /subx
parent43711b0e9f18e0225ce14687fb6ea0902aa6fc61 (diff)
downloadmu-4a943d4ed313eff001504c2b5c472266e86a38af.tar.gz
5001 - drop the :(scenario) DSL
I've been saying for a while[1][2][3] that adding extra abstractions makes
things harder for newcomers, and adding new notations doubly so. And then
I notice this DSL in my own backyard. Makes me feel like a hypocrite.

[1] https://news.ycombinator.com/item?id=13565743#13570092
[2] https://lobste.rs/s/to8wpr/configuration_files_are_canary_warning
[3] https://lobste.rs/s/mdmcdi/little_languages_by_jon_bentley_1986#c_3miuf2

The implementation of the DSL was also highly hacky:

a) It was happening in the tangle/ tool, but was utterly unrelated to tangling
layers.

b) There were several persnickety constraints on the different kinds of
lines and the specific order they were expected in. I kept finding bugs
where the translator would silently do the wrong thing. Or the error messages
sucked, and readers may be stuck looking at the generated code to figure
out what happened. Fixing error messages would require a lot more code,
which is one of my arguments against DSLs in the first place: they may
be easy to implement, but they're hard to design to go with the grain of
the underlying platform. They require lots of iteration. Is that effort
worth prioritizing in this project?

On the other hand, the DSL did make at least some readers' life easier,
the ones who weren't immediately put off by having to learn a strange syntax.
There were fewer quotes to parse, fewer backslash escapes.

Anyway, since there are also people who dislike having to put up with strange
syntaxes, we'll call that consideration a wash and tear this DSL out.

---

This commit was sheer drudgery. Hopefully it won't need to be redone with
a new DSL because I grow sick of backslashes.
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);