about summary refs log tree commit diff stats
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
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.
-rw-r--r--003trace.cc44
-rw-r--r--003trace.test.cc4
-rw-r--r--011load.cc379
-rw-r--r--013update_operation.cc18
-rw-r--r--014literal_string.cc185
-rw-r--r--015literal_noninteger.cc16
-rw-r--r--016dilated_reagent.cc61
-rw-r--r--017parse_tree.cc72
-rw-r--r--018constant.cc50
-rw-r--r--019type_abbreviations.cc144
-rw-r--r--020run.cc199
-rw-r--r--021check_instruction.cc112
-rw-r--r--022arithmetic.cc805
-rw-r--r--023boolean.cc166
-rw-r--r--024jump.cc183
-rw-r--r--025compare.cc439
-rw-r--r--026call.cc100
-rw-r--r--027call_ingredient.cc127
-rw-r--r--028call_return.cc134
-rw-r--r--029tools.cc63
-rw-r--r--030container.cc593
-rw-r--r--031merge.cc183
-rw-r--r--032array.cc451
-rw-r--r--033exclusive_container.cc585
-rw-r--r--034address.cc342
-rw-r--r--035lookup.cc646
-rw-r--r--036abandon.cc105
-rw-r--r--038new_text.cc191
-rw-r--r--040brace.cc550
-rw-r--r--041jump_target.cc211
-rw-r--r--042name.cc307
-rw-r--r--043space.cc235
-rw-r--r--044space_surround.cc77
-rw-r--r--045closure_name.cc139
-rw-r--r--046check_type_by_name.cc265
-rw-r--r--050scenario.cc729
-rw-r--r--052tangle.cc596
-rw-r--r--053recipe_header.cc731
-rw-r--r--054static_dispatch.cc578
-rw-r--r--055shape_shifting_container.cc682
-rw-r--r--056shape_shifting_recipe.cc1363
-rw-r--r--057immutable.cc895
-rw-r--r--060rewrite_literal_string.cc25
-rw-r--r--062convert_ingredients_to_text.cc168
-rw-r--r--069hash.cc422
-rw-r--r--072recipe.cc623
-rw-r--r--073scheduler.cc512
-rw-r--r--074wait.cc433
-rw-r--r--076continuation.cc298
-rw-r--r--082scenario_screen.cc269
-rw-r--r--085scenario_console.cc150
-rw-r--r--089scenario_filesystem.cc137
-rw-r--r--099hardware_checks.cc18
-rw-r--r--101run_sandboxed.cc423
-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
-rw-r--r--tangle/001trace.cc2
-rw-r--r--tangle/001trace.test.cc9
-rw-r--r--tangle/003tangle.cc149
-rw-r--r--tangle/003tangle.test.cc530
-rw-r--r--transect/000organization.cc136
-rw-r--r--transect/001help.cc261
-rw-r--r--transect/002test.cc104
-rw-r--r--transect/003trace.cc408
-rw-r--r--transect/003trace.test.cc124
-rw-r--r--transect/010vm.cc230
-rw-r--r--transect/011load.cc228
-rw-r--r--vimrc.vim23
85 files changed, 13399 insertions, 11234 deletions
diff --git a/003trace.cc b/003trace.cc
index af707354..18f15347 100644
--- a/003trace.cc
+++ b/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
@@ -177,7 +175,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;
@@ -221,7 +219,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()) {
@@ -292,14 +290,14 @@ void scroll_to_bottom_and_close_console() {
     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;
@@ -413,7 +411,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/003trace.test.cc b/003trace.test.cc
index 67b4c345..30f61b5a 100644
--- a/003trace.test.cc
+++ b/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/011load.cc b/011load.cc
index 575509ea..4bb06c36 100644
--- a/011load.cc
+++ b/011load.cc
@@ -3,16 +3,19 @@
 //: The process of running Mu code:
 //:   load -> transform -> run
 
-:(scenarios load)  // use 'load' instead of 'run' in all scenarios in this layer
-:(scenario first_recipe)
-def main [
-  1:number <- copy 23
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {1: "number"}
+void test_first_recipe() {
+  load(
+      "def main [\n"
+      "  1:number <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+  );
+}
 
-:(code)
 vector<recipe_ordinal> load(string form) {
   istringstream in(form);
   in >> std::noskipws;
@@ -237,123 +240,183 @@ void skip_comment(istream& in) {
   }
 }
 
-:(scenario recipe_instead_of_def)
-recipe main [
-  1:number <- copy 23
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {1: "number"}
-
-:(scenario parse_comment_outside_recipe)
-# this comment will be dropped by the tangler, so we need a dummy recipe to stop that
-def f1 [
-]
-# this comment will go through to 'load'
-def main [
-  1:number <- copy 23
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {1: "number"}
-
-:(scenario parse_comment_amongst_instruction)
-def main [
-  # comment
-  1:number <- copy 23
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {1: "number"}
-
-:(scenario parse_comment_amongst_instruction_2)
-def main [
-  # comment
-  1:number <- copy 23
-  # comment
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {1: "number"}
-
-:(scenario parse_comment_amongst_instruction_3)
-def main [
-  1:number <- copy 23
-  # comment
-  2:number <- copy 23
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {1: "number"}
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {2: "number"}
-
-:(scenario parse_comment_after_instruction)
-def main [
-  1:number <- copy 23  # comment
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {1: "number"}
-
-:(scenario parse_label)
-def main [
-  +foo
-]
-+parse: label: +foo
-
-:(scenario parse_dollar_as_recipe_name)
-def main [
-  $foo
-]
-+parse: instruction: $foo
-
-:(scenario parse_multiple_properties)
-def main [
-  1:number <- copy 23/foo:bar:baz
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal", "foo": ("bar" "baz")}
-+parse:   product: {1: "number"}
-
-:(scenario parse_multiple_products)
-def main [
-  1:number, 2:number <- copy 23
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   product: {1: "number"}
-+parse:   product: {2: "number"}
-
-:(scenario parse_multiple_ingredients)
-def main [
-  1:number, 2:number <- copy 23, 4:number
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   ingredient: {4: "number"}
-+parse:   product: {1: "number"}
-+parse:   product: {2: "number"}
-
-:(scenario parse_multiple_types)
-def main [
-  1:number, 2:address:number <- copy 23, 4:number
-]
-+parse: instruction: copy
-+parse:   ingredient: {23: "literal"}
-+parse:   ingredient: {4: "number"}
-+parse:   product: {1: "number"}
-+parse:   product: {2: ("address" "number")}
-
-:(scenario parse_properties)
-def main [
-  1:address:number/lookup <- copy 23
-]
-+parse:   product: {1: ("address" "number"), "lookup": ()}
-
-//: this test we can't represent with a scenario
-:(code)
+void test_recipe_instead_of_def() {
+  load(
+      "recipe main [\n"
+      "  1:number <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+  );
+}
+
+void test_parse_comment_outside_recipe() {
+  load(
+      "# comment\n"
+      "def main [\n"
+      "  1:number <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+  );
+}
+
+void test_parse_comment_amongst_instruction() {
+  load(
+      "def main [\n"
+      "  # comment\n"
+      "  1:number <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+  );
+}
+
+void test_parse_comment_amongst_instruction_2() {
+  load(
+      "def main [\n"
+      "  # comment\n"
+      "  1:number <- copy 23\n"
+      "  # comment\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+  );
+}
+
+void test_parse_comment_amongst_instruction_3() {
+  load(
+      "def main [\n"
+      "  1:number <- copy 23\n"
+      "  # comment\n"
+      "  2:number <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {2: \"number\"}\n"
+  );
+}
+
+void test_parse_comment_after_instruction() {
+  load(
+      "def main [\n"
+      "  1:number <- copy 23  # comment\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+  );
+}
+
+void test_parse_label() {
+  load(
+      "def main [\n"
+      "  +foo\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: label: +foo\n"
+  );
+}
+
+void test_parse_dollar_as_recipe_name() {
+  load(
+      "def main [\n"
+      "  $foo\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: $foo\n"
+  );
+}
+
+void test_parse_multiple_properties() {
+  load(
+      "def main [\n"
+      "  1:number <- copy 23/foo:bar:baz\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\", \"foo\": (\"bar\" \"baz\")}\n"
+      "parse:   product: {1: \"number\"}\n"
+  );
+}
+
+void test_parse_multiple_products() {
+  load(
+      "def main [\n"
+      "  1:number, 2:number <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+      "parse:   product: {2: \"number\"}\n"
+  );
+}
+
+void test_parse_multiple_ingredients() {
+  load(
+      "def main [\n"
+      "  1:number, 2:number <- copy 23, 4:number\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   ingredient: {4: \"number\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+      "parse:   product: {2: \"number\"}\n"
+  );
+}
+
+void test_parse_multiple_types() {
+  load(
+      "def main [\n"
+      "  1:number, 2:address:number <- copy 23, 4:number\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: instruction: copy\n"
+      "parse:   ingredient: {23: \"literal\"}\n"
+      "parse:   ingredient: {4: \"number\"}\n"
+      "parse:   product: {1: \"number\"}\n"
+      "parse:   product: {2: (\"address\" \"number\")}\n"
+  );
+}
+
+void test_parse_properties() {
+  load(
+      "def main [\n"
+      "  1:address:number/lookup <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   product: {1: (\"address\" \"number\"), \"lookup\": ()}\n"
+  );
+}
+
 void test_parse_comment_terminated_by_eof() {
   load("recipe main [\n"
        "  a:number <- copy 34\n"
@@ -362,12 +425,17 @@ void test_parse_comment_terminated_by_eof() {
   cerr << ".";  // termination = success
 }
 
-:(scenario warn_on_missing_space_before_bracket)
-% Hide_errors = true;
-def main[
-  1:number <- copy 23
-]
-+error: insert a space before '[' in 'main['
+void test_warn_on_missing_space_before_bracket() {
+  Hide_errors = true;
+  load(
+      "def main[\n"
+      "  1:number <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: insert a space before '[' in 'main['\n"
+  );
+}
 
 //: Warn if a recipe gets redefined, because large codebases can accidentally
 //: step on their own toes. But there'll be many occasions later where
@@ -382,27 +450,34 @@ bool should_check_for_redefine(const string& recipe_name) {
   return true;
 }
 
-:(scenario forbid_redefining_recipes)
-% Hide_errors = true;
-def main [
-  1:number <- copy 23
-]
-def main [
-  1:number <- copy 24
-]
-+error: redefining recipe main
-
-:(scenario permit_forcibly_redefining_recipes)
-def main [
-  1:number <- copy 23
-]
-def! main [
-  1:number <- copy 24
-]
--error: redefining recipe main
-$error: 0
+void test_forbid_redefining_recipes() {
+  Hide_errors = true;
+  load(
+      "def main [\n"
+      "  1:number <- copy 23\n"
+      "]\n"
+      "def main [\n"
+      "  1:number <- copy 24\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: redefining recipe main\n"
+  );
+}
+
+void test_permit_forcibly_redefining_recipes() {
+  load(
+      "def main [\n"
+      "  1:number <- copy 23\n"
+      "]\n"
+      "def! main [\n"
+      "  1:number <- copy 24\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("error: redefining recipe main");
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(code)
 // for debugging
 void show_rest_of_stream(istream& in) {
   cerr << '^';
diff --git a/013update_operation.cc b/013update_operation.cc
index c5a736f6..ffe3dbb9 100644
--- a/013update_operation.cc
+++ b/013update_operation.cc
@@ -27,10 +27,14 @@ string maybe(string recipe_name) {
   return recipe_name + ": ";
 }
 
-:(scenarios transform)
-:(scenario missing_arrow)
-% Hide_errors = true;
-def main [
-  1:number , copy 0  # typo: ',' instead of '<-'
-]
-+error: main: instruction '1:number' has no recipe in '1:number copy, 0'
+void test_missing_arrow() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  1:number , copy 0\n"  // typo: ',' instead of '<-'
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: instruction '1:number' has no recipe in '1:number copy, 0'\n"
+  );
+}
diff --git a/014literal_string.cc b/014literal_string.cc
index eb5422ff..84dbe8d0 100644
--- a/014literal_string.cc
+++ b/014literal_string.cc
@@ -6,18 +6,27 @@
 //: imagine that 'recipe' might one day itself be defined in Mu, doing its own
 //: parsing.
 
-:(scenarios load)
-:(scenario string_literal)
-def main [
-  1:address:array:character <- copy [abc def]
-]
-+parse:   ingredient: {"abc def": "literal-string"}
-
-:(scenario string_literal_with_colons)
-def main [
-  1:address:array:character <- copy [abc:def/ghi]
-]
-+parse:   ingredient: {"abc:def/ghi": "literal-string"}
+void test_string_literal() {
+  load(
+      "def main [\n"
+      "  1:address:array:character <- copy [abc def]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {\"abc def\": \"literal-string\"}\n"
+  );
+}
+
+void test_string_literal_with_colons() {
+  load(
+      "def main [\n"
+      "  1:address:array:character <- copy [abc:def/ghi]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {\"abc:def/ghi\": \"literal-string\"}\n"
+  );
+}
 
 :(before "End Mu Types Initialization")
 put(Type_ordinal, "literal-string", 0);
@@ -167,59 +176,99 @@ void slurp_one_past_backslashes(istream& in, ostream& out) {
   }
 }
 
-:(scenario string_literal_nested)
-def main [
-  1:address:array:character <- copy [abc [def]]
-]
-+parse:   ingredient: {"abc [def]": "literal-string"}
-
-:(scenario string_literal_escaped)
-def main [
-  1:address:array:character <- copy [abc \[def]
-]
-+parse:   ingredient: {"abc [def": "literal-string"}
-
-:(scenario string_literal_escaped_twice)
-def main [
-  1:address:array:character <- copy [
-abc \\[def]
-]
-+parse:   ingredient: {"\nabc \[def": "literal-string"}
-
-:(scenario string_literal_and_comment)
-def main [
-  1:address:array:character <- copy [abc]  # comment
-]
-+parse: --- defining main
-+parse: instruction: copy
-+parse:   number of ingredients: 1
-+parse:   ingredient: {"abc": "literal-string"}
-+parse:   product: {1: ("address" "array" "character")}
-
-:(scenario string_literal_escapes_newlines_in_trace)
-def main [
-  copy [abc
-def]
-]
-+parse:   ingredient: {"abc\ndef": "literal-string"}
-
-:(scenario string_literal_can_skip_past_comments)
-def main [
-  copy [
-    # ']' inside comment
-    bar
-  ]
-]
-+parse:   ingredient: {"\n    # ']' inside comment\n    bar\n  ": "literal-string"}
-
-:(scenario string_literal_empty)
-def main [
-  copy []
-]
-+parse:   ingredient: {"": "literal-string"}
-
-:(scenario multiple_unfinished_recipes)
-% Hide_errors = true;
-def f1 [
-def f2 [
-+error: unbalanced '['
+void test_string_literal_nested() {
+  load(
+      "def main [\n"
+      "  1:address:array:character <- copy [abc [def]]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {\"abc [def]\": \"literal-string\"}\n"
+  );
+}
+
+void test_string_literal_escaped() {
+  load(
+      "def main [\n"
+      "  1:address:array:character <- copy [abc \\[def]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {\"abc [def\": \"literal-string\"}\n"
+  );
+}
+
+void test_string_literal_escaped_twice() {
+  load(
+      "def main [\n"
+      "  1:address:array:character <- copy [\n"
+      "abc \\\\[def]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {\"\\nabc \\[def\": \"literal-string\"}\n"
+  );
+}
+
+void test_string_literal_and_comment() {
+  load(
+      "def main [\n"
+      "  1:address:array:character <- copy [abc]  # comment\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: --- defining main\n"
+      "parse: instruction: copy\n"
+      "parse:   number of ingredients: 1\n"
+      "parse:   ingredient: {\"abc\": \"literal-string\"}\n"
+      "parse:   product: {1: (\"address\" \"array\" \"character\")}\n"
+  );
+}
+
+void test_string_literal_escapes_newlines_in_trace() {
+  load(
+      "def main [\n"
+      "  copy [abc\n"
+      "def]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {\"abc\\ndef\": \"literal-string\"}\n"
+  );
+}
+
+void test_string_literal_can_skip_past_comments() {
+  load(
+      "def main [\n"
+      "  copy [\n"
+      "    # ']' inside comment\n"
+      "    bar\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {\"\\n    # ']' inside comment\\n    bar\\n  \": \"literal-string\"}\n"
+  );
+}
+
+void test_string_literal_empty() {
+  load(
+      "def main [\n"
+      "  copy []\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {\"\": \"literal-string\"}\n"
+  );
+}
+
+void test_multiple_unfinished_recipes() {
+  Hide_errors = true;
+  load(
+      "def f1 [\n"
+      "def f2 [\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: unbalanced '['\n"
+  );
+}
diff --git a/015literal_noninteger.cc b/015literal_noninteger.cc
index 554bcdd5..12f24586 100644
--- a/015literal_noninteger.cc
+++ b/015literal_noninteger.cc
@@ -1,11 +1,15 @@
 //: Support literal non-integers.
 
-:(scenarios load)
-:(scenario noninteger_literal)
-def main [
-  1:number <- copy 3.14159
-]
-+parse:   ingredient: {3.14159: "literal-fractional-number"}
+void test_noninteger_literal() {
+  load(
+      "def main [\n"
+      "  1:number <- copy 3.14159\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {3.14159: \"literal-fractional-number\"}\n"
+  );
+}
 
 :(after "Parsing reagent(string s)")
 if (is_noninteger(s)) {
diff --git a/016dilated_reagent.cc b/016dilated_reagent.cc
index 1d89ba8d..1354ba45 100644
--- a/016dilated_reagent.cc
+++ b/016dilated_reagent.cc
@@ -2,32 +2,47 @@
 //: grouped by brackets. We'll use this ability in the next layer, when we
 //: generalize types from lists to trees of properties.
 
-:(scenarios load)
-:(scenario dilated_reagent)
-def main [
-  {1: number, foo: bar} <- copy 34
-]
-+parse:   product: {1: "number", "foo": "bar"}
+void test_dilated_reagent() {
+  load(
+      "def main [\n"
+      "  {1: number, foo: bar} <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   product: {1: \"number\", \"foo\": \"bar\"}\n"
+  );
+}
 
-:(scenario load_trailing_space_after_curly_bracket)
-def main [
-  # line below has a space at the end
-  { 
-]
-# successfully parsed
+void test_load_trailing_space_after_curly_bracket() {
+  load(
+      "def main [\n"
+      "  # line below has a space at the end\n"
+      "  { \n"
+      "]\n"
+      "# successfully parsed\n"
+  );
+}
 
-:(scenario dilated_reagent_with_comment)
-def main [
-  {1: number, foo: bar} <- copy 34  # test comment
-]
-+parse:   product: {1: "number", "foo": "bar"}
-$error: 0
+void test_dilated_reagent_with_comment() {
+  load(
+      "def main [\n"
+      "  {1: number, foo: bar} <- copy 34  # test comment\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   product: {1: \"number\", \"foo\": \"bar\"}\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(scenario dilated_reagent_with_comment_immediately_following)
-def main [
-  1:number <- copy {34: literal}  # test comment
-]
-$error: 0
+void test_dilated_reagent_with_comment_immediately_following() {
+  load(
+      "def main [\n"
+      "  1:number <- copy {34: literal}  # test comment\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 //: First augment next_word to group balanced brackets together.
 
diff --git a/017parse_tree.cc b/017parse_tree.cc
index f0130d97..02170f6d 100644
--- a/017parse_tree.cc
+++ b/017parse_tree.cc
@@ -10,12 +10,16 @@
 // For now you can't use the simpler 'colon-based' representation inside type
 // trees. Once you start typing parens, keep on typing parens.
 
-:(scenarios load)
-:(scenario dilated_reagent_with_nested_brackets)
-def main [
-  {1: number, foo: (bar (baz quux))} <- copy 34
-]
-+parse:   product: {1: "number", "foo": ("bar" ("baz" "quux"))}
+void test_dilated_reagent_with_nested_brackets() {
+  load(
+      "def main [\n"
+      "  {1: number, foo: (bar (baz quux))} <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   product: {1: \"number\", \"foo\": (\"bar\" (\"baz\" \"quux\"))}\n"
+  );
+}
 
 :(before "End Parsing Dilated Reagent Property(value)")
 value = parse_string_tree(value);
@@ -81,26 +85,40 @@ string_tree* parse_string_tree(istream& in) {
   return result;
 }
 
-:(scenario dilated_reagent_with_type_tree)
-% Hide_errors = true;  // 'map' isn't defined yet
-def main [
-  {1: (foo (address array character) (bar number))} <- copy 34
-]
-# just to avoid errors
-container foo [
-]
-container bar [
-]
-+parse:   product: {1: ("foo" ("address" "array" "character") ("bar" "number"))}
+void test_dilated_reagent_with_type_tree() {
+  Hide_errors = true;  // 'map' isn't defined yet
+  load(
+      "def main [\n"
+      "  {1: (foo (address array character) (bar number))} <- copy 34\n"
+      "]\n"
+      "container foo [\n"
+      "]\n"
+      "container bar [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   product: {1: (\"foo\" (\"address\" \"array\" \"character\") (\"bar\" \"number\"))}\n"
+  );
+}
 
-:(scenario dilated_empty_tree)
-def main [
-  {1: number, foo: ()} <- copy 34
-]
-+parse:   product: {1: "number", "foo": ()}
+void test_dilated_empty_tree() {
+  load(
+      "def main [\n"
+      "  {1: number, foo: ()} <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   product: {1: \"number\", \"foo\": ()}\n"
+  );
+}
 
-:(scenario dilated_singleton_tree)
-def main [
-  {1: number, foo: (bar)} <- copy 34
-]
-+parse:   product: {1: "number", "foo": ("bar")}
+void test_dilated_singleton_tree() {
+  load(
+      "def main [\n"
+      "  {1: number, foo: (bar)} <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   product: {1: \"number\", \"foo\": (\"bar\")}\n"
+  );
+}
diff --git a/018constant.cc b/018constant.cc
index 97239bbe..bbf3a412 100644
--- a/018constant.cc
+++ b/018constant.cc
@@ -1,17 +1,21 @@
 //: A few literal constants.
 
-:(scenarios load)  // use 'load' instead of 'run' in all scenarios in this layer
-
 :(before "End Mu Types Initialization")
 put(Type_ordinal, "literal-boolean", 0);
 
 //: 'true'
 
-:(scenario true)
-def main [
-  1:boolean <- copy true
-]
-+parse:   ingredient: {true: "literal-boolean"}
+:(code)
+void test_true() {
+  load(
+      "def main [\n"
+      "  1:boolean <- copy true\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {true: \"literal-boolean\"}\n"
+  );
+}
 
 :(before "End Parsing reagent")
 if (name == "true") {
@@ -25,11 +29,17 @@ if (name == "true") {
 
 //: 'false'
 
-:(scenario false)
-def main [
-  1:boolean <- copy false
-]
-+parse:   ingredient: {false: "literal-boolean"}
+:(code)
+void test_false() {
+  load(
+      "def main [\n"
+      "  1:boolean <- copy false\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {false: \"literal-boolean\"}\n"
+  );
+}
 
 :(before "End Parsing reagent")
 if (name == "false") {
@@ -46,11 +56,17 @@ if (name == "false") {
 :(before "End Mu Types Initialization")
 put(Type_ordinal, "literal-address", 0);
 
-:(scenario null)
-def main [
-  1:address:number <- copy null
-]
-+parse:   ingredient: {null: "literal-address"}
+:(code)
+void test_null() {
+  load(
+      "def main [\n"
+      "  1:address:number <- copy null\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse:   ingredient: {null: \"literal-address\"}\n"
+  );
+}
 
 :(before "End Parsing reagent")
 if (name == "null") {
diff --git a/019type_abbreviations.cc b/019type_abbreviations.cc
index dfa214c9..35cc5d90 100644
--- a/019type_abbreviations.cc
+++ b/019type_abbreviations.cc
@@ -1,12 +1,16 @@
 //: For convenience, allow Mu types to be abbreviated.
 
-:(scenarios transform)
-:(scenario type_abbreviations)
-type foo = number
-def main [
-  a:foo <- copy 34
-]
-+transform: product type after expanding abbreviations: "number"
+void test_type_abbreviations() {
+  transform(
+      "type foo = number\n"
+      "def main [\n"
+      "  a:foo <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: product type after expanding abbreviations: \"number\"\n"
+  );
+}
 
 :(before "End Globals")
 map<string, type_tree*> Type_abbreviations, Type_abbreviations_snapshot;
@@ -69,33 +73,58 @@ string_tree* parse_string_list(const string& s) {
   return parse_property_list(in);
 }
 
-:(scenario type_error1)
-% Hide_errors = true;
-type foo
-+error: incomplete 'type' statement 'type foo'
+void test_type_error1() {
+  Hide_errors = true;
+  transform(
+      "type foo\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: incomplete 'type' statement 'type foo'\n"
+  );
+}
 
-:(scenario type_error2)
-% Hide_errors = true;
-type foo =
-+error: incomplete 'type' statement 'type foo ='
+void test_type_error2() {
+  Hide_errors = true;
+  transform(
+      "type foo =\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: incomplete 'type' statement 'type foo ='\n"
+  );
+}
 
-:(scenario type_error3)
-% Hide_errors = true;
-type foo bar baz
-+error: 'type' statements must be of the form 'type <new type name> = <type expression>' but got 'type foo bar'
+void test_type_error3() {
+  Hide_errors = true;
+  transform(
+      "type foo bar baz\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: 'type' statements must be of the form 'type <new type name> = <type expression>' but got 'type foo bar'\n"
+  );
+}
 
-:(scenario type_conflict_error)
-% Hide_errors = true;
-type foo = bar
-type foo = baz
-+error: 'type' conflict: 'foo' defined as both 'bar' and 'baz'
+void test_type_conflict_error() {
+  Hide_errors = true;
+  transform(
+      "type foo = bar\n"
+      "type foo = baz\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: 'type' conflict: 'foo' defined as both 'bar' and 'baz'\n"
+  );
+}
 
-:(scenario type_abbreviation_for_compound)
-type foo = address:number
-def main [
-  1:foo <- copy null
-]
-+transform: product type after expanding abbreviations: ("address" "number")
+void test_type_abbreviation_for_compound() {
+  transform(
+      "type foo = address:number\n"
+      "def main [\n"
+      "  1:foo <- copy null\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: product type after expanding abbreviations: (\"address\" \"number\")\n"
+  );
+}
 
 //: cleaning up type abbreviations between tests and before exiting
 
@@ -129,33 +158,44 @@ put(Type_abbreviations, "num", new_type_tree("number"));
 put(Type_abbreviations, "bool", new_type_tree("boolean"));
 put(Type_abbreviations, "char", new_type_tree("character"));
 
-:(scenario use_type_abbreviations_when_declaring_type_abbreviations)
-type foo = &:num
-def main [
-  1:foo <- copy null
-]
-+transform: product type after expanding abbreviations: ("address" "number")
+:(code)
+void test_use_type_abbreviations_when_declaring_type_abbreviations() {
+  transform(
+      "type foo = &:num\n"
+      "def main [\n"
+      "  1:foo <- copy null\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: product type after expanding abbreviations: (\"address\" \"number\")\n"
+  );
+}
 
 //:: Expand type aliases before running.
 //: We'll do this in a transform so that we don't need to define abbreviations
 //: before we use them.
 
-:(scenario abbreviations_for_address_and_array)
-def main [
-  f 1:&:num  # abbreviation for 'address:number'
-  f 2:@:num  # abbreviation for 'array:number'
-  f 3:&:@:num  # combining '&' and '@'
-  f 4:&:&:@:&:@:num  # ..any number of times
-  f {5: (array (& num) 3)}  # support for dilated reagents and more complex parse trees
-]
-def f [
-]
-+transform: --- expand type abbreviations in recipe 'main'
-+transform: ingredient type after expanding abbreviations: ("address" "number")
-+transform: ingredient type after expanding abbreviations: ("array" "number")
-+transform: ingredient type after expanding abbreviations: ("address" "array" "number")
-+transform: ingredient type after expanding abbreviations: ("address" "address" "array" "address" "array" "number")
-+transform: ingredient type after expanding abbreviations: ("array" ("address" "number") "3")
+void test_abbreviations_for_address_and_array() {
+  transform(
+      "def main [\n"
+      "  f 1:&:num\n"  // abbreviation for 'address:number'
+      "  f 2:@:num\n"  // abbreviation for 'array:number'
+      "  f 3:&:@:num\n"  // combining '&' and '@'
+      "  f 4:&:&:@:&:@:num\n"  // ..any number of times
+      "  f {5: (array (& num) 3)}\n"  // support for dilated reagents and more complex parse trees
+      "]\n"
+      "def f [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- expand type abbreviations in recipe 'main'\n"
+      "transform: ingredient type after expanding abbreviations: (\"address\" \"number\")\n"
+      "transform: ingredient type after expanding abbreviations: (\"array\" \"number\")\n"
+      "transform: ingredient type after expanding abbreviations: (\"address\" \"array\" \"number\")\n"
+      "transform: ingredient type after expanding abbreviations: (\"address\" \"address\" \"array\" \"address\" \"array\" \"number\")\n"
+      "transform: ingredient type after expanding abbreviations: (\"array\" (\"address\" \"number\") \"3\")\n"
+  );
+}
 
 :(before "Transform.push_back(update_instruction_operations)")
 Transform.push_back(expand_type_abbreviations);  // idempotent
diff --git a/020run.cc b/020run.cc
index 5d466bbc..aa4513e4 100644
--- a/020run.cc
+++ b/020run.cc
@@ -12,28 +12,43 @@
 //: does nothing, and COPY, which can copy numbers from one memory location to
 //: another. Later layers will add more primitives.
 
-:(scenario copy_literal)
-def main [
-  1:num <- copy 23
-]
-+run: {1: "number"} <- copy {23: "literal"}
-+mem: storing 23 in location 1
-
-:(scenario copy)
-def main [
-  1:num <- copy 23
-  2:num <- copy 1:num
-]
-+run: {2: "number"} <- copy {1: "number"}
-+mem: location 1 is 23
-+mem: storing 23 in location 2
-
-:(scenario copy_multiple)
-def main [
-  1:num, 2:num <- copy 23, 24
-]
-+mem: storing 23 in location 1
-+mem: storing 24 in location 2
+void test_copy_literal() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: {1: \"number\"} <- copy {23: \"literal\"}\n"
+      "mem: storing 23 in location 1\n"
+  );
+}
+
+void test_copy() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 23\n"
+      "  2:num <- copy 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: {2: \"number\"} <- copy {1: \"number\"}\n"
+      "mem: location 1 is 23\n"
+      "mem: storing 23 in location 2\n"
+  );
+}
+
+void test_copy_multiple() {
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- copy 23, 24\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 23 in location 1\n"
+      "mem: storing 24 in location 2\n"
+  );
+}
 
 :(before "End Types")
 // Book-keeping while running a recipe.
@@ -258,13 +273,13 @@ if (!Run_tests && contains_key(Recipe_ordinal, "main") && contains_key(Recipe, g
   assert(Num_calls_to_transform_all == 1);
   run_main(argc, argv);
 }
+
 :(code)
 void run_main(int argc, char* argv[]) {
   recipe_ordinal r = get(Recipe_ordinal, "main");
   if (r) run(r);
 }
 
-:(code)
 void load_file_or_directory(string filename) {
   if (is_directory(filename)) {
     load_all(filename);
@@ -272,7 +287,7 @@ void load_file_or_directory(string filename) {
   }
   ifstream fin(filename.c_str());
   if (!fin) {
-    cerr << "no such file '" << filename << "'\n" << end();  // don't raise, just warn. just in case it's just a name for a scenario to run.
+    cerr << "no such file '" << filename << "'\n" << end();  // don't raise, just warn. just in case it's just a name for a test to run.
     return;
   }
   trace(2, "load") << "=== " << filename << end();
@@ -421,60 +436,96 @@ void run(const string& form) {
     run(tmp.front());
 }
 
-:(scenario run_label)
-def main [
-  +foo
-  1:num <- copy 23
-  2:num <- copy 1:num
-]
-+run: {1: "number"} <- copy {23: "literal"}
-+run: {2: "number"} <- copy {1: "number"}
--run: +foo
-
-:(scenario run_dummy)
-def main [
-  _ <- copy 0
-]
-+run: _ <- copy {0: "literal"}
-
-:(scenario run_null)
-def main [
-  1:&:num <- copy null
-]
-
-:(scenario write_to_0_disallowed)
-% Hide_errors = true;
-def main [
-  0:num <- copy 34
-]
--mem: storing 34 in location 0
+void test_run_label() {
+  run(
+      "def main [\n"
+      "  +foo\n"
+      "  1:num <- copy 23\n"
+      "  2:num <- copy 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: {1: \"number\"} <- copy {23: \"literal\"}\n"
+      "run: {2: \"number\"} <- copy {1: \"number\"}\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: +foo");
+}
+
+void test_run_dummy() {
+  run(
+      "def main [\n"
+      "  _ <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: _ <- copy {0: \"literal\"}\n"
+  );
+}
+
+void test_run_null() {
+  run(
+      "def main [\n"
+      "  1:&:num <- copy null\n"
+      "]\n"
+  );
+}
+
+void test_write_to_0_disallowed() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  0:num <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 34 in location 0");
+}
 
 //: Mu is robust to various combinations of commas and spaces. You just have
 //: to put spaces around the '<-'.
 
-:(scenario comma_without_space)
-def main [
-  1:num, 2:num <- copy 2,2
-]
-+mem: storing 2 in location 1
-
-:(scenario space_without_comma)
-def main [
-  1:num, 2:num <- copy 2 2
-]
-+mem: storing 2 in location 1
-
-:(scenario comma_before_space)
-def main [
-  1:num, 2:num <- copy 2, 2
-]
-+mem: storing 2 in location 1
-
-:(scenario comma_after_space)
-def main [
-  1:num, 2:num <- copy 2 ,2
-]
-+mem: storing 2 in location 1
+void test_comma_without_space() {
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- copy 2,2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
+
+void test_space_without_comma() {
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- copy 2 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
+
+void test_comma_before_space() {
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- copy 2, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
+
+void test_comma_after_space() {
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- copy 2 ,2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
 
 //:: Counters for trying to understand where Mu programs are spending their
 //:: time.
diff --git a/021check_instruction.cc b/021check_instruction.cc
index 5bc7fa9c..5a8e1324 100644
--- a/021check_instruction.cc
+++ b/021check_instruction.cc
@@ -43,48 +43,76 @@ void check_instruction(const recipe_ordinal r) {
   }
 }
 
-:(scenario copy_checks_reagent_count)
-% Hide_errors = true;
-def main [
-  1:num, 2:num <- copy 34
-]
-+error: main: too many products in '1:num, 2:num <- copy 34'
-
-:(scenario write_scalar_to_array_disallowed)
-% Hide_errors = true;
-def main [
-  1:array:num <- copy 34
-]
-+error: main: can't copy '34' to '1:array:num'; types don't match
-
-:(scenario write_scalar_to_array_disallowed_2)
-% Hide_errors = true;
-def main [
-  1:num, 2:array:num <- copy 34, 35
-]
-+error: main: can't copy '35' to '2:array:num'; types don't match
-
-:(scenario write_scalar_to_address_disallowed)
-% Hide_errors = true;
-def main [
-  1:&:num <- copy 34
-]
-+error: main: can't copy '34' to '1:&:num'; types don't match
-
-:(scenario write_address_to_character_disallowed)
-% Hide_errors = true;
-def main [
-  1:&:num <- copy 12/unsafe
-  2:char <- copy 1:&:num
-]
-+error: main: can't copy '1:&:num' to '2:char'; types don't match
-
-:(scenario write_number_to_character_allowed)
-def main [
-  1:num <- copy 97
-  2:char <- copy 1:num
-]
-$error: 0
+void test_copy_checks_reagent_count() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too many products in '1:num, 2:num <- copy 34'\n"
+  );
+}
+
+void test_write_scalar_to_array_disallowed() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:array:num <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: can't copy '34' to '1:array:num'; types don't match\n"
+  );
+}
+
+void test_write_scalar_to_array_disallowed_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num, 2:array:num <- copy 34, 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: can't copy '35' to '2:array:num'; types don't match\n"
+  );
+}
+
+void test_write_scalar_to_address_disallowed() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:&:num <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: can't copy '34' to '1:&:num'; types don't match\n"
+  );
+}
+
+void test_write_address_to_character_disallowed() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:&:num <- copy 12/unsafe\n"
+      "  2:char <- copy 1:&:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: can't copy '1:&:num' to '2:char'; types don't match\n"
+  );
+}
+
+void test_write_number_to_character_allowed() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 97\n"
+      "  2:char <- copy 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 :(code)
 // types_match with some leniency
diff --git a/022arithmetic.cc b/022arithmetic.cc
index ce8cf1b9..05e79e3b 100644
--- a/022arithmetic.cc
+++ b/022arithmetic.cc
@@ -34,39 +34,65 @@ case ADD: {
   break;
 }
 
-:(scenario add_literal)
-def main [
-  1:num <- add 23, 34
-]
-+mem: storing 57 in location 1
-
-:(scenario add)
-def main [
-  1:num <- copy 23
-  2:num <- copy 34
-  3:num <- add 1:num, 2:num
-]
-+mem: storing 57 in location 3
-
-:(scenario add_multiple)
-def main [
-  1:num <- add 3, 4, 5
-]
-+mem: storing 12 in location 1
-
-:(scenario add_checks_type)
-% Hide_errors = true;
-def main [
-  1:num <- add 2:bool, 1
-]
-+error: main: 'add' requires number ingredients, but got '2:bool'
-
-:(scenario add_checks_return_type)
-% Hide_errors = true;
-def main [
-  1:&:num <- add 2, 2
-]
-+error: main: 'add' should yield a number, but got '1:&:num'
+:(code)
+void test_add_literal() {
+  run(
+      "def main [\n"
+      "  1:num <- add 23, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 57 in location 1\n"
+  );
+}
+
+void test_add() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 23\n"
+      "  2:num <- copy 34\n"
+      "  3:num <- add 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 57 in location 3\n"
+  );
+}
+
+void test_add_multiple() {
+  run(
+      "def main [\n"
+      "  1:num <- add 3, 4, 5\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 12 in location 1\n"
+  );
+}
+
+void test_add_checks_type() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- add 2:bool, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'add' requires number ingredients, but got '2:bool'\n"
+  );
+}
+
+void test_add_checks_return_type() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:&:num <- add 2, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'add' should yield a number, but got '1:&:num'\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 SUBTRACT,
@@ -104,25 +130,41 @@ case SUBTRACT: {
   break;
 }
 
-:(scenario subtract_literal)
-def main [
-  1:num <- subtract 5, 2
-]
-+mem: storing 3 in location 1
+:(code)
+void test_subtract_literal() {
+  run(
+      "def main [\n"
+      "  1:num <- subtract 5, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 1\n"
+  );
+}
 
-:(scenario subtract)
-def main [
-  1:num <- copy 23
-  2:num <- copy 34
-  3:num <- subtract 1:num, 2:num
-]
-+mem: storing -11 in location 3
+void test_subtract() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 23\n"
+      "  2:num <- copy 34\n"
+      "  3:num <- subtract 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing -11 in location 3\n"
+  );
+}
 
-:(scenario subtract_multiple)
-def main [
-  1:num <- subtract 6, 3, 2
-]
-+mem: storing 1 in location 1
+void test_subtract_multiple() {
+  run(
+      "def main [\n"
+      "  1:num <- subtract 6, 3, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 MULTIPLY,
@@ -157,25 +199,41 @@ case MULTIPLY: {
   break;
 }
 
-:(scenario multiply_literal)
-def main [
-  1:num <- multiply 2, 3
-]
-+mem: storing 6 in location 1
+:(code)
+void test_multiply_literal() {
+  run(
+      "def main [\n"
+      "  1:num <- multiply 2, 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 6 in location 1\n"
+  );
+}
 
-:(scenario multiply)
-def main [
-  1:num <- copy 4
-  2:num <- copy 6
-  3:num <- multiply 1:num, 2:num
-]
-+mem: storing 24 in location 3
+void test_multiply() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 4\n"
+      "  2:num <- copy 6\n"
+      "  3:num <- multiply 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 24 in location 3\n"
+  );
+}
 
-:(scenario multiply_multiple)
-def main [
-  1:num <- multiply 2, 3, 4
-]
-+mem: storing 24 in location 1
+void test_multiply_multiple() {
+  run(
+      "def main [\n"
+      "  1:num <- multiply 2, 3, 4\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 24 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 DIVIDE,
@@ -213,25 +271,41 @@ case DIVIDE: {
   break;
 }
 
-:(scenario divide_literal)
-def main [
-  1:num <- divide 8, 2
-]
-+mem: storing 4 in location 1
+:(code)
+void test_divide_literal() {
+  run(
+      "def main [\n"
+      "  1:num <- divide 8, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 1\n"
+  );
+}
 
-:(scenario divide)
-def main [
-  1:num <- copy 27
-  2:num <- copy 3
-  3:num <- divide 1:num, 2:num
-]
-+mem: storing 9 in location 3
+void test_divide() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 27\n"
+      "  2:num <- copy 3\n"
+      "  3:num <- divide 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 9 in location 3\n"
+  );
+}
 
-:(scenario divide_multiple)
-def main [
-  1:num <- divide 12, 3, 2
-]
-+mem: storing 2 in location 1
+void test_divide_multiple() {
+  run(
+      "def main [\n"
+      "  1:num <- divide 12, 3, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
 
 //: Integer division
 
@@ -281,41 +355,67 @@ case DIVIDE_WITH_REMAINDER: {
   break;
 }
 
-:(scenario divide_with_remainder_literal)
-def main [
-  1:num, 2:num <- divide-with-remainder 9, 2
-]
-+mem: storing 4 in location 1
-+mem: storing 1 in location 2
-
-:(scenario divide_with_remainder)
-def main [
-  1:num <- copy 27
-  2:num <- copy 11
-  3:num, 4:num <- divide-with-remainder 1:num, 2:num
-]
-+mem: storing 2 in location 3
-+mem: storing 5 in location 4
-
-:(scenario divide_with_decimal_point)
-def main [
-  1:num <- divide 5, 2
-]
-+mem: storing 2.5 in location 1
-
-:(scenario divide_by_zero)
-def main [
-  1:num <- divide 4, 0
-]
-+mem: storing inf in location 1
-
-:(scenario divide_by_zero_2)
-% Hide_errors = true;
-def main [
-  1:num <- divide-with-remainder 4, 0
-]
-# integer division can't return floating-point infinity
-+error: main: divide by zero in '1:num <- divide-with-remainder 4, 0'
+:(code)
+void test_divide_with_remainder_literal() {
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- divide-with-remainder 9, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 1\n"
+      "mem: storing 1 in location 2\n"
+  );
+}
+
+void test_divide_with_remainder() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 27\n"
+      "  2:num <- copy 11\n"
+      "  3:num, 4:num <- divide-with-remainder 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 3\n"
+      "mem: storing 5 in location 4\n"
+  );
+}
+
+void test_divide_with_decimal_point() {
+  run(
+      "def main [\n"
+      "  1:num <- divide 5, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2.5 in location 1\n"
+  );
+}
+
+void test_divide_by_zero() {
+  run(
+      "def main [\n"
+      "  1:num <- divide 4, 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing inf in location 1\n"
+  );
+}
+
+void test_divide_by_zero_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- divide-with-remainder 4, 0\n"
+      "]\n"
+  );
+  // integer division can't return floating-point infinity
+  CHECK_TRACE_CONTENTS(
+      "error: main: divide by zero in '1:num <- divide-with-remainder 4, 0'\n"
+  );
+}
 
 //: Bitwise shifts
 
@@ -358,37 +458,63 @@ case SHIFT_LEFT: {
   break;
 }
 
-:(scenario shift_left_by_zero)
-def main [
-  1:num <- shift-left 1, 0
-]
-+mem: storing 1 in location 1
-
-:(scenario shift_left_1)
-def main [
-  1:num <- shift-left 1, 4
-]
-+mem: storing 16 in location 1
-
-:(scenario shift_left_2)
-def main [
-  1:num <- shift-left 3, 2
-]
-+mem: storing 12 in location 1
-
-:(scenario shift_left_by_negative)
-% Hide_errors = true;
-def main [
-  1:num <- shift-left 3, -1
-]
-+error: main: second ingredient can't be negative in '1:num <- shift-left 3, -1'
-
-:(scenario shift_left_ignores_fractional_part)
-def main [
-  1:num <- divide 3, 2
-  2:num <- shift-left 1:num, 1
-]
-+mem: storing 2 in location 2
+:(code)
+void test_shift_left_by_zero() {
+  run(
+      "def main [\n"
+      "  1:num <- shift-left 1, 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
+
+void test_shift_left_1() {
+  run(
+      "def main [\n"
+      "  1:num <- shift-left 1, 4\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 16 in location 1\n"
+  );
+}
+
+void test_shift_left_2() {
+  run(
+      "def main [\n"
+      "  1:num <- shift-left 3, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 12 in location 1\n"
+  );
+}
+
+void test_shift_left_by_negative() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- shift-left 3, -1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: second ingredient can't be negative in '1:num <- shift-left 3, -1'\n"
+  );
+}
+
+void test_shift_left_ignores_fractional_part() {
+  run(
+      "def main [\n"
+      "  1:num <- divide 3, 2\n"
+      "  2:num <- shift-left 1:num, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 2\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 SHIFT_RIGHT,
@@ -429,37 +555,63 @@ case SHIFT_RIGHT: {
   break;
 }
 
-:(scenario shift_right_by_zero)
-def main [
-  1:num <- shift-right 1, 0
-]
-+mem: storing 1 in location 1
-
-:(scenario shift_right_1)
-def main [
-  1:num <- shift-right 1024, 1
-]
-+mem: storing 512 in location 1
-
-:(scenario shift_right_2)
-def main [
-  1:num <- shift-right 3, 1
-]
-+mem: storing 1 in location 1
-
-:(scenario shift_right_by_negative)
-% Hide_errors = true;
-def main [
-  1:num <- shift-right 4, -1
-]
-+error: main: second ingredient can't be negative in '1:num <- shift-right 4, -1'
-
-:(scenario shift_right_ignores_fractional_part)
-def main [
-  1:num <- divide 3, 2
-  2:num <- shift-right 1:num, 1
-]
-+mem: storing 0 in location 2
+:(code)
+void test_shift_right_by_zero() {
+  run(
+      "def main [\n"
+      "  1:num <- shift-right 1, 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
+
+void test_shift_right_1() {
+  run(
+      "def main [\n"
+      "  1:num <- shift-right 1024, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 512 in location 1\n"
+  );
+}
+
+void test_shift_right_2() {
+  run(
+      "def main [\n"
+      "  1:num <- shift-right 3, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
+
+void test_shift_right_by_negative() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- shift-right 4, -1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: second ingredient can't be negative in '1:num <- shift-right 4, -1'\n"
+  );
+}
+
+void test_shift_right_ignores_fractional_part() {
+  run(
+      "def main [\n"
+      "  1:num <- divide 3, 2\n"
+      "  2:num <- shift-right 1:num, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 2\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 AND_BITS,
@@ -495,29 +647,50 @@ case AND_BITS: {
   break;
 }
 
-:(scenario and_bits_1)
-def main [
-  1:num <- and-bits 8, 3
-]
-+mem: storing 0 in location 1
+:(code)
+void test_and_bits_1() {
+  run(
+      "def main [\n"
+      "  1:num <- and-bits 8, 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
-:(scenario and_bits_2)
-def main [
-  1:num <- and-bits 3, 2
-]
-+mem: storing 2 in location 1
+void test_and_bits_2() {
+  run(
+      "def main [\n"
+      "  1:num <- and-bits 3, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
 
-:(scenario and_bits_3)
-def main [
-  1:num <- and-bits 14, 3
-]
-+mem: storing 2 in location 1
+void test_and_bits_3() {
+  run(
+      "def main [\n"
+      "  1:num <- and-bits 14, 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
 
-:(scenario and_bits_negative)
-def main [
-  1:num <- and-bits -3, 4
-]
-+mem: storing 4 in location 1
+void test_and_bits_negative() {
+  run(
+      "def main [\n"
+      "  1:num <- and-bits -3, 4\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 OR_BITS,
@@ -553,23 +726,39 @@ case OR_BITS: {
   break;
 }
 
-:(scenario or_bits_1)
-def main [
-  1:num <- or-bits 3, 8
-]
-+mem: storing 11 in location 1
+:(code)
+void test_or_bits_1() {
+  run(
+      "def main [\n"
+      "  1:num <- or-bits 3, 8\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 11 in location 1\n"
+  );
+}
 
-:(scenario or_bits_2)
-def main [
-  1:num <- or-bits 3, 10
-]
-+mem: storing 11 in location 1
+void test_or_bits_2() {
+  run(
+      "def main [\n"
+      "  1:num <- or-bits 3, 10\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 11 in location 1\n"
+  );
+}
 
-:(scenario or_bits_3)
-def main [
-  1:num <- or-bits 4, 6
-]
-+mem: storing 6 in location 1
+void test_or_bits_3() {
+  run(
+      "def main [\n"
+      "  1:num <- or-bits 4, 6\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 6 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 XOR_BITS,
@@ -605,23 +794,39 @@ case XOR_BITS: {
   break;
 }
 
-:(scenario xor_bits_1)
-def main [
-  1:num <- xor-bits 3, 8
-]
-+mem: storing 11 in location 1
+:(code)
+void test_xor_bits_1() {
+  run(
+      "def main [\n"
+      "  1:num <- xor-bits 3, 8\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 11 in location 1\n"
+  );
+}
 
-:(scenario xor_bits_2)
-def main [
-  1:num <- xor-bits 3, 10
-]
-+mem: storing 9 in location 1
+void test_xor_bits_2() {
+  run(
+      "def main [\n"
+      "  1:num <- xor-bits 3, 10\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 9 in location 1\n"
+  );
+}
 
-:(scenario xor_bits_3)
-def main [
-  1:num <- xor-bits 4, 6
-]
-+mem: storing 2 in location 1
+void test_xor_bits_3() {
+  run(
+      "def main [\n"
+      "  1:num <- xor-bits 4, 6\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 FLIP_BITS,
@@ -656,29 +861,50 @@ case FLIP_BITS: {
   break;
 }
 
-:(scenario flip_bits_zero)
-def main [
-  1:num <- flip-bits 0
-]
-+mem: storing -1 in location 1
+:(code)
+void test_flip_bits_zero() {
+  run(
+      "def main [\n"
+      "  1:num <- flip-bits 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing -1 in location 1\n"
+  );
+}
 
-:(scenario flip_bits_negative)
-def main [
-  1:num <- flip-bits -1
-]
-+mem: storing 0 in location 1
+void test_flip_bits_negative() {
+  run(
+      "def main [\n"
+      "  1:num <- flip-bits -1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
-:(scenario flip_bits_1)
-def main [
-  1:num <- flip-bits 3
-]
-+mem: storing -4 in location 1
+void test_flip_bits_1() {
+  run(
+      "def main [\n"
+      "  1:num <- flip-bits 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing -4 in location 1\n"
+  );
+}
 
-:(scenario flip_bits_2)
-def main [
-  1:num <- flip-bits 12
-]
-+mem: storing -13 in location 1
+void test_flip_bits_2() {
+  run(
+      "def main [\n"
+      "  1:num <- flip-bits 12\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing -13 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 ROUND,
@@ -703,23 +929,39 @@ case ROUND: {
   break;
 }
 
-:(scenario round_to_nearest_integer)
-def main [
-  1:num <- round 12.2
-]
-+mem: storing 12 in location 1
+:(code)
+void test_round_to_nearest_integer() {
+  run(
+      "def main [\n"
+      "  1:num <- round 12.2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 12 in location 1\n"
+  );
+}
 
-:(scenario round_halves_toward_zero)
-def main [
-  1:num <- round 12.5
-]
-+mem: storing 12 in location 1
+void test_round_halves_toward_zero() {
+  run(
+      "def main [\n"
+      "  1:num <- round 12.5\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 12 in location 1\n"
+  );
+}
 
-:(scenario round_halves_toward_zero_2)
-def main [
-  1:num <- round -12.5
-]
-+mem: storing -12 in location 1
+void test_round_halves_toward_zero_2() {
+  run(
+      "def main [\n"
+      "  1:num <- round -12.5\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing -12 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 TRUNCATE,
@@ -744,17 +986,28 @@ case TRUNCATE: {
   break;
 }
 
-:(scenario truncate_to_nearest_integer)
-def main [
-  1:num <- truncate 12.2
-]
-+mem: storing 12 in location 1
+:(code)
+void test_truncate_to_nearest_integer() {
+  run(
+      "def main [\n"
+      "  1:num <- truncate 12.2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 12 in location 1\n"
+  );
+}
 
-:(scenario truncate_negative)
-def main [
-  1:num <- truncate -12.2
-]
-+mem: storing -12 in location 1
+void test_truncate_negative() {
+  run(
+      "def main [\n"
+      "  1:num <- truncate -12.2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing -12 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 SQUARE_ROOT,
diff --git a/023boolean.cc b/023boolean.cc
index 6d13325f..c6c5cac7 100644
--- a/023boolean.cc
+++ b/023boolean.cc
@@ -38,31 +38,51 @@ double scalar_ingredient(const vector<vector<double> >& ingredients, int i) {
   return ingredients.at(i).at(0);
 }
 
-:(scenario and)
-def main [
-  1:bool <- copy true
-  2:bool <- copy false
-  3:bool <- and 1:bool, 2:bool
-]
-+mem: storing 0 in location 3
+void test_and() {
+  run(
+      "def main [\n"
+      "  1:bool <- copy true\n"
+      "  2:bool <- copy false\n"
+      "  3:bool <- and 1:bool, 2:bool\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 3\n"
+  );
+}
 
-:(scenario and_2)
-def main [
-  1:bool <- and true, true
-]
-+mem: storing 1 in location 1
+void test_and_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- and true, true\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
-:(scenario and_multiple)
-def main [
-  1:bool <- and true, true, false
-]
-+mem: storing 0 in location 1
+void test_and_multiple() {
+  run(
+      "def main [\n"
+      "  1:bool <- and true, true, false\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
-:(scenario and_multiple_2)
-def main [
-  1:bool <- and true, true, true
-]
-+mem: storing 1 in location 1
+void test_and_multiple_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- and true, true, true\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 OR,
@@ -96,31 +116,52 @@ case OR: {
   break;
 }
 
-:(scenario or)
-def main [
-  1:bool <- copy true
-  2:bool <- copy false
-  3:bool <- or 1:bool, 2:bool
-]
-+mem: storing 1 in location 3
+:(code)
+void test_or() {
+  run(
+      "def main [\n"
+      "  1:bool <- copy true\n"
+      "  2:bool <- copy false\n"
+      "  3:bool <- or 1:bool, 2:bool\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario or_2)
-def main [
-  1:bool <- or false, false
-]
-+mem: storing 0 in location 1
+void test_or_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- or false, false\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
-:(scenario or_multiple)
-def main [
-  1:bool <- or false, false, false
-]
-+mem: storing 0 in location 1
+void test_or_multiple() {
+  run(
+      "def main [\n"
+      "  1:bool <- or false, false, false\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
-:(scenario or_multiple_2)
-def main [
-  1:bool <- or false, false, true
-]
-+mem: storing 1 in location 1
+void test_or_multiple_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- or false, false, true\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 NOT,
@@ -156,17 +197,28 @@ case NOT: {
   break;
 }
 
-:(scenario not)
-def main [
-  1:bool <- copy true
-  2:bool <- not 1:bool
-]
-+mem: storing 0 in location 2
+:(code)
+void test_not() {
+  run(
+      "def main [\n"
+      "  1:bool <- copy true\n"
+      "  2:bool <- not 1:bool\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 2\n"
+  );
+}
 
-:(scenario not_multiple)
-def main [
-  1:bool, 2:bool, 3:bool <- not true, false, true
-]
-+mem: storing 0 in location 1
-+mem: storing 1 in location 2
-+mem: storing 0 in location 3
+void test_not_multiple() {
+  run(
+      "def main [\n"
+      "  1:bool, 2:bool, 3:bool <- not true, false, true\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+      "mem: storing 1 in location 2\n"
+      "mem: storing 0 in location 3\n"
+  );
+}
diff --git a/024jump.cc b/024jump.cc
index 5b95fd19..66e238e6 100644
--- a/024jump.cc
+++ b/024jump.cc
@@ -1,13 +1,18 @@
 //: Jump primitives
 
-:(scenario jump_can_skip_instructions)
-def main [
-  jump 1:offset
-  1:num <- copy 1
-]
-+run: jump {1: "offset"}
--run: {1: "number"} <- copy {1: "literal"}
--mem: storing 1 in location 1
+void test_jump_can_skip_instructions() {
+  run(
+      "def main [\n"
+      "  jump 1:offset\n"
+      "  1:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: jump {1: \"offset\"}\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: {1: \"number\"} <- copy {1: \"literal\"}");
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 1 in location 1");
+}
 
 :(before "End Primitive Recipe Declarations")
 JUMP,
@@ -44,23 +49,34 @@ case JUMP: {
 :(before "End Mu Types Initialization")
 put(Type_ordinal, "offset", 0);
 
-:(scenario jump_backward)
-def main [
-  jump 1:offset  # 0 -+
-  jump 3:offset  #    |   +-+ 1
-                 #   \/  /\ |
-  jump -2:offset #  2 +-->+ |
-]                #         \/ 3
-+run: jump {1: "offset"}
-+run: jump {-2: "offset"}
-+run: jump {3: "offset"}
+:(code)
+void test_jump_backward() {
+  run(
+      "def main [\n"
+      "  jump 1:offset\n"  // 0 -+
+      "  jump 3:offset\n"  //    |   +-+ 1
+                           //   \/  /\ |
+      "  jump -2:offset\n" //  2 +-->+ |
+      "]\n"                //         \/ 3
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: jump {1: \"offset\"}\n"
+      "run: jump {-2: \"offset\"}\n"
+      "run: jump {3: \"offset\"}\n"
+  );
+}
 
-:(scenario jump_takes_no_products)
-% Hide_errors = true;
-def main [
-  1:num <- jump 1
-]
-+error: main: 'jump' instructions write no products
+void test_jump_takes_no_products() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- jump 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'jump' instructions write no products\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 JUMP_IF,
@@ -102,36 +118,52 @@ case JUMP_IF: {
   break;
 }
 
-:(scenario jump_if)
-def main [
-  jump-if 999, 1:offset
-  123:num <- copy 1
-]
-+run: jump-if {999: "literal"}, {1: "offset"}
-+run: jumping to instruction 2
--run: {123: "number"} <- copy {1: "literal"}
--mem: storing 1 in location 123
+:(code)
+void test_jump_if() {
+  run(
+      "def main [\n"
+      "  jump-if 999, 1:offset\n"
+      "  123:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: jump-if {999: \"literal\"}, {1: \"offset\"}\n"
+      "run: jumping to instruction 2\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: {123: \"number\"} <- copy {1: \"literal\"}");
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 1 in location 123");
+}
 
-:(scenario jump_if_fallthrough)
-def main [
-  jump-if 0, 1:offset
-  123:num <- copy 1
-]
-+run: jump-if {0: "literal"}, {1: "offset"}
-+run: jump-if fell through
-+run: {123: "number"} <- copy {1: "literal"}
-+mem: storing 1 in location 123
+void test_jump_if_fallthrough() {
+  run(
+      "def main [\n"
+      "  jump-if 0, 1:offset\n"
+      "  123:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: jump-if {0: \"literal\"}, {1: \"offset\"}\n"
+      "run: jump-if fell through\n"
+      "run: {123: \"number\"} <- copy {1: \"literal\"}\n"
+      "mem: storing 1 in location 123\n"
+  );
+}
 
-:(scenario jump_if_on_address)
-def main [
-  10:num/alloc-id, 11:num <- copy 0, 999
-  jump-if 10:&:number, 1:offset
-  123:num <- copy 1
-]
-+run: jump-if {10: ("address" "number")}, {1: "offset"}
-+run: jumping to instruction 3
--run: {123: "number"} <- copy {1: "literal"}
--mem: storing 1 in location 123
+void test_jump_if_on_address() {
+  run(
+      "def main [\n"
+      "  10:num/alloc-id, 11:num <- copy 0, 999\n"
+      "  jump-if 10:&:number, 1:offset\n"
+      "  123:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: jump-if {10: (\"address\" \"number\")}, {1: \"offset\"}\n"
+      "run: jumping to instruction 3\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: {123: \"number\"} <- copy {1: \"literal\"}");
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 1 in location 123");
+}
 
 :(before "End Primitive Recipe Declarations")
 JUMP_UNLESS,
@@ -173,22 +205,33 @@ case JUMP_UNLESS: {
   break;
 }
 
-:(scenario jump_unless)
-def main [
-  jump-unless 0, 1:offset
-  123:num <- copy 1
-]
-+run: jump-unless {0: "literal"}, {1: "offset"}
-+run: jumping to instruction 2
--run: {123: "number"} <- copy {1: "literal"}
--mem: storing 1 in location 123
+:(code)
+void test_jump_unless() {
+  run(
+      "def main [\n"
+      "  jump-unless 0, 1:offset\n"
+      "  123:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: jump-unless {0: \"literal\"}, {1: \"offset\"}\n"
+      "run: jumping to instruction 2\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: {123: \"number\"} <- copy {1: \"literal\"}");
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 1 in location 123");
+}
 
-:(scenario jump_unless_fallthrough)
-def main [
-  jump-unless 999, 1:offset
-  123:num <- copy 1
-]
-+run: jump-unless {999: "literal"}, {1: "offset"}
-+run: jump-unless fell through
-+run: {123: "number"} <- copy {1: "literal"}
-+mem: storing 1 in location 123
+void test_jump_unless_fallthrough() {
+  run(
+      "def main [\n"
+      "  jump-unless 999, 1:offset\n"
+      "  123:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: jump-unless {999: \"literal\"}, {1: \"offset\"}\n"
+      "run: jump-unless fell through\n"
+      "run: {123: \"number\"} <- copy {1: \"literal\"}\n"
+      "mem: storing 1 in location 123\n"
+  );
+}
diff --git a/025compare.cc b/025compare.cc
index e12fa1dc..bade3c9c 100644
--- a/025compare.cc
+++ b/025compare.cc
@@ -46,37 +46,58 @@ case EQUAL: {
   break;
 }
 
-:(scenario equal)
-def main [
-  1:num <- copy 34
-  2:num <- copy 33
-  3:bool <- equal 1:num, 2:num
-]
-+mem: location 1 is 34
-+mem: location 2 is 33
-+mem: storing 0 in location 3
+:(code)
+void test_equal() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: location 1 is 34\n"
+      "mem: location 2 is 33\n"
+      "mem: storing 0 in location 3\n"
+  );
+}
 
-:(scenario equal_2)
-def main [
-  1:num <- copy 34
-  2:num <- copy 34
-  3:bool <- equal 1:num, 2:num
-]
-+mem: location 1 is 34
-+mem: location 2 is 34
-+mem: storing 1 in location 3
+void test_equal_2() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 34\n"
+      "  3:bool <- equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: location 1 is 34\n"
+      "mem: location 2 is 34\n"
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario equal_multiple)
-def main [
-  1:bool <- equal 34, 34, 34
-]
-+mem: storing 1 in location 1
+void test_equal_multiple() {
+  run(
+      "def main [\n"
+      "  1:bool <- equal 34, 34, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
-:(scenario equal_multiple_2)
-def main [
-  1:bool <- equal 34, 34, 35
-]
-+mem: storing 0 in location 1
+void test_equal_multiple_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- equal 34, 34, 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 NOT_EQUAL,
@@ -116,25 +137,36 @@ case NOT_EQUAL: {
   break;
 }
 
-:(scenario not_equal)
-def main [
-  1:num <- copy 34
-  2:num <- copy 33
-  3:bool <- not-equal 1:num, 2:num
-]
-+mem: location 1 is 34
-+mem: location 2 is 33
-+mem: storing 1 in location 3
+:(code)
+void test_not_equal() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- not-equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: location 1 is 34\n"
+      "mem: location 2 is 33\n"
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario not_equal_2)
-def main [
-  1:num <- copy 34
-  2:num <- copy 34
-  3:bool <- not-equal 1:num, 2:num
-]
-+mem: location 1 is 34
-+mem: location 2 is 34
-+mem: storing 0 in location 3
+void test_not_equal_2() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 34\n"
+      "  3:bool <- not-equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: location 1 is 34\n"
+      "mem: location 2 is 34\n"
+      "mem: storing 0 in location 3\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 GREATER_THAN,
@@ -175,33 +207,54 @@ case GREATER_THAN: {
   break;
 }
 
-:(scenario greater_than)
-def main [
-  1:num <- copy 34
-  2:num <- copy 33
-  3:bool <- greater-than 1:num, 2:num
-]
-+mem: storing 1 in location 3
+:(code)
+void test_greater_than() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- greater-than 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario greater_than_2)
-def main [
-  1:num <- copy 34
-  2:num <- copy 34
-  3:bool <- greater-than 1:num, 2:num
-]
-+mem: storing 0 in location 3
+void test_greater_than_2() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 34\n"
+      "  3:bool <- greater-than 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 3\n"
+  );
+}
 
-:(scenario greater_than_multiple)
-def main [
-  1:bool <- greater-than 36, 35, 34
-]
-+mem: storing 1 in location 1
+void test_greater_than_multiple() {
+  run(
+      "def main [\n"
+      "  1:bool <- greater-than 36, 35, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
-:(scenario greater_than_multiple_2)
-def main [
-  1:bool <- greater-than 36, 35, 35
-]
-+mem: storing 0 in location 1
+void test_greater_than_multiple_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- greater-than 36, 35, 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 LESSER_THAN,
@@ -242,34 +295,54 @@ case LESSER_THAN: {
   break;
 }
 
-:(scenario lesser_than)
-def main [
-  1:num <- copy 32
-  2:num <- copy 33
-  3:bool <- lesser-than 1:num, 2:num
-]
-+mem: storing 1 in location 3
-
-:(scenario lesser_than_2)
-def main [
-  1:num <- copy 34
-  2:num <- copy 33
-  3:bool <- lesser-than 1:num, 2:num
-]
-+mem: storing 0 in location 3
+:(code)
+void test_lesser_than() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 32\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- lesser-than 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario lesser_than_multiple)
-def main [
-  1:bool <- lesser-than 34, 35, 36
-]
-+mem: storing 1 in location 1
+void test_lesser_than_2() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- lesser-than 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 3\n"
+  );
+}
 
-:(scenario lesser_than_multiple_2)
-def main [
-  1:bool <- lesser-than 34, 35, 35
-]
-+mem: storing 0 in location 1
+void test_lesser_than_multiple() {
+  run(
+      "def main [\n"
+      "  1:bool <- lesser-than 34, 35, 36\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
+void test_lesser_than_multiple_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- lesser-than 34, 35, 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 :(before "End Primitive Recipe Declarations")
 GREATER_OR_EQUAL,
 :(before "End Primitive Recipe Numbers")
@@ -309,41 +382,67 @@ case GREATER_OR_EQUAL: {
   break;
 }
 
-:(scenario greater_or_equal)
-def main [
-  1:num <- copy 34
-  2:num <- copy 33
-  3:bool <- greater-or-equal 1:num, 2:num
-]
-+mem: storing 1 in location 3
+:(code)
+void test_greater_or_equal() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- greater-or-equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario greater_or_equal_2)
-def main [
-  1:num <- copy 34
-  2:num <- copy 34
-  3:bool <- greater-or-equal 1:num, 2:num
-]
-+mem: storing 1 in location 3
+void test_greater_or_equal_2() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 34\n"
+      "  3:bool <- greater-or-equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario greater_or_equal_3)
-def main [
-  1:num <- copy 34
-  2:num <- copy 35
-  3:bool <- greater-or-equal 1:num, 2:num
-]
-+mem: storing 0 in location 3
+void test_greater_or_equal_3() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 35\n"
+      "  3:bool <- greater-or-equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 3\n"
+  );
+}
 
-:(scenario greater_or_equal_multiple)
-def main [
-  1:bool <- greater-or-equal 36, 35, 35
-]
-+mem: storing 1 in location 1
+void test_greater_or_equal_multiple() {
+  run(
+      "def main [\n"
+      "  1:bool <- greater-or-equal 36, 35, 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
-:(scenario greater_or_equal_multiple_2)
-def main [
-  1:bool <- greater-or-equal 36, 35, 36
-]
-+mem: storing 0 in location 1
+void test_greater_or_equal_multiple_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- greater-or-equal 36, 35, 36\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 LESSER_OR_EQUAL,
@@ -384,41 +483,67 @@ case LESSER_OR_EQUAL: {
   break;
 }
 
-:(scenario lesser_or_equal)
-def main [
-  1:num <- copy 32
-  2:num <- copy 33
-  3:bool <- lesser-or-equal 1:num, 2:num
-]
-+mem: storing 1 in location 3
+:(code)
+void test_lesser_or_equal() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 32\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- lesser-or-equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario lesser_or_equal_2)
-def main [
-  1:num <- copy 33
-  2:num <- copy 33
-  3:bool <- lesser-or-equal 1:num, 2:num
-]
-+mem: storing 1 in location 3
+void test_lesser_or_equal_2() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 33\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- lesser-or-equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario lesser_or_equal_3)
-def main [
-  1:num <- copy 34
-  2:num <- copy 33
-  3:bool <- lesser-or-equal 1:num, 2:num
-]
-+mem: storing 0 in location 3
+void test_lesser_or_equal_3() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 33\n"
+      "  3:bool <- lesser-or-equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 3\n"
+  );
+}
 
-:(scenario lesser_or_equal_multiple)
-def main [
-  1:bool <- lesser-or-equal 34, 35, 35
-]
-+mem: storing 1 in location 1
+void test_lesser_or_equal_multiple() {
+  run(
+      "def main [\n"
+      "  1:bool <- lesser-or-equal 34, 35, 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+  );
+}
 
-:(scenario lesser_or_equal_multiple_2)
-def main [
-  1:bool <- lesser-or-equal 34, 35, 34
-]
-+mem: storing 0 in location 1
+void test_lesser_or_equal_multiple_2() {
+  run(
+      "def main [\n"
+      "  1:bool <- lesser-or-equal 34, 35, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 MAX,
diff --git a/026call.cc b/026call.cc
index 1f130ef5..faa9455d 100644
--- a/026call.cc
+++ b/026call.cc
@@ -1,33 +1,41 @@
 //: So far the recipes we define can't run each other. Let's fix that.
 
-:(scenario calling_recipe)
-def main [
-  f
-]
-def f [
-  3:num <- add 2, 2
-]
-+mem: storing 4 in location 3
-
-:(scenario return_on_fallthrough)
-def main [
-  f
-  1:num <- copy 0
-  2:num <- copy 0
-  3:num <- copy 0
-]
-def f [
-  4:num <- copy 0
-  5:num <- copy 0
-]
-+run: f
-# running f
-+run: {4: "number"} <- copy {0: "literal"}
-+run: {5: "number"} <- copy {0: "literal"}
-# back out to main
-+run: {1: "number"} <- copy {0: "literal"}
-+run: {2: "number"} <- copy {0: "literal"}
-+run: {3: "number"} <- copy {0: "literal"}
+void test_calling_recipe() {
+  run(
+      "def main [\n"
+      "  f\n"
+      "]\n"
+      "def f [\n"
+      "  3:num <- add 2, 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 3\n"
+  );
+}
+
+void test_return_on_fallthrough() {
+  run(
+      "def main [\n"
+      "  f\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "  3:num <- copy 0\n"
+      "]\n"
+      "def f [\n"
+      "  4:num <- copy 0\n"
+      "  5:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: f\n"
+      "run: {4: \"number\"} <- copy {0: \"literal\"}\n"
+      "run: {5: \"number\"} <- copy {0: \"literal\"}\n"
+      "run: {1: \"number\"} <- copy {0: \"literal\"}\n"
+      "run: {2: \"number\"} <- copy {0: \"literal\"}\n"
+      "run: {3: \"number\"} <- copy {0: \"literal\"}\n"
+  );
+}
 
 :(before "struct routine {")
 // Everytime a recipe runs another, we interrupt it and start running the new
@@ -166,19 +174,29 @@ void finish_call_housekeeping(const instruction& call_instruction, const vector<
   // End Call Housekeeping
 }
 
-:(scenario calling_undefined_recipe_fails)
-% Hide_errors = true;
-def main [
-  foo
-]
-+error: main: undefined operation in 'foo'
-
-:(scenario calling_undefined_recipe_handles_missing_result)
-% Hide_errors = true;
-def main [
-  x:num <- foo
-]
-+error: main: undefined operation in 'x:num <- foo'
+void test_calling_undefined_recipe_fails() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  foo\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: undefined operation in 'foo'\n"
+  );
+}
+
+void test_calling_undefined_recipe_handles_missing_result() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  x:num <- foo\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: undefined operation in 'x:num <- foo'\n"
+  );
+}
 
 //:: finally, we need to fix the termination conditions for the run loop
 
diff --git a/027call_ingredient.cc b/027call_ingredient.cc
index df7bba3e..659f644d 100644
--- a/027call_ingredient.cc
+++ b/027call_ingredient.cc
@@ -1,24 +1,34 @@
 //: Calls can take ingredients just like primitives. To access a recipe's
 //: ingredients, use 'next-ingredient'.
 
-:(scenario next_ingredient)
-def main [
-  f 2
-]
-def f [
-  12:num <- next-ingredient
-  13:num <- add 1, 12:num
-]
-+mem: storing 3 in location 13
+void test_next_ingredient() {
+  run(
+      "def main [\n"
+      "  f 2\n"
+      "]\n"
+      "def f [\n"
+      "  12:num <- next-ingredient\n"
+      "  13:num <- add 1, 12:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 13\n"
+  );
+}
 
-:(scenario next_ingredient_missing)
-def main [
-  f
-]
-def f [
-  _, 12:num <- next-ingredient
-]
-+mem: storing 0 in location 12
+void test_next_ingredient_missing() {
+  run(
+      "def main [\n"
+      "  f\n"
+      "]\n"
+      "def f [\n"
+      "  _, 12:num <- next-ingredient\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 12\n"
+  );
+}
 
 :(before "End call Fields")
 vector<vector<double> > ingredient_atoms;
@@ -84,30 +94,41 @@ case NEXT_INGREDIENT: {
   break;
 }
 
-:(scenario next_ingredient_fail_on_missing)
-% Hide_errors = true;
-def main [
-  f
-]
-def f [
-  11:num <- next-ingredient
-]
-+error: f: no ingredient to save in '11:num'
+:(code)
+void test_next_ingredient_fail_on_missing() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  f\n"
+      "]\n"
+      "def f [\n"
+      "  11:num <- next-ingredient\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: f: no ingredient to save in '11:num'\n"
+  );
+}
 
-:(scenario rewind_ingredients)
-def main [
-  f 2
-]
-def f [
-  12:num <- next-ingredient  # consume ingredient
-  _, 1:bool <- next-ingredient  # will not find any ingredients
-  rewind-ingredients
-  13:num, 2:bool <- next-ingredient  # will find ingredient again
-]
-+mem: storing 2 in location 12
-+mem: storing 0 in location 1
-+mem: storing 2 in location 13
-+mem: storing 1 in location 2
+void test_rewind_ingredients() {
+  run(
+      "def main [\n"
+      "  f 2\n"
+      "]\n"
+      "def f [\n"
+      "  12:num <- next-ingredient\n"  // consume ingredient
+      "  _, 1:bool <- next-ingredient\n"  // will not find any ingredients
+      "  rewind-ingredients\n"
+      "  13:num, 2:bool <- next-ingredient\n"  // will find ingredient again
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 12\n"
+      "mem: storing 0 in location 1\n"
+      "mem: storing 2 in location 13\n"
+      "mem: storing 1 in location 2\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 REWIND_INGREDIENTS,
@@ -126,16 +147,22 @@ case REWIND_INGREDIENTS: {
 
 //: another primitive: 'ingredient' for random access
 
-:(scenario ingredient)
-def main [
-  f 1, 2
-]
-def f [
-  12:num <- ingredient 1  # consume second ingredient first
-  13:num, 1:bool <- next-ingredient  # next-ingredient tries to scan past that
-]
-+mem: storing 2 in location 12
-+mem: storing 0 in location 1
+:(code)
+void test_ingredient() {
+  run(
+      "def main [\n"
+      "  f 1, 2\n"
+      "]\n"
+      "def f [\n"
+      "  12:num <- ingredient 1\n"  // consume second ingredient first
+      "  13:num, 1:bool <- next-ingredient\n"  // next-ingredient tries to scan past that
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 12\n"
+      "mem: storing 0 in location 1\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 INGREDIENT,
diff --git a/028call_return.cc b/028call_return.cc
index af158884..056db7b9 100644
--- a/028call_return.cc
+++ b/028call_return.cc
@@ -1,28 +1,38 @@
 //: Calls can also generate products, using 'reply' or 'return'.
 
-:(scenario return)
-def main [
-  1:num, 2:num <- f 34
-]
-def f [
-  12:num <- next-ingredient
-  13:num <- add 1, 12:num
-  return 12:num, 13:num
-]
-+mem: storing 34 in location 1
-+mem: storing 35 in location 2
+void test_return() {
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- f 34\n"
+      "]\n"
+      "def f [\n"
+      "  12:num <- next-ingredient\n"
+      "  13:num <- add 1, 12:num\n"
+      "  return 12:num, 13:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+      "mem: storing 35 in location 2\n"
+  );
+}
 
-:(scenario reply)
-def main [
-  1:num, 2:num <- f 34
-]
-def f [
-  12:num <- next-ingredient
-  13:num <- add 1, 12:num
-  reply 12:num, 13:num
-]
-+mem: storing 34 in location 1
-+mem: storing 35 in location 2
+void test_reply() {
+  run(
+      "def main [\n"
+      "  1:num, 2:num <- f 34\n"
+      "]\n"
+      "def f [\n"
+      "  12:num <- next-ingredient\n"
+      "  13:num <- add 1, 12:num\n"
+      "  reply 12:num, 13:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+      "mem: storing 35 in location 2\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 RETURN,
@@ -114,18 +124,23 @@ bool is_primitive(recipe_ordinal r) {
   return r < MAX_PRIMITIVE_RECIPES;
 }
 
-:(scenario return_type_mismatch)
-% Hide_errors = true;
-def main [
-  3:num <- f 2
-]
-def f [
-  12:num <- next-ingredient
-  13:num <- copy 35
-  14:point <- copy 12:point/raw
-  return 14:point
-]
-+error: f: return ingredient '14:point' can't be saved in '3:num'
+void test_return_type_mismatch() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  3:num <- f 2\n"
+      "]\n"
+      "def f [\n"
+      "  12:num <- next-ingredient\n"
+      "  13:num <- copy 35\n"
+      "  14:point <- copy 12:point/raw\n"
+      "  return 14:point\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: f: return ingredient '14:point' can't be saved in '3:num'\n"
+  );
+}
 
 //: In Mu we'd like to assume that any instruction doesn't modify its
 //: ingredients unless they're also products. The /same-as-ingredient inside
@@ -134,30 +149,37 @@ def f [
 //: 'ingredient-products' (sometimes called in-out parameters in other
 //: languages).
 
-:(scenario return_same_as_ingredient)
-% Hide_errors = true;
-def main [
-  1:num <- copy 0
-  2:num <- test1 1:num  # call with different ingredient and product
-]
-def test1 [
-  10:num <- next-ingredient
-  return 10:num/same-as-ingredient:0
-]
-+error: main: '2:num <- test1 1:num' should write to '1:num' rather than '2:num'
+void test_return_same_as_ingredient() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- test1 1:num  # call with different ingredient and product\n"
+      "]\n"
+      "def test1 [\n"
+      "  10:num <- next-ingredient\n"
+      "  return 10:num/same-as-ingredient:0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: '2:num <- test1 1:num' should write to '1:num' rather than '2:num'\n"
+  );
+}
 
-:(scenario return_same_as_ingredient_dummy)
-def main [
-  1:num <- copy 0
-  _ <- test1 1:num  # call with different ingredient and product
-]
-def test1 [
-  10:num <- next-ingredient
-  return 10:num/same-as-ingredient:0
-]
-$error: 0
+void test_return_same_as_ingredient_dummy() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  _ <- test1 1:num  # call with different ingredient and product\n"
+      "]\n"
+      "def test1 [\n"
+      "  10:num <- next-ingredient\n"
+      "  return 10:num/same-as-ingredient:0\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(code)
 string to_string(const vector<double>& in) {
   if (in.empty()) return "[]";
   ostringstream out;
diff --git a/029tools.cc b/029tools.cc
index 7c4a9a68..4cca09f6 100644
--- a/029tools.cc
+++ b/029tools.cc
@@ -1,10 +1,15 @@
 //: Allow Mu programs to log facts just like we've been doing in C++ so far.
 
-:(scenario trace)
-def main [
-  trace 1, [foo], [this is a trace in Mu]
-]
-+foo: this is a trace in Mu
+void test_trace() {
+  run(
+      "def main [\n"
+      "  trace 1, [foo], [this is a trace in Mu]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "foo: this is a trace in Mu\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 TRACE,
@@ -63,24 +68,40 @@ case STASH: {
   break;
 }
 
-:(scenario stash_literal_string)
-def main [
-  stash [foo]
-]
-+app: foo
+:(code)
+void test_stash_literal_string() {
+  run(
+      "def main [\n"
+      "  stash [foo]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo\n"
+  );
+}
 
-:(scenario stash_literal_number)
-def main [
-  stash [foo:], 4
-]
-+app: foo: 4
+void test_stash_literal_number() {
+  run(
+      "def main [\n"
+      "  stash [foo:], 4\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo: 4\n"
+  );
+}
 
-:(scenario stash_number)
-def main [
-  1:num <- copy 34
-  stash [foo:], 1:num
-]
-+app: foo: 34
+void test_stash_number() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  stash [foo:], 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo: 34\n"
+  );
+}
 
 :(code)
 string inspect(const reagent& r, const vector<double>& data) {
diff --git a/030container.cc b/030container.cc
index ca2d6743..e20af025 100644
--- a/030container.cc
+++ b/030container.cc
@@ -15,22 +15,34 @@ get(Type, point).elements.push_back(reagent("y:number"));
 //: Tests in this layer often explicitly set up memory before reading it as a
 //: container. Don't do this in general. I'm tagging such cases with /unsafe;
 //: they'll be exceptions to later checks.
-:(scenario copy_multiple_locations)
-def main [
-  1:num <- copy 34
-  2:num <- copy 35
-  3:point <- copy 1:point/unsafe
-]
-+mem: storing 34 in location 3
-+mem: storing 35 in location 4
+
+:(code)
+void test_copy_multiple_locations() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 35\n"
+      "  3:point <- copy 1:point/unsafe\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 3\n"
+      "mem: storing 35 in location 4\n"
+  );
+}
 
 //: trying to copy to a differently-typed destination will fail
-:(scenario copy_checks_size)
-% Hide_errors = true;
-def main [
-  2:point <- copy 1:num
-]
-+error: main: can't copy '1:num' to '2:point'; types don't match
+void test_copy_checks_size() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  2:point <- copy 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: can't copy '1:num' to '2:point'; types don't match\n"
+  );
+}
 
 :(before "End Mu Types Initialization")
 // A more complex example container, containing another container as one of
@@ -42,55 +54,76 @@ get(Type, point_number).name = "point-number";
 get(Type, point_number).elements.push_back(reagent("xy:point"));
 get(Type, point_number).elements.push_back(reagent("z:number"));
 
-:(scenario copy_handles_nested_container_elements)
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  14:num <- copy 36
-  15:point-number <- copy 12:point-number/unsafe
-]
-+mem: storing 36 in location 17
+:(code)
+void test_copy_handles_nested_container_elements() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  15:point-number <- copy 12:point-number/unsafe\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 36 in location 17\n"
+  );
+}
 
 //: products of recipes can include containers
-:(scenario return_container)
-def main [
-  3:point <- f 2
-]
-def f [
-  12:num <- next-ingredient
-  13:num <- copy 35
-  return 12:point/raw
-]
-+run: result 0 is [2, 35]
-+mem: storing 2 in location 3
-+mem: storing 35 in location 4
+void test_return_container() {
+  run(
+      "def main [\n"
+      "  3:point <- f 2\n"
+      "]\n"
+      "def f [\n"
+      "  12:num <- next-ingredient\n"
+      "  13:num <- copy 35\n"
+      "  return 12:point/raw\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: result 0 is [2, 35]\n"
+      "mem: storing 2 in location 3\n"
+      "mem: storing 35 in location 4\n"
+  );
+}
 
 //: Containers can be checked for equality with a single instruction just like
 //: numbers, no matter how large they are.
 
-:(scenario compare_multiple_locations)
-def main [
-  1:num <- copy 34  # first
-  2:num <- copy 35
-  3:num <- copy 36
-  4:num <- copy 34  # second
-  5:num <- copy 35
-  6:num <- copy 36
-  7:bool <- equal 1:point-number/raw, 4:point-number/unsafe
-]
-+mem: storing 1 in location 7
-
-:(scenario compare_multiple_locations_2)
-def main [
-  1:num <- copy 34  # first
-  2:num <- copy 35
-  3:num <- copy 36
-  4:num <- copy 34  # second
-  5:num <- copy 35
-  6:num <- copy 37  # different
-  7:bool <- equal 1:point-number/raw, 4:point-number/unsafe
-]
-+mem: storing 0 in location 7
+void test_compare_multiple_locations() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"  // first
+      "  2:num <- copy 35\n"
+      "  3:num <- copy 36\n"
+      "  4:num <- copy 34\n"  // second
+      "  5:num <- copy 35\n"
+      "  6:num <- copy 36\n"
+      "  7:bool <- equal 1:point-number/raw, 4:point-number/unsafe\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 7\n"
+  );
+}
+
+void test_compare_multiple_locations_2() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"  // first
+      "  2:num <- copy 35\n"
+      "  3:num <- copy 36\n"
+      "  4:num <- copy 34\n"  // second
+      "  5:num <- copy 35\n"
+      "  6:num <- copy 37\n"  // different
+      "  7:bool <- equal 1:point-number/raw, 4:point-number/unsafe\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 7\n"
+  );
+}
 
 :(before "End size_of(type) Special-cases")
 if (type->value == -1) return 1;  // error value, but we'll raise it elsewhere
@@ -114,26 +147,37 @@ if (t.kind == CONTAINER) {
   return result;
 }
 
-:(scenario stash_container)
-def main [
-  1:num <- copy 34  # first
-  2:num <- copy 35
-  3:num <- copy 36
-  stash [foo:], 1:point-number/raw
-]
-+app: foo: 34 35 36
+:(code)
+void test_stash_container() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"  // first
+      "  2:num <- copy 35\n"
+      "  3:num <- copy 36\n"
+      "  stash [foo:], 1:point-number/raw\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo: 34 35 36\n"
+  );
+}
 
 //:: To access elements of a container, use 'get'
 //: 'get' takes a 'base' container and an 'offset' into it and returns the
 //: appropriate element of the container value.
 
-:(scenario get)
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  15:num <- get 12:point/raw, 1:offset  # unsafe
-]
-+mem: storing 35 in location 15
+void test_get() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  15:num <- get 12:point/raw, 1:offset\n"  // unsafe
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 35 in location 15\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 GET,
@@ -223,66 +267,94 @@ const reagent element_type(const type_tree* type, int offset_value) {
   return element;
 }
 
-:(scenario get_handles_nested_container_elements)
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  14:num <- copy 36
-  15:num <- get 12:point-number/raw, 1:offset  # unsafe
-]
-+mem: storing 36 in location 15
-
-:(scenario get_out_of_bounds)
-% Hide_errors = true;
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  14:num <- copy 36
-  get 12:point-number/raw, 2:offset  # point-number occupies 3 locations but has only 2 fields; out of bounds
-]
-+error: main: invalid offset '2' for 'point-number'
-
-:(scenario get_out_of_bounds_2)
-% Hide_errors = true;
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  14:num <- copy 36
-  get 12:point-number/raw, -1:offset
-]
-+error: main: invalid offset '-1' for 'point-number'
-
-:(scenario get_product_type_mismatch)
-% Hide_errors = true;
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  14:num <- copy 36
-  15:&:num <- get 12:point-number/raw, 1:offset
-]
-+error: main: 'get 12:point-number/raw, 1:offset' should write to number but '15' has type (address number)
+void test_get_handles_nested_container_elements() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  15:num <- get 12:point-number/raw, 1:offset\n"  // unsafe
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 36 in location 15\n"
+  );
+}
+
+void test_get_out_of_bounds() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  get 12:point-number/raw, 2:offset\n"  // point-number occupies 3 locations but has only 2 fields; out of bounds
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: invalid offset '2' for 'point-number'\n"
+  );
+}
+
+void test_get_out_of_bounds_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  get 12:point-number/raw, -1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: invalid offset '-1' for 'point-number'\n"
+  );
+}
+
+void test_get_product_type_mismatch() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  15:&:num <- get 12:point-number/raw, 1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'get 12:point-number/raw, 1:offset' should write to number but '15' has type (address number)\n"
+  );
+}
 
 //: we might want to call 'get' without saving the results, say in a sandbox
 
-:(scenario get_without_product)
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  get 12:point/raw, 1:offset  # unsafe
-]
-# just don't die
+void test_get_without_product() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  get 12:point/raw, 1:offset\n"  // unsafe
+      "]\n"
+  );
+  // just don't die
+}
 
 //:: To write to elements of containers, use 'put'.
 
-:(scenario put)
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  $clear-trace
-  12:point <- put 12:point, 1:offset, 36
-]
-+mem: storing 36 in location 13
--mem: storing 34 in location 12
+void test_put() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  $clear-trace\n"
+      "  12:point <- put 12:point, 1:offset, 36\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 36 in location 13"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 34 in location 12");
+}
 
 :(before "End Primitive Recipe Declarations")
 PUT,
@@ -367,67 +439,85 @@ case PUT: {
   break;
 }
 
-:(scenario put_product_error)
-% Hide_errors = true;
-def main [
-  local-scope
-  load-ingredients
-  1:point <- merge 34, 35
-  3:point <- put 1:point, x:offset, 36
-]
-+error: main: product of 'put' must be first ingredient '1:point', but got '3:point'
+:(code)
+void test_put_product_error() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  1:point <- merge 34, 35\n"
+      "  3:point <- put 1:point, x:offset, 36\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: product of 'put' must be first ingredient '1:point', but got '3:point'\n"
+  );
+}
 
 //:: Allow containers to be defined in Mu code.
 
-:(scenarios load)
-:(scenario container)
-container foo [
-  x:num
-  y:num
-]
-+parse: --- defining container foo
-+parse: element: {x: "number"}
-+parse: element: {y: "number"}
-
-:(scenario container_use_before_definition)
-container foo [
-  x:num
-  y:bar
-]
-container bar [
-  x:num
-  y:num
-]
-+parse: --- defining container foo
-+parse: type number: 1000
-+parse:   element: {x: "number"}
-# todo: brittle
-# type bar is unknown at this point, but we assign it a number
-+parse:   element: {y: "bar"}
-# later type bar geon
-+parse: --- defining container bar
-+parse: type number: 1001
-+parse:   element: {x: "number"}
-+parse:   element: {y: "number"}
+void test_container() {
+  load(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: --- defining container foo\n"
+      "parse: element: {x: \"number\"}\n"
+      "parse: element: {y: \"number\"}\n"
+  );
+}
+
+void test_container_use_before_definition() {
+  load(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: --- defining container foo\n"
+      "parse: type number: 1000\n"
+      "parse:   element: {x: \"number\"}\n"
+      // todo: brittle
+      // type bar is unknown at this point, but we assign it a number
+      "parse:   element: {y: \"bar\"}\n"
+      // later type bar gets a definition
+      "parse: --- defining container bar\n"
+      "parse: type number: 1001\n"
+      "parse:   element: {x: \"number\"}\n"
+      "parse:   element: {y: \"number\"}\n"
+  );
+}
 
 //: if a container is defined again, the new fields add to the original definition
-:(scenarios run)
-:(scenario container_extend)
-container foo [
-  x:num
-]
-# add to previous definition
-container foo [
-  y:num
-]
-def main [
-  1:num <- copy 34
-  2:num <- copy 35
-  3:num <- get 1:foo, 0:offset
-  4:num <- get 1:foo, 1:offset
-]
-+mem: storing 34 in location 3
-+mem: storing 35 in location 4
+void test_container_extend() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "]\n"
+      "container foo [\n"  // add to previous definition
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 35\n"
+      "  3:num <- get 1:foo, 0:offset\n"
+      "  4:num <- get 1:foo, 1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 3\n"
+      "mem: storing 35 in location 4\n"
+  );
+}
 
 :(before "End Command Handlers")
 else if (command == "container") {
@@ -521,25 +611,35 @@ void skip_bracket(istream& in, string message) {
     raise << message << '\n' << end();
 }
 
-:(scenario multi_word_line_in_container_declaration)
-% Hide_errors = true;
-container foo [
-  x:num y:num
-]
-+error: container 'foo' contains multiple elements on a single line. Containers and exclusive containers must only contain elements, one to a line, no code.
+void test_multi_word_line_in_container_declaration() {
+  Hide_errors = true;
+  run(
+      "container foo [\n"
+      "  x:num y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: container 'foo' contains multiple elements on a single line. Containers and exclusive containers must only contain elements, one to a line, no code.\n"
+  );
+}
 
 //: support type abbreviations in container definitions
 
-:(scenario type_abbreviations_in_containers)
-type foo = number
-container bar [
-  x:foo
-]
-def main [
-  1:num <- copy 34
-  2:foo <- get 1:bar/unsafe, 0:offset
-]
-+mem: storing 34 in location 2
+void test_type_abbreviations_in_containers() {
+  run(
+      "type foo = number\n"
+      "container bar [\n"
+      "  x:foo\n"
+      "]\n"
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:foo <- get 1:bar/unsafe, 0:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 2\n"
+  );
+}
 
 :(after "Transform.push_back(expand_type_abbreviations)")
 Transform.push_back(expand_type_abbreviations_in_containers);  // idempotent
@@ -579,22 +679,29 @@ void test_error_on_transform_all_between_container_definition_and_extension() {
 //:: Allow container definitions anywhere in the codebase, but complain if you
 //:: can't find a definition at the end.
 
-:(scenario run_complains_on_unknown_types)
-% Hide_errors = true;
-def main [
-  # integer is not a type
-  1:integer <- copy 0
-]
-+error: main: unknown type integer in '1:integer <- copy 0'
-
-:(scenario run_allows_type_definition_after_use)
-def main [
-  1:bar <- copy 0/unsafe
-]
-container bar [
-  x:num
-]
-$error: 0
+void test_run_complains_on_unknown_types() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:integer <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: unknown type integer in '1:integer <- copy 0'\n"
+  );
+}
+
+void test_run_allows_type_definition_after_use() {
+  run(
+      "def main [\n"
+      "  1:bar <- copy 0/unsafe\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 :(before "End Type Modifying Transforms")
 Transform.push_back(check_or_set_invalid_types);  // idempotent
@@ -636,29 +743,41 @@ void check_or_set_invalid_types(type_tree* type, const string& location_for_erro
   }
 }
 
-:(scenario container_unknown_field)
-% Hide_errors = true;
-container foo [
-  x:num
-  y:bar
-]
-+error: foo: unknown type in y
-
-:(scenario read_container_with_bracket_in_comment)
-container foo [
-  x:num
-  # ']' in comment
-  y:num
-]
-+parse: --- defining container foo
-+parse: element: {x: "number"}
-+parse: element: {y: "number"}
-
-:(scenario container_with_compound_field_type)
-container foo [
-  {x: (address array (address array character))}
-]
-$error: 0
+void test_container_unknown_field() {
+  Hide_errors = true;
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: unknown type in y\n"
+  );
+}
+
+void test_read_container_with_bracket_in_comment() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  # ']' in comment\n"
+      "  y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: --- defining container foo\n"
+      "parse: element: {x: \"number\"}\n"
+      "parse: element: {y: \"number\"}\n"
+  );
+}
+void test_container_with_compound_field_type() {
+  run(
+      "container foo [\n"
+      "  {x: (address array (address array character))}\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 :(before "End transform_all")
 check_container_field_types();
diff --git a/031merge.cc b/031merge.cc
index 99b6846a..ace4387b 100644
--- a/031merge.cc
+++ b/031merge.cc
@@ -1,15 +1,20 @@
 //: Construct types out of their constituent fields.
 
-:(scenario merge)
-container foo [
-  x:num
-  y:num
-]
-def main [
-  1:foo <- merge 3, 4
-]
-+mem: storing 3 in location 1
-+mem: storing 4 in location 2
+void test_merge() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 3, 4\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 1\n"
+      "mem: storing 4 in location 2\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 MERGE,
@@ -31,66 +36,99 @@ case MERGE: {
 
 //: type-check 'merge' to avoid interpreting numbers as addresses
 
-:(scenario merge_check)
-def main [
-  1:point <- merge 3, 4
-]
-$error: 0
-
-:(scenario merge_check_missing_element)
-% Hide_errors = true;
-def main [
-  1:point <- merge 3
-]
-+error: main: too few ingredients in '1:point <- merge 3'
-
-:(scenario merge_check_extra_element)
-% Hide_errors = true;
-def main [
-  1:point <- merge 3, 4, 5
-]
-+error: main: too many ingredients in '1:point <- merge 3, 4, 5'
+:(code)
+void test_merge_check() {
+  run(
+      "def main [\n"
+      "  1:point <- merge 3, 4\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_missing_element() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:point <- merge 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too few ingredients in '1:point <- merge 3'\n"
+  );
+}
+
+void test_merge_check_extra_element() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:point <- merge 3, 4, 5\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too many ingredients in '1:point <- merge 3, 4, 5'\n"
+  );
+}
 
 //: We want to avoid causing memory corruption, but other than that we want to
 //: be flexible in how we construct containers of containers. It should be
 //: equally easy to define a container out of primitives or intermediate
 //: container fields.
 
-:(scenario merge_check_recursive_containers)
-def main [
-  1:point <- merge 3, 4
-  1:point-number <- merge 1:point, 5
-]
-$error: 0
-
-:(scenario merge_check_recursive_containers_2)
-% Hide_errors = true;
-def main [
-  1:point <- merge 3, 4
-  2:point-number <- merge 1:point
-]
-+error: main: too few ingredients in '2:point-number <- merge 1:point'
-
-:(scenario merge_check_recursive_containers_3)
-def main [
-  1:point-number <- merge 3, 4, 5
-]
-$error: 0
-
-:(scenario merge_check_recursive_containers_4)
-% Hide_errors = true;
-def main [
-  1:point-number <- merge 3, 4
-]
-+error: main: too few ingredients in '1:point-number <- merge 3, 4'
-
-:(scenario merge_check_reflexive)
-% Hide_errors = true;
-def main [
-  1:point <- merge 3, 4
-  2:point <- merge 1:point
-]
-$error: 0
+void test_merge_check_recursive_containers() {
+  run(
+      "def main [\n"
+      "  1:point <- merge 3, 4\n"
+      "  1:point-number <- merge 1:point, 5\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_recursive_containers_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:point <- merge 3, 4\n"
+      "  2:point-number <- merge 1:point\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too few ingredients in '2:point-number <- merge 1:point'\n"
+  );
+}
+
+void test_merge_check_recursive_containers_3() {
+  run(
+      "def main [\n"
+      "  1:point-number <- merge 3, 4, 5\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_recursive_containers_4() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:point-number <- merge 3, 4\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too few ingredients in '1:point-number <- merge 3, 4'\n"
+  );
+}
+
+void test_merge_check_reflexive() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:point <- merge 3, 4\n"
+      "  2:point <- merge 1:point\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 //: Since a container can be merged in several ways, we need to be able to
 //: backtrack through different possibilities. Later we'll allow creating
@@ -215,12 +253,17 @@ const type_tree* get_base_type(const type_tree* t) {
   return t;
 }
 
-:(scenario merge_check_product)
-% Hide_errors = true;
-def main [
-  1:num <- merge 3
-]
-+error: main: 'merge' should yield a container in '1:num <- merge 3'
+void test_merge_check_product() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- merge 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'merge' should yield a container in '1:num <- merge 3'\n"
+  );
+}
 
 :(before "End Includes")
 #include <stack>
diff --git a/032array.cc b/032array.cc
index 7f36411f..30adde20 100644
--- a/032array.cc
+++ b/032array.cc
@@ -6,12 +6,17 @@
 //: Create containers containing addresses to arrays instead.
 
 //: You can create arrays using 'create-array'.
-:(scenario create_array)
-def main [
-  # create an array occupying locations 1 (for the size) and 2-4 (for the elements)
-  1:array:num:3 <- create-array
-]
-+run: creating array from 4 locations
+void test_create_array() {
+  run(
+      "def main [\n"
+      // create an array occupying locations 1 (for the size) and 2-4 (for the elements)
+      "  1:array:num:3 <- create-array\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: creating array from 4 locations\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 CREATE_ARRAY,
@@ -69,34 +74,45 @@ case CREATE_ARRAY: {
   break;
 }
 
-:(scenario copy_array)
-# Arrays can be copied around with a single instruction just like numbers,
-# no matter how large they are.
-# You don't need to pass the size around, since each array variable stores its
-# size in memory at run-time. We'll call a variable with an explicit size a
-# 'static' array, and one without a 'dynamic' array since it can contain
-# arrays of many different sizes.
-def main [
-  1:array:num:3 <- create-array
-  2:num <- copy 14
-  3:num <- copy 15
-  4:num <- copy 16
-  5:array:num <- copy 1:array:num:3
-]
-+mem: storing 3 in location 5
-+mem: storing 14 in location 6
-+mem: storing 15 in location 7
-+mem: storing 16 in location 8
-
-:(scenario stash_array)
-def main [
-  1:array:num:3 <- create-array
-  2:num <- copy 14
-  3:num <- copy 15
-  4:num <- copy 16
-  stash [foo:], 1:array:num:3
-]
-+app: foo: 3 14 15 16
+:(code)
+// Arrays can be copied around with a single instruction just like numbers,
+// no matter how large they are.
+// You don't need to pass the size around, since each array variable stores its
+// size in memory at run-time. We'll call a variable with an explicit size a
+// 'static' array, and one without a 'dynamic' array since it can contain
+// arrays of many different sizes.
+void test_copy_array() {
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  2:num <- copy 14\n"
+      "  3:num <- copy 15\n"
+      "  4:num <- copy 16\n"
+      "  5:array:num <- copy 1:array:num:3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 5\n"
+      "mem: storing 14 in location 6\n"
+      "mem: storing 15 in location 7\n"
+      "mem: storing 16 in location 8\n"
+  );
+}
+
+void test_stash_array() {
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  2:num <- copy 14\n"
+      "  3:num <- copy 15\n"
+      "  4:num <- copy 16\n"
+      "  stash [foo:], 1:array:num:3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo: 3 14 15 16\n"
+  );
+}
 
 :(before "End types_coercible Special-cases")
 if (is_mu_array(from) && is_mu_array(to))
@@ -133,23 +149,33 @@ if (x.type && !x.type->atom && x.type->left->value == Array_type_ordinal) return
 //: arrays are disallowed inside containers unless their length is fixed in
 //: advance
 
-:(scenario container_permits_static_array_element)
-container foo [
-  x:array:num:3
-]
-$error: 0
+:(code)
+void test_container_permits_static_array_element() {
+  run(
+      "container foo [\n"
+      "  x:array:num:3\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 :(before "End insert_container Special-cases")
 else if (is_integer(type->name)) {  // sometimes types will contain non-type tags, like numbers for the size of an array
   type->value = 0;
 }
 
-:(scenario container_disallows_dynamic_array_element)
-% Hide_errors = true;
-container foo [
-  x:array:num
-]
-+error: container 'foo' cannot determine size of element 'x'
+:(code)
+void test_container_disallows_dynamic_array_element() {
+  Hide_errors = true;
+  run(
+      "container foo [\n"
+      "  x:array:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: container 'foo' cannot determine size of element 'x'\n"
+  );
+}
 
 :(before "End Load Container Element Definition")
 {
@@ -178,64 +204,86 @@ if (current_call().running_step_index < SIZE(get(Recipe, current_call().running_
   return false;
 }
 
-:(scenario merge_static_array_into_container)
-container foo [
-  x:num
-  y:array:num:3
-]
-def main [
-  1:array:num:3 <- create-array
-  10:foo <- merge 34, 1:array:num:3
-]
-# no errors
-
-:(scenario code_inside_container)
-% Hide_errors = true;
-container card [
-  rank:num <- next-ingredient
-]
-def foo [
-  1:card <- merge 3
-  2:num <- get 1:card rank:offset
-]
-# shouldn't die
+:(code)
+void test_merge_static_array_into_container() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:array:num:3\n"
+      "]\n"
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  10:foo <- merge 34, 1:array:num:3\n"
+      "]\n"
+  );
+  // no errors
+}
+
+void test_code_inside_container() {
+  Hide_errors = true;
+  run(
+      "container card [\n"
+      "  rank:num <- next-ingredient\n"
+      "]\n"
+      "def foo [\n"
+      "  1:card <- merge 3\n"
+      "  2:num <- get 1:card rank:offset\n"
+      "]\n"
+  );
+  // shouldn't die
+}
 
 //:: To access elements of an array, use 'index'
 
-:(scenario index)
-def main [
-  1:array:num:3 <- create-array
-  2:num <- copy 14
-  3:num <- copy 15
-  4:num <- copy 16
-  10:num <- index 1:array:num:3, 0/index  # the index must be a non-negative whole number
-]
-+mem: storing 14 in location 10
-
-:(scenario index_compound_element)
-def main [
-  {1: (array (address number) 3)} <- create-array
-  # skip alloc id
-  3:num <- copy 14
-  # skip alloc id
-  5:num <- copy 15
-  # skip alloc id
-  7:num <- copy 16
-  10:address:num <- index {1: (array (address number) 3)}, 0
-]
-# skip alloc id
-+mem: storing 14 in location 11
-
-:(scenario index_direct_offset)
-def main [
-  1:array:num:3 <- create-array
-  2:num <- copy 14
-  3:num <- copy 15
-  4:num <- copy 16
-  10:num <- copy 0
-  20:num <- index 1:array:num, 10:num
-]
-+mem: storing 14 in location 20
+void test_index() {
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  2:num <- copy 14\n"
+      "  3:num <- copy 15\n"
+      "  4:num <- copy 16\n"
+      "  10:num <- index 1:array:num:3, 0/index\n"  // the index must be a non-negative whole number
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 14 in location 10\n"
+  );
+}
+
+void test_index_compound_element() {
+  run(
+      "def main [\n"
+      "  {1: (array (address number) 3)} <- create-array\n"
+      // skip alloc id
+      "  3:num <- copy 14\n"
+      // skip alloc id
+      "  5:num <- copy 15\n"
+      // skip alloc id
+      "  7:num <- copy 16\n"
+      "  10:address:num <- index {1: (array (address number) 3)}, 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // skip alloc id
+      "mem: storing 14 in location 11\n"
+  );
+}
+
+void test_index_direct_offset() {
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  2:num <- copy 14\n"
+      "  3:num <- copy 15\n"
+      "  4:num <- copy 16\n"
+      "  10:num <- copy 0\n"
+      "  20:num <- index 1:array:num, 10:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 14 in location 20\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 INDEX,
@@ -344,58 +392,86 @@ void test_array_length_static() {
   CHECK_EQ(array_length(x), 3);
 }
 
-:(scenario index_truncates)
-def main [
-  1:array:num:3 <- create-array
-  2:num <- copy 14
-  3:num <- copy 15
-  4:num <- copy 16
-  10:num <- index 1:array:num:3, 1.5  # non-whole number
-]
-# fraction is truncated away
-+mem: storing 15 in location 10
-
-:(scenario index_out_of_bounds)
-% Hide_errors = true;
-def main [
-  1:array:point:3 <- create-array
-  index 1:array:point:3, 4  # less than size of array in locations, but larger than its length in elements
-]
-+error: main: invalid index 4 in 'index 1:array:point:3, 4'
-
-:(scenario index_out_of_bounds_2)
-% Hide_errors = true;
-def main [
-  1:array:num:3 <- create-array
-  index 1:array:num, -1
-]
-+error: main: invalid index -1 in 'index 1:array:num, -1'
-
-:(scenario index_product_type_mismatch)
-% Hide_errors = true;
-def main [
-  1:array:point:3 <- create-array
-  10:num <- index 1:array:point, 0
-]
-+error: main: 'index' on '1:array:point' can't be saved in '10:num'; type should be 'point'
+void test_index_truncates() {
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  2:num <- copy 14\n"
+      "  3:num <- copy 15\n"
+      "  4:num <- copy 16\n"
+      "  10:num <- index 1:array:num:3, 1.5\n"  // non-whole number
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // fraction is truncated away
+      "mem: storing 15 in location 10\n"
+  );
+}
+
+void test_index_out_of_bounds() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:array:point:3 <- create-array\n"
+      "  index 1:array:point:3, 4\n"  // less than size of array in locations, but larger than its length in elements
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: invalid index 4 in 'index 1:array:point:3, 4'\n"
+  );
+}
+
+void test_index_out_of_bounds_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  index 1:array:num, -1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: invalid index -1 in 'index 1:array:num, -1'\n"
+  );
+}
+
+void test_index_product_type_mismatch() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:array:point:3 <- create-array\n"
+      "  10:num <- index 1:array:point, 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'index' on '1:array:point' can't be saved in '10:num'; type should be 'point'\n"
+  );
+}
 
 //: we might want to call 'index' without saving the results, say in a sandbox
 
-:(scenario index_without_product)
-def main [
-  1:array:num:3 <- create-array
-  index 1:array:num:3, 0
-]
-# just don't die
+void test_index_without_product() {
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  index 1:array:num:3, 0\n"
+      "]\n"
+  );
+  // just don't die
+}
 
 //:: To write to elements of arrays, use 'put'.
 
-:(scenario put_index)
-def main [
-  1:array:num:3 <- create-array
-  1:array:num <- put-index 1:array:num, 1, 34
-]
-+mem: storing 34 in location 3
+void test_put_index() {
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  1:array:num <- put-index 1:array:num, 1, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 3\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 PUT_INDEX,
@@ -464,40 +540,61 @@ case PUT_INDEX: {
   break;
 }
 
-:(scenario put_index_out_of_bounds)
-% Hide_errors = true;
-def main [
-  1:array:point:3 <- create-array
-  8:point <- merge 34, 35
-  1:array:point <- put-index 1:array:point, 4, 8:point  # '4' is less than size of array in locations, but larger than its length in elements
-]
-+error: main: invalid index 4 in '1:array:point <- put-index 1:array:point, 4, 8:point'
-
-:(scenario put_index_out_of_bounds_2)
-% Hide_errors = true;
-def main [
-  1:array:point:3 <- create-array
-  10:point <- merge 34, 35
-  1:array:point <- put-index 1:array:point, -1, 10:point
-]
-+error: main: invalid index -1 in '1:array:point <- put-index 1:array:point, -1, 10:point'
-
-:(scenario put_index_product_error)
-% Hide_errors = true;
-def main [
-  1:array:num:3 <- create-array
-  4:array:num:3 <- put-index 1:array:num:3, 0, 34
-]
-+error: main: product of 'put-index' must be first ingredient '1:array:num:3', but got '4:array:num:3'
+:(code)
+void test_put_index_out_of_bounds() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:array:point:3 <- create-array\n"
+      "  8:point <- merge 34, 35\n"
+      "  1:array:point <- put-index 1:array:point, 4, 8:point\n"  // '4' is less than size of array in locations, but larger than its length in elements
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: invalid index 4 in '1:array:point <- put-index 1:array:point, 4, 8:point'\n"
+  );
+}
+
+void test_put_index_out_of_bounds_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:array:point:3 <- create-array\n"
+      "  10:point <- merge 34, 35\n"
+      "  1:array:point <- put-index 1:array:point, -1, 10:point\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: invalid index -1 in '1:array:point <- put-index 1:array:point, -1, 10:point'\n"
+  );
+}
+
+void test_put_index_product_error() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  4:array:num:3 <- put-index 1:array:num:3, 0, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: product of 'put-index' must be first ingredient '1:array:num:3', but got '4:array:num:3'\n"
+  );
+}
 
 //:: compute the length of an array
 
-:(scenario array_length)
-def main [
-  1:array:num:3 <- create-array
-  10:num <- length 1:array:num
-]
-+mem: storing 3 in location 10
+void test_array_length() {
+  run(
+      "def main [\n"
+      "  1:array:num:3 <- create-array\n"
+      "  10:num <- length 1:array:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 10\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 LENGTH,
diff --git a/033exclusive_container.cc b/033exclusive_container.cc
index 5c57972e..fc944f8d 100644
--- a/033exclusive_container.cc
+++ b/033exclusive_container.cc
@@ -18,17 +18,25 @@ get(Type, tmp).elements.push_back(reagent("p:point"));
 //: Tests in this layer often explicitly set up memory before reading it as a
 //: container. Don't do this in general. I'm tagging such cases with /unsafe;
 //: they'll be exceptions to later checks.
-:(scenario copy_exclusive_container)
-# Copying exclusive containers copies all their contents and an extra location for the tag.
-def main [
-  1:num <- copy 1  # 'point' variant
-  2:num <- copy 34
-  3:num <- copy 35
-  4:number-or-point <- copy 1:number-or-point/unsafe
-]
-+mem: storing 1 in location 4
-+mem: storing 34 in location 5
-+mem: storing 35 in location 6
+
+:(code)
+void test_copy_exclusive_container() {
+  run(
+      // Copying exclusive containers copies all their contents, and an extra
+      // location for the tag.
+      "def main [\n"
+      "  1:num <- copy 1\n"  // 'point' variant
+      "  2:num <- copy 34\n"
+      "  3:num <- copy 35\n"
+      "  4:number-or-point <- copy 1:number-or-point/unsafe\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 4\n"
+      "mem: storing 34 in location 5\n"
+      "mem: storing 35 in location 6\n"
+  );
+}
 
 :(before "End size_of(type) Special-cases")
 if (t.kind == EXCLUSIVE_CONTAINER) {
@@ -55,29 +63,41 @@ if (t.kind == EXCLUSIVE_CONTAINER) {
 :(before "End Mu Types Initialization")
 put(Type_ordinal, "variant", 0);
 
-:(scenario maybe_convert)
-def main [
-  12:num <- copy 1
-  13:num <- copy 35
-  14:num <- copy 36
-  20:point, 22:bool <- maybe-convert 12:number-or-point/unsafe, 1:variant
-]
-# boolean
-+mem: storing 1 in location 22
-# point
-+mem: storing 35 in location 20
-+mem: storing 36 in location 21
-
-:(scenario maybe_convert_fail)
-def main [
-  12:num <- copy 1
-  13:num <- copy 35
-  14:num <- copy 36
-  20:num, 21:bool <- maybe-convert 12:number-or-point/unsafe, 0:variant
-]
-# boolean
-+mem: storing 0 in location 21
-# number: no write
+:(code)
+void test_maybe_convert() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 1\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  20:point, 22:bool <- maybe-convert 12:number-or-point/unsafe, 1:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // boolean
+      "mem: storing 1 in location 22\n"
+      // point
+      "mem: storing 35 in location 20\n"
+      "mem: storing 36 in location 21\n"
+  );
+}
+
+void test_maybe_convert_fail() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 1\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  20:num, 21:bool <- maybe-convert 12:number-or-point/unsafe, 0:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // boolean
+      "mem: storing 0 in location 21\n"
+      // number: no write
+  );
+}
+
 
 :(before "End Primitive Recipe Declarations")
 MAYBE_CONVERT,
@@ -185,35 +205,48 @@ const reagent variant_type(const type_tree* type, int tag) {
   return element;
 }
 
-:(scenario maybe_convert_product_type_mismatch)
-% Hide_errors = true;
-def main [
-  12:num <- copy 1
-  13:num <- copy 35
-  14:num <- copy 36
-  20:num, 21:bool <- maybe-convert 12:number-or-point/unsafe, 1:variant
-]
-+error: main: 'maybe-convert 12:number-or-point/unsafe, 1:variant' should write to point but '20' has type number
-
-:(scenario maybe_convert_dummy_product)
-def main [
-  12:num <- copy 1
-  13:num <- copy 35
-  14:num <- copy 36
-  _, 21:bool <- maybe-convert 12:number-or-point/unsafe, 1:variant
-]
-$error: 0
+void test_maybe_convert_product_type_mismatch() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  12:num <- copy 1\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  20:num, 21:bool <- maybe-convert 12:number-or-point/unsafe, 1:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'maybe-convert 12:number-or-point/unsafe, 1:variant' should write to point but '20' has type number\n"
+  );
+}
+
+void test_maybe_convert_dummy_product() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 1\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  _, 21:bool <- maybe-convert 12:number-or-point/unsafe, 1:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 //:: Allow exclusive containers to be defined in Mu code.
 
-:(scenario exclusive_container)
-exclusive-container foo [
-  x:num
-  y:num
-]
-+parse: --- defining exclusive-container foo
-+parse: element: {x: "number"}
-+parse: element: {y: "number"}
+void test_exclusive_container() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: --- defining exclusive-container foo\n"
+      "parse: element: {x: \"number\"}\n"
+      "parse: element: {y: \"number\"}\n"
+  );
+}
 
 :(before "End Command Handlers")
 else if (command == "exclusive-container") {
@@ -223,84 +256,113 @@ else if (command == "exclusive-container") {
 //: arrays are disallowed inside exclusive containers unless their length is
 //: fixed in advance
 
-:(scenario exclusive_container_contains_array)
-exclusive-container foo [
-  x:@:num:3
-]
-$error: 0
+:(code)
+void test_exclusive_container_contains_array() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:@:num:3\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(scenario exclusive_container_disallows_dynamic_array_element)
-% Hide_errors = true;
-exclusive-container foo [
-  x:@:num
-]
-+error: container 'foo' cannot determine size of element 'x'
+void test_exclusive_container_disallows_dynamic_array_element() {
+  Hide_errors = true;
+  run(
+      "exclusive-container foo [\n"
+      "  x:@:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: container 'foo' cannot determine size of element 'x'\n"
+  );
+}
 
 //:: To construct exclusive containers out of variant types, use 'merge'.
-:(scenario lift_to_exclusive_container)
-exclusive-container foo [
-  x:num
-  y:num
-]
-def main [
-  1:num <- copy 34
-  2:foo <- merge 0/x, 1:num  # tag must be a literal when merging exclusive containers
-  4:foo <- merge 1/y, 1:num
-]
-+mem: storing 0 in location 2
-+mem: storing 34 in location 3
-+mem: storing 1 in location 4
-+mem: storing 34 in location 5
+void test_lift_to_exclusive_container() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:foo <- merge 0/x, 1:num\n"  // tag must be a literal when merging exclusive containers
+      "  4:foo <- merge 1/y, 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 2\n"
+      "mem: storing 34 in location 3\n"
+      "mem: storing 1 in location 4\n"
+      "mem: storing 34 in location 5\n"
+  );
+}
 
 //: type-checking for 'merge' on exclusive containers
 
-:(scenario merge_handles_exclusive_container)
-exclusive-container foo [
-  x:num
-  y:bar
-]
-container bar [
-  z:num
-]
-def main [
-  1:foo <- merge 0/x, 34
-]
-+mem: storing 0 in location 1
-+mem: storing 34 in location 2
-$error: 0
-
-:(scenario merge_requires_literal_tag_for_exclusive_container)
-% Hide_errors = true;
-exclusive-container foo [
-  x:num
-  y:bar
-]
-container bar [
-  z:num
-]
-def main [
-  1:num <- copy 0
-  2:foo <- merge 1:num, 34
-]
-+error: main: ingredient 0 of 'merge' should be a literal, for the tag of exclusive-container 'foo' in '2:foo <- merge 1:num, 34'
-
-:(scenario merge_handles_exclusive_container_inside_exclusive_container)
-exclusive-container foo [
-  x:num
-  y:bar
-]
-exclusive-container bar [
-  a:num
-  b:num
-]
-def main [
-  1:num <- copy 0
-  2:bar <- merge 0/a, 34
-  4:foo <- merge 1/y, 2:bar
-]
-+mem: storing 0 in location 5
-+mem: storing 34 in location 6
-$error: 0
+void test_merge_handles_exclusive_container() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "container bar [\n"
+      "  z:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 0/x, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+      "mem: storing 34 in location 2\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_requires_literal_tag_for_exclusive_container() {
+  Hide_errors = true;
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "container bar [\n"
+      "  z:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  2:foo <- merge 1:num, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: ingredient 0 of 'merge' should be a literal, for the tag of exclusive-container 'foo' in '2:foo <- merge 1:num, 34'\n"
+  );
+}
+
+void test_merge_handles_exclusive_container_inside_exclusive_container() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "exclusive-container bar [\n"
+      "  a:num\n"
+      "  b:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  2:bar <- merge 0/a, 34\n"
+      "  4:foo <- merge 1/y, 2:bar\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 5\n"
+      "mem: storing 34 in location 6\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 :(before "End check_merge_call Special-cases")
 case EXCLUSIVE_CONTAINER: {
@@ -328,98 +390,125 @@ case EXCLUSIVE_CONTAINER: {
   break;
 }
 
-:(scenario merge_check_container_containing_exclusive_container)
-container foo [
-  x:num
-  y:bar
-]
-exclusive-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo <- merge 23, 1/y, 34
-]
-+mem: storing 23 in location 1
-+mem: storing 1 in location 2
-+mem: storing 34 in location 3
-$error: 0
-
-:(scenario merge_check_container_containing_exclusive_container_2)
-% Hide_errors = true;
-container foo [
-  x:num
-  y:bar
-]
-exclusive-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo <- merge 23, 1/y, 34, 35
-]
-+error: main: too many ingredients in '1:foo <- merge 23, 1/y, 34, 35'
-
-:(scenario merge_check_exclusive_container_containing_container)
-exclusive-container foo [
-  x:num
-  y:bar
-]
-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo <- merge 1/y, 23, 34
-]
-+mem: storing 1 in location 1
-+mem: storing 23 in location 2
-+mem: storing 34 in location 3
-$error: 0
-
-:(scenario merge_check_exclusive_container_containing_container_2)
-exclusive-container foo [
-  x:num
-  y:bar
-]
-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo <- merge 0/x, 23
-]
-$error: 0
-
-:(scenario merge_check_exclusive_container_containing_container_3)
-% Hide_errors = true;
-exclusive-container foo [
-  x:num
-  y:bar
-]
-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo <- merge 1/y, 23
-]
-+error: main: too few ingredients in '1:foo <- merge 1/y, 23'
-
-:(scenario merge_check_exclusive_container_containing_container_4)
-exclusive-container foo [
-  x:num
-  y:bar
-]
-container bar [
-  a:num
-  b:num
-]
-def main [
-  1:bar <- merge 23, 24
-  3:foo <- merge 1/y, 1:bar
-]
-$error: 0
+:(code)
+void test_merge_check_container_containing_exclusive_container() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "exclusive-container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 23, 1/y, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 23 in location 1\n"
+      "mem: storing 1 in location 2\n"
+      "mem: storing 34 in location 3\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_container_containing_exclusive_container_2() {
+  Hide_errors = true;
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "exclusive-container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 23, 1/y, 34, 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too many ingredients in '1:foo <- merge 23, 1/y, 34, 35'\n"
+  );
+}
+
+void test_merge_check_exclusive_container_containing_container() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 1/y, 23, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+      "mem: storing 23 in location 2\n"
+      "mem: storing 34 in location 3\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_exclusive_container_containing_container_2() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 0/x, 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_exclusive_container_containing_container_3() {
+  Hide_errors = true;
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 1/y, 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too few ingredients in '1:foo <- merge 1/y, 23'\n"
+  );
+}
+
+void test_merge_check_exclusive_container_containing_container_4() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:num\n"
+      "  y:bar\n"
+      "]\n"
+      "container bar [\n"
+      "  a:num\n"
+      "  b:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:bar <- merge 23, 24\n"
+      "  3:foo <- merge 1/y, 1:bar\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 //: Since the different variants of an exclusive-container might have
 //: different sizes, relax the size mismatch check for 'merge' instructions.
@@ -436,24 +525,30 @@ if (current_step_index() < SIZE(Current_routine->steps())
     return size_of(x) < SIZE(data);
 }
 
-:(scenario merge_exclusive_container_with_mismatched_sizes)
-container foo [
-  x:num
-  y:num
-]
-exclusive-container bar [
-  x:num
-  y:foo
-]
-def main [
-  1:num <- copy 34
-  2:num <- copy 35
-  3:bar <- merge 0/x, 1:num
-  6:bar <- merge 1/foo, 1:num, 2:num
-]
-+mem: storing 0 in location 3
-+mem: storing 34 in location 4
-# bar is always 3 large so location 5 is skipped
-+mem: storing 1 in location 6
-+mem: storing 34 in location 7
-+mem: storing 35 in location 8
+:(code)
+void test_merge_exclusive_container_with_mismatched_sizes() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "exclusive-container bar [\n"
+      "  x:num\n"
+      "  y:foo\n"
+      "]\n"
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 35\n"
+      "  3:bar <- merge 0/x, 1:num\n"
+      "  6:bar <- merge 1/foo, 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 3\n"
+      "mem: storing 34 in location 4\n"
+      // bar is always 3 large so location 5 is skipped
+      "mem: storing 1 in location 6\n"
+      "mem: storing 34 in location 7\n"
+      "mem: storing 35 in location 8\n"
+  );
+}
diff --git a/034address.cc b/034address.cc
index af98fc14..bafde7b4 100644
--- a/034address.cc
+++ b/034address.cc
@@ -52,37 +52,54 @@ Next_alloc_id = 0;
 
 //: todo: give 'new' a custodian ingredient. Following malloc/free is a temporary hack.
 
-:(scenario new)
-# call 'new' two times with identical types without modifying the results; you
-# should get back different results
-def main [
-  10:&:num <- new num:type
-  12:&:num <- new num:type
-  20:bool <- equal 10:&:num, 12:&:num
-]
-+mem: storing 1000 in location 11
-+mem: storing 0 in location 20
-
-:(scenario new_array)
-# call 'new' with a second ingredient to allocate an array of some type rather than a single copy
-def main [
-  10:&:@:num <- new num:type, 5
-  12:&:num <- new num:type
-  20:num/alloc2, 21:num/alloc1 <- deaddress 10:&:@:num, 12:&:num
-  30:num <- subtract 21:num/alloc2, 20:num/alloc1
-]
-+run: {10: ("address" "array" "number")} <- new {num: "type"}, {5: "literal"}
-+mem: array length is 5
-# skip alloc id in allocation
-+mem: storing 1000 in location 11
-# don't forget the extra locations for alloc id and array length
-+mem: storing 7 in location 30
-
-:(scenario dilated_reagent_with_new)
-def main [
-  10:&:&:num <- new {(& num): type}
-]
-+new: size of '(& num)' is 2
+:(code)
+void test_new() {
+  run(
+      // call 'new' two times with identical types without modifying the
+      // results; you should get back different results
+      "def main [\n"
+      "  10:&:num <- new num:type\n"
+      "  12:&:num <- new num:type\n"
+      "  20:bool <- equal 10:&:num, 12:&:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1000 in location 11\n"
+      "mem: storing 0 in location 20\n"
+  );
+}
+
+void test_new_array() {
+  run(
+      // call 'new' with a second ingredient to allocate an array of some type
+      // rather than a single copy
+      "def main [\n"
+      "  10:&:@:num <- new num:type, 5\n"
+      "  12:&:num <- new num:type\n"
+      "  20:num/alloc2, 21:num/alloc1 <- deaddress 10:&:@:num, 12:&:num\n"
+      "  30:num <- subtract 21:num/alloc2, 20:num/alloc1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: {10: (\"address\" \"array\" \"number\")} <- new {num: \"type\"}, {5: \"literal\"}\n"
+      "mem: array length is 5\n"
+      // skip alloc id in allocation
+      "mem: storing 1000 in location 11\n"
+      // don't forget the extra locations for alloc id and array length
+      "mem: storing 7 in location 30\n"
+  );
+}
+
+void test_dilated_reagent_with_new() {
+  run(
+      "def main [\n"
+      "  10:&:&:num <- new {(& num): type}\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "new: size of '(& num)' is 2\n"
+  );
+}
 
 //: 'new' takes a weird 'type' as its first ingredient; don't error on it
 :(before "End Mu Types Initialization")
@@ -123,6 +140,7 @@ case NEW: {
   }
   break;
 }
+
 :(code)
 bool product_of_new_is_valid(const instruction& inst) {
   reagent/*copy*/ product = inst.products.at(0);
@@ -160,38 +178,59 @@ void drop_from_type(reagent& r, string expected_type) {
   delete tmp;
 }
 
-:(scenario new_returns_incorrect_type)
-% Hide_errors = true;
-def main [
-  1:bool <- new num:type
-]
-+error: main: product of 'new' has incorrect type: '1:bool <- new num:type'
-
-:(scenario new_discerns_singleton_list_from_atom_container)
-% Hide_errors = true;
-def main [
-  1:&:num <- new {(num): type}  # should be '{num: type}'
-]
-+error: main: product of 'new' has incorrect type: '1:&:num <- new {(num): type}'
-
-:(scenario new_with_type_abbreviation)
-def main [
-  1:&:num <- new num:type
-]
-$error: 0
-
-:(scenario new_with_type_abbreviation_inside_compound)
-def main [
-  {1: (address address number), raw: ()} <- new {(& num): type}
-]
-$error: 0
-
-:(scenario equal_result_of_new_with_null)
-def main [
-  1:&:num <- new num:type
-  10:bool <- equal 1:&:num, null
-]
-+mem: storing 0 in location 10
+void test_new_returns_incorrect_type() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:bool <- new num:type\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: product of 'new' has incorrect type: '1:bool <- new num:type'\n"
+  );
+}
+
+void test_new_discerns_singleton_list_from_atom_container() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:&:num <- new {(num): type}\n"  // should be '{num: type}'
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: product of 'new' has incorrect type: '1:&:num <- new {(num): type}'\n"
+  );
+}
+
+void test_new_with_type_abbreviation() {
+  run(
+      "def main [\n"
+      "  1:&:num <- new num:type\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_new_with_type_abbreviation_inside_compound() {
+  run(
+      "def main [\n"
+      "  {1: (address address number), raw: ()} <- new {(& num): type}\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_equal_result_of_new_with_null() {
+  run(
+      "def main [\n"
+      "  1:&:num <- new num:type\n"
+      "  10:bool <- equal 1:&:num, null\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 10\n"
+  );
+}
 
 //: To implement 'new', a Mu transform turns all 'new' instructions into
 //: 'allocate' instructions that precompute the amount of memory they want to
@@ -329,79 +368,114 @@ void ensure_space(int size) {
   }
 }
 
-:(scenario new_initializes)
-% Memory_allocated_until = 10;
-% put(Memory, Memory_allocated_until, 1);
-def main [
-  1:&:num <- new num:type
-]
-+mem: storing 0 in location 10
-+mem: storing 0 in location 11
-+mem: storing 10 in location 2
-
-:(scenario new_initializes_alloc_id)
-% Memory_allocated_until = 10;
-% put(Memory, Memory_allocated_until, 1);
-% Next_alloc_id = 23;
-def main [
-  1:&:num <- new num:type
-]
-# initialize memory
-+mem: storing 0 in location 10
-+mem: storing 0 in location 11
-# alloc-id in payload
-+mem: storing alloc-id 23 in location 10
-# alloc-id in address
-+mem: storing 23 in location 1
-
-:(scenario new_size)
-def main [
-  10:&:num <- new num:type
-  12:&:num <- new num:type
-  20:num/alloc1, 21:num/alloc2 <- deaddress 10:&:num, 12:&:num
-  30:num <- subtract 21:num/alloc2, 20:num/alloc1
-]
-# size of number + alloc id
-+mem: storing 2 in location 30
-
-:(scenario new_array_size)
-def main [
-  10:&:@:num <- new num:type, 5
-  12:&:num <- new num:type
-  20:num/alloc1, 21:num/alloc2 <- deaddress 10:&:num, 12:&:num
-  30:num <- subtract 21:num/alloc2, 20:num/alloc1
-]
-# 5 locations for array contents + array length + alloc id
-+mem: storing 7 in location 30
-
-:(scenario new_empty_array)
-def main [
-  10:&:@:num <- new num:type, 0
-  12:&:num <- new num:type
-  20:num/alloc1, 21:num/alloc2 <- deaddress 10:&:@:num, 12:&:num
-  30:num <- subtract 21:num/alloc2, 20:num/alloc1
-]
-+run: {10: ("address" "array" "number")} <- new {num: "type"}, {0: "literal"}
-+mem: array length is 0
-# one location for array length
-+mem: storing 2 in location 30
+void test_new_initializes() {
+  Memory_allocated_until = 10;
+  put(Memory, Memory_allocated_until, 1);
+  run(
+      "def main [\n"
+      "  1:&:num <- new num:type\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 10\n"
+      "mem: storing 0 in location 11\n"
+      "mem: storing 10 in location 2\n"
+  );
+}
+
+void test_new_initializes_alloc_id() {
+  Memory_allocated_until = 10;
+  put(Memory, Memory_allocated_until, 1);
+  Next_alloc_id = 23;
+  run(
+      "def main [\n"
+      "  1:&:num <- new num:type\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // initialize memory
+      "mem: storing 0 in location 10\n"
+      "mem: storing 0 in location 11\n"
+      // alloc-id in payload
+      "mem: storing alloc-id 23 in location 10\n"
+      // alloc-id in address
+      "mem: storing 23 in location 1\n"
+  );
+}
+
+void test_new_size() {
+  run(
+      "def main [\n"
+      "  10:&:num <- new num:type\n"
+      "  12:&:num <- new num:type\n"
+      "  20:num/alloc1, 21:num/alloc2 <- deaddress 10:&:num, 12:&:num\n"
+      "  30:num <- subtract 21:num/alloc2, 20:num/alloc1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // size of number + alloc id
+      "mem: storing 2 in location 30\n"
+  );
+}
+
+void test_new_array_size() {
+  run(
+      "def main [\n"
+      "  10:&:@:num <- new num:type, 5\n"
+      "  12:&:num <- new num:type\n"
+      "  20:num/alloc1, 21:num/alloc2 <- deaddress 10:&:num, 12:&:num\n"
+      "  30:num <- subtract 21:num/alloc2, 20:num/alloc1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // 5 locations for array contents + array length + alloc id
+      "mem: storing 7 in location 30\n"
+  );
+}
+
+void test_new_empty_array() {
+  run(
+      "def main [\n"
+      "  10:&:@:num <- new num:type, 0\n"
+      "  12:&:num <- new num:type\n"
+      "  20:num/alloc1, 21:num/alloc2 <- deaddress 10:&:@:num, 12:&:num\n"
+      "  30:num <- subtract 21:num/alloc2, 20:num/alloc1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: {10: (\"address\" \"array\" \"number\")} <- new {num: \"type\"}, {0: \"literal\"}\n"
+      "mem: array length is 0\n"
+      // one location for array length and one for alloc id
+      "mem: storing 2 in location 30\n"
+  );
+}
 
 //: If a routine runs out of its initial allocation, it should allocate more.
-:(scenario new_overflow)
-% Initial_memory_per_routine = 3;  // barely enough room for point allocation below
-def main [
-  10:&:num <- new num:type
-  12:&:point <- new point:type  # not enough room in initial page
-]
-+new: routine allocated memory from 1000 to 1003
-+new: routine allocated memory from 1003 to 1006
-
-:(scenario new_without_ingredient)
-% Hide_errors = true;
-def main [
-  1:&:num <- new  # missing ingredient
-]
-+error: main: 'new' requires one or two ingredients, but got '1:&:num <- new'
+void test_new_overflow() {
+  Initial_memory_per_routine = 3;  // barely enough room for point allocation below
+  run(
+      "def main [\n"
+      "  10:&:num <- new num:type\n"
+      "  12:&:point <- new point:type\n"  // not enough room in initial page
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "new: routine allocated memory from 1000 to 1003\n"
+      "new: routine allocated memory from 1003 to 1006\n"
+  );
+}
+
+void test_new_without_ingredient() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:&:num <- new\n"  // missing ingredient
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'new' requires one or two ingredients, but got '1:&:num <- new'\n"
+  );
+}
 
 //: a little helper: convert address to number
 
diff --git a/035lookup.cc b/035lookup.cc
index b7c05201..4229651a 100644
--- a/035lookup.cc
+++ b/035lookup.cc
@@ -3,66 +3,92 @@
 //: The tests in this layer use unsafe operations so as to stay decoupled from
 //: 'new'.
 
-:(scenario copy_indirect)
-def main [
-  # skip alloc id for 10:&:num
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 94
-  # Treat locations 10 and 11 as an address to look up, pointing at the
-  # payload in locations 20 and 21.
-  30:num <- copy 10:&:num/lookup
-]
-+mem: storing 94 in location 30
+void test_copy_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:num
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 94\n"
+      // Treat locations 10 and 11 as an address to look up, pointing at the
+      // payload in locations 20 and 21.
+      "  30:num <- copy 10:&:num/lookup\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 94 in location 30\n"
+  );
+}
 
 :(before "End Preprocess read_memory(x)")
 canonize(x);
 
 //: similarly, write to addresses pointing at other locations using the
 //: 'lookup' property
-:(scenario store_indirect)
-def main [
-  # skip alloc id for 10:&:num
-  11:num <- copy 10
-  10:&:num/lookup <- copy 94
-]
-+mem: storing 94 in location 11
+:(code)
+void test_store_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:num
+      "  11:num <- copy 10\n"
+      "  10:&:num/lookup <- copy 94\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 94 in location 11\n"
+  );
+}
 
 :(before "End Preprocess write_memory(x, data)")
 canonize(x);
 
 //: writes to address 0 always loudly fail
-:(scenario store_to_0_fails)
-% Hide_errors = true;
-def main [
-  10:&:num <- copy null
-  10:&:num/lookup <- copy 94
-]
--mem: storing 94 in location 0
-+error: main: tried to lookup 0 in '10:&:num/lookup <- copy 94'
+:(code)
+void test_store_to_0_fails() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  10:&:num <- copy null\n"
+      "  10:&:num/lookup <- copy 94\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 94 in location 0");
+  CHECK_TRACE_CONTENTS(
+      "error: main: tried to lookup 0 in '10:&:num/lookup <- copy 94'\n"
+  );
+}
 
 //: attempts to /lookup address 0 always loudly fail
-:(scenario lookup_0_fails)
-% Hide_errors = true;
-def main [
-  10:&:num <- copy null
-  20:num <- copy 10:&:num/lookup
-]
-+error: main: tried to lookup 0 in '20:num <- copy 10:&:num/lookup'
-
-:(scenario lookup_0_dumps_callstack)
-% Hide_errors = true;
-def main [
-  foo null
-]
-def foo [
-  10:&:num <- next-input
-  20:num <- copy 10:&:num/lookup
-]
-+error: foo: tried to lookup 0 in '20:num <- copy 10:&:num/lookup'
-+error:   called from main: foo null
+void test_lookup_0_fails() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  10:&:num <- copy null\n"
+      "  20:num <- copy 10:&:num/lookup\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: tried to lookup 0 in '20:num <- copy 10:&:num/lookup'\n"
+  );
+}
+
+void test_lookup_0_dumps_callstack() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  foo null\n"
+      "]\n"
+      "def foo [\n"
+      "  10:&:num <- next-input\n"
+      "  20:num <- copy 10:&:num/lookup\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: tried to lookup 0 in '20:num <- copy 10:&:num/lookup'\n"
+      "error:   called from main: foo null\n"
+  );
+}
 
-:(code)
 void canonize(reagent& x) {
   if (is_literal(x)) return;
   // Begin canonize(x) Lookups
@@ -174,40 +200,55 @@ void drop_one_lookup(reagent& r) {
 //: Most instructions don't require fixup if they use the 'ingredients' and
 //: 'products' variables in run_current_routine().
 
-:(scenario get_indirect)
-def main [
-  # skip alloc id for 10:&:point
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 94
-  22:num <- copy 95
-  30:num <- get 10:&:point/lookup, 0:offset
-]
-+mem: storing 94 in location 30
-
-:(scenario get_indirect2)
-def main [
-  # skip alloc id for 10:&:point
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 94
-  22:num <- copy 95
-  # skip alloc id for destination
-  31:num <- copy 40
-  30:&:num/lookup <- get 10:&:point/lookup, 0:offset
-]
-+mem: storing 94 in location 41
-
-:(scenario include_nonlookup_properties)
-def main [
-  # skip alloc id for 10:&:point
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 94
-  22:num <- copy 95
-  30:num <- get 10:&:point/lookup/foo, 0:offset
-]
-+mem: storing 94 in location 30
+void test_get_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:point
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 94\n"
+      "  22:num <- copy 95\n"
+      "  30:num <- get 10:&:point/lookup, 0:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 94 in location 30\n"
+  );
+}
+
+void test_get_indirect2() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:point
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 94\n"
+      "  22:num <- copy 95\n"
+      // skip alloc id for destination
+      "  31:num <- copy 40\n"
+      "  30:&:num/lookup <- get 10:&:point/lookup, 0:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 94 in location 41\n"
+  );
+}
+
+void test_include_nonlookup_properties() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:point
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 94\n"
+      "  22:num <- copy 95\n"
+      "  30:num <- get 10:&:point/lookup/foo, 0:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 94 in location 30\n"
+  );
+}
 
 :(after "Update GET base in Check")
 if (!canonize_type(base)) break;
@@ -216,16 +257,22 @@ if (!canonize_type(product)) break;
 :(after "Update GET base in Run")
 canonize(base);
 
-:(scenario put_indirect)
-def main [
-  # skip alloc id for 10:&:point
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 94
-  22:num <- copy 95
-  10:&:point/lookup <- put 10:&:point/lookup, 0:offset, 96
-]
-+mem: storing 96 in location 21
+:(code)
+void test_put_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:point
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 94\n"
+      "  22:num <- copy 95\n"
+      "  10:&:point/lookup <- put 10:&:point/lookup, 0:offset, 96\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 96 in location 21\n"
+  );
+}
 
 :(after "Update PUT base in Check")
 if (!canonize_type(base)) break;
@@ -234,17 +281,21 @@ if (!canonize_type(offset)) break;
 :(after "Update PUT base in Run")
 canonize(base);
 
-:(scenario put_product_error_with_lookup)
-% Hide_errors = true;
-def main [
-  # skip alloc id for 10:&:point
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 94
-  22:num <- copy 95
-  10:&:point <- put 10:&:point/lookup, x:offset, 96
-]
-+error: main: product of 'put' must be first ingredient '10:&:point/lookup', but got '10:&:point'
+:(code)
+void test_put_product_error_with_lookup() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  11:num <- copy 20\n"
+      "  21:num <- copy 94\n"
+      "  22:num <- copy 95\n"
+      "  10:&:point <- put 10:&:point/lookup, x:offset, 96\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: product of 'put' must be first ingredient '10:&:point/lookup', but got '10:&:point'\n"
+  );
+}
 
 :(before "End PUT Product Checks")
 reagent/*copy*/ p = inst.products.at(0);
@@ -256,57 +307,80 @@ if (!types_strictly_match(p, i)) {
   break;
 }
 
-:(scenario new_error)
-% Hide_errors = true;
-def main [
-  1:num/raw <- new num:type
-]
-+error: main: product of 'new' has incorrect type: '1:num/raw <- new num:type'
+:(code)
+void test_new_error() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num/raw <- new num:type\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: product of 'new' has incorrect type: '1:num/raw <- new num:type'\n"
+  );
+}
 
 :(after "Update NEW product in Check")
 canonize_type(product);
 
-:(scenario copy_array_indirect)
-def main [
-  # skip alloc id for 10:&:@:num
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 3  # array length
-  22:num <- copy 94
-  23:num <- copy 95
-  24:num <- copy 96
-  30:@:num <- copy 10:&:@:num/lookup
-]
-+mem: storing 3 in location 30
-+mem: storing 94 in location 31
-+mem: storing 95 in location 32
-+mem: storing 96 in location 33
-
-:(scenario create_array_indirect)
-def main [
-  # skip alloc id for 10:&:@:num:3
-  11:num <- copy 3000
-  10:&:array:num:3/lookup <- create-array
-]
-+mem: storing 3 in location 3001
+:(code)
+void test_copy_array_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:@:num
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 3\n"  // array length
+      "  22:num <- copy 94\n"
+      "  23:num <- copy 95\n"
+      "  24:num <- copy 96\n"
+      "  30:@:num <- copy 10:&:@:num/lookup\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 30\n"
+      "mem: storing 94 in location 31\n"
+      "mem: storing 95 in location 32\n"
+      "mem: storing 96 in location 33\n"
+  );
+}
+
+void test_create_array_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:@:num:3
+      "  11:num <- copy 3000\n"
+      "  10:&:array:num:3/lookup <- create-array\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 3001\n"
+  );
+}
 
 :(after "Update CREATE_ARRAY product in Check")
 if (!canonize_type(product)) break;
 :(after "Update CREATE_ARRAY product in Run")
 canonize(product);
 
-:(scenario index_indirect)
-def main [
-  # skip alloc id for 10:&:@:num
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 3  # array length
-  22:num <- copy 94
-  23:num <- copy 95
-  24:num <- copy 96
-  30:num <- index 10:&:@:num/lookup, 1
-]
-+mem: storing 95 in location 30
+:(code)
+void test_index_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:@:num
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 3\n"  // array length
+      "  22:num <- copy 94\n"
+      "  23:num <- copy 95\n"
+      "  24:num <- copy 96\n"
+      "  30:num <- index 10:&:@:num/lookup, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 95 in location 30\n"
+  );
+}
 
 :(before "Update INDEX base in Check")
 if (!canonize_type(base)) break;
@@ -320,46 +394,60 @@ canonize(base);
 :(before "Update INDEX index in Run")
 canonize(index);
 
-:(scenario put_index_indirect)
-def main [
-  # skip alloc id for 10:&:@:num
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 3  # array length
-  22:num <- copy 94
-  23:num <- copy 95
-  24:num <- copy 96
-  10:&:@:num/lookup <- put-index 10:&:@:num/lookup, 1, 97
-]
-+mem: storing 97 in location 23
-
-:(scenario put_index_indirect_2)
-def main [
-  10:num <- copy 3  # array length
-  11:num <- copy 94
-  12:num <- copy 95
-  13:num <- copy 96
-  # skip alloc id for address
-  21:num <- copy 30
-  # skip alloc id for payload
-  31:num <- copy 1  # index
-  10:@:num <- put-index 10:@:num, 20:&:num/lookup, 97
-]
-+mem: storing 97 in location 12
-
-:(scenario put_index_product_error_with_lookup)
-% Hide_errors = true;
-def main [
-  # skip alloc id for 10:&:@:num
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 3  # array length
-  22:num <- copy 94
-  23:num <- copy 95
-  24:num <- copy 96
-  10:&:@:num <- put-index 10:&:@:num/lookup, 1, 34
-]
-+error: main: product of 'put-index' must be first ingredient '10:&:@:num/lookup', but got '10:&:@:num'
+:(code)
+void test_put_index_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:@:num
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 3\n"  // array length
+      "  22:num <- copy 94\n"
+      "  23:num <- copy 95\n"
+      "  24:num <- copy 96\n"
+      "  10:&:@:num/lookup <- put-index 10:&:@:num/lookup, 1, 97\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 97 in location 23\n"
+  );
+}
+
+void test_put_index_indirect_2() {
+  run(
+      "def main [\n"
+      "  10:num <- copy 3\n"  // array length
+      "  11:num <- copy 94\n"
+      "  12:num <- copy 95\n"
+      "  13:num <- copy 96\n"
+      // skip alloc id for address
+      "  21:num <- copy 30\n"
+      // skip alloc id for payload
+      "  31:num <- copy 1\n"  // index
+      "  10:@:num <- put-index 10:@:num, 20:&:num/lookup, 97\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 97 in location 12\n"
+  );
+}
+
+void test_put_index_product_error_with_lookup() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  11:num <- copy 20\n"
+      "  21:num <- copy 3\n"  // array length
+      "  22:num <- copy 94\n"
+      "  23:num <- copy 95\n"
+      "  24:num <- copy 96\n"
+      "  10:&:@:num <- put-index 10:&:@:num/lookup, 1, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: product of 'put-index' must be first ingredient '10:&:@:num/lookup', but got '10:&:@:num'\n"
+  );
+}
 
 :(before "End PUT_INDEX Product Checks")
 reagent/*copy*/ p = inst.products.at(0);
@@ -371,16 +459,22 @@ if (!types_strictly_match(p, i)) {
   break;
 }
 
-:(scenario dilated_reagent_in_static_array)
-def main [
-  {1: (array (& num) 3)} <- create-array
-  10:&:num <- new num:type
-  {1: (array (& num) 3)} <- put-index {1: (array (& num) 3)}, 0, 10:&:num
-  *10:&:num <- copy 94
-  20:num <- copy *10:&:num
-]
-+run: creating array from 7 locations
-+mem: storing 94 in location 20
+:(code)
+void test_dilated_reagent_in_static_array() {
+  run(
+      "def main [\n"
+      "  {1: (array (& num) 3)} <- create-array\n"
+      "  10:&:num <- new num:type\n"
+      "  {1: (array (& num) 3)} <- put-index {1: (array (& num) 3)}, 0, 10:&:num\n"
+      "  *10:&:num <- copy 94\n"
+      "  20:num <- copy *10:&:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: creating array from 7 locations\n"
+      "mem: storing 94 in location 20\n"
+  );
+}
 
 :(before "Update PUT_INDEX base in Check")
 if (!canonize_type(base)) break;
@@ -394,60 +488,82 @@ canonize(base);
 :(before "Update PUT_INDEX index in Run")
 canonize(index);
 
-:(scenario length_indirect)
-def main [
-  # skip alloc id for 10:&:@:num
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 3  # array length
-  22:num <- copy 94
-  23:num <- copy 95
-  24:num <- copy 96
-  30:num <- length 10:&:array:num/lookup
-]
-+mem: storing 3 in location 30
+:(code)
+void test_length_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:@:num
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 3\n"  // array length
+      "  22:num <- copy 94\n"
+      "  23:num <- copy 95\n"
+      "  24:num <- copy 96\n"
+      "  30:num <- length 10:&:array:num/lookup\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 30\n"
+  );
+}
 
 :(before "Update LENGTH array in Check")
 if (!canonize_type(array)) break;
 :(before "Update LENGTH array in Run")
 canonize(array);
 
-:(scenario maybe_convert_indirect)
-def main [
-  # skip alloc id for 10:&:number-or-point
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:number-or-point <- merge 0/number, 94
-  30:num, 31:bool <- maybe-convert 10:&:number-or-point/lookup, i:variant
-]
-+mem: storing 1 in location 31
-+mem: storing 94 in location 30
-
-:(scenario maybe_convert_indirect_2)
-def main [
-  # skip alloc id for 10:&:number-or-point
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:number-or-point <- merge 0/number, 94
-  # skip alloc id for 30:&:num
-  31:num <- copy 40
-  30:&:num/lookup, 50:bool <- maybe-convert 10:&:number-or-point/lookup, i:variant
-]
-+mem: storing 1 in location 50
-+mem: storing 94 in location 41
-
-:(scenario maybe_convert_indirect_3)
-def main [
-  # skip alloc id for 10:&:number-or-point
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:number-or-point <- merge 0/number, 94
-  # skip alloc id for 30:&:bool
-  31:num <- copy 40
-  50:num, 30:&:bool/lookup <- maybe-convert 10:&:number-or-point/lookup, i:variant
-]
-+mem: storing 1 in location 41
-+mem: storing 94 in location 50
+:(code)
+void test_maybe_convert_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:number-or-point
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:number-or-point <- merge 0/number, 94\n"
+      "  30:num, 31:bool <- maybe-convert 10:&:number-or-point/lookup, i:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 31\n"
+      "mem: storing 94 in location 30\n"
+  );
+}
+
+void test_maybe_convert_indirect_2() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:number-or-point
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:number-or-point <- merge 0/number, 94\n"
+      // skip alloc id for 30:&:num
+      "  31:num <- copy 40\n"
+      "  30:&:num/lookup, 50:bool <- maybe-convert 10:&:number-or-point/lookup, i:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 50\n"
+      "mem: storing 94 in location 41\n"
+  );
+}
+
+void test_maybe_convert_indirect_3() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:number-or-point
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:number-or-point <- merge 0/number, 94\n"
+      // skip alloc id for 30:&:bool
+      "  31:num <- copy 40\n"
+      "  50:num, 30:&:bool/lookup <- maybe-convert 10:&:number-or-point/lookup, i:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 41\n"
+      "mem: storing 94 in location 50\n"
+  );
+}
 
 :(before "Update MAYBE_CONVERT base in Check")
 if (!canonize_type(base)) break;
@@ -463,31 +579,43 @@ canonize(product);
 :(before "Update MAYBE_CONVERT status in Run")
 canonize(status);
 
-:(scenario merge_exclusive_container_indirect)
-def main [
-  # skip alloc id for 10:&:number-or-point
-  11:num <- copy 20
-  10:&:number-or-point/lookup <- merge 0/number, 34
-]
-# skip alloc id
-+mem: storing 0 in location 21
-+mem: storing 34 in location 22
+:(code)
+void test_merge_exclusive_container_indirect() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:number-or-point
+      "  11:num <- copy 20\n"
+      "  10:&:number-or-point/lookup <- merge 0/number, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // skip alloc id
+      "mem: storing 0 in location 21\n"
+      "mem: storing 34 in location 22\n"
+  );
+}
 
 :(before "Update size_mismatch Check for MERGE(x)
 canonize(x);
 
 //: abbreviation for '/lookup': a prefix '*'
 
-:(scenario lookup_abbreviation)
-def main [
-  # skip alloc id for 10:&:num
-  11:num <- copy 20
-  # skip alloc id for payload
-  21:num <- copy 94
-  30:num <- copy *10:&:num
-]
-+parse: ingredient: {10: ("&" "num"), "lookup": ()}
-+mem: storing 94 in location 30
+:(code)
+void test_lookup_abbreviation() {
+  run(
+      "def main [\n"
+      // skip alloc id for 10:&:num
+      "  11:num <- copy 20\n"
+      // skip alloc id for payload
+      "  21:num <- copy 94\n"
+      "  30:num <- copy *10:&:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: ingredient: {10: (\"&\" \"num\"), \"lookup\": ()}\n"
+      "mem: storing 94 in location 30\n"
+  );
+}
 
 :(before "End Parsing reagent")
 {
diff --git a/036abandon.cc b/036abandon.cc
index 4dcf572f..a1f66523 100644
--- a/036abandon.cc
+++ b/036abandon.cc
@@ -1,16 +1,21 @@
 //: Reclaiming memory when it's no longer used.
 
-:(scenario new_reclaim)
-def main [
-  10:&:num <- new number:type
-  20:num <- deaddress 10:&:num
-  abandon 10:&:num
-  30:&:num <- new number:type  # must be same size as abandoned memory to reuse
-  40:num <- deaddress 30:&:num
-  50:bool <- equal 20:num, 40:num
-]
-# both allocations should have returned the same address
-+mem: storing 1 in location 50
+void test_new_reclaim() {
+  run(
+      "def main [\n"
+      "  10:&:num <- new number:type\n"
+      "  20:num <- deaddress 10:&:num\n"
+      "  abandon 10:&:num\n"
+      "  30:&:num <- new number:type\n"  // must be same size as abandoned memory to reuse
+      "  40:num <- deaddress 30:&:num\n"
+      "  50:bool <- equal 20:num, 40:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // both allocations should have returned the same address
+      "mem: storing 1 in location 50\n"
+  );
+}
 
 //: When abandoning addresses we'll save them to a 'free list', segregated by size.
 
@@ -97,36 +102,52 @@ if (get_or_insert(Current_routine->free_list, size)) {
   return result;
 }
 
-:(scenario new_differing_size_no_reclaim)
-def main [
-  1:&:num <- new number:type
-  2:num <- deaddress 1:&:num
-  abandon 1:&:num
-  3:&:@:num <- new number:type, 2  # different size
-  4:num <- deaddress 3:&:@:num
-  5:bool <- equal 2:num, 4:num
-]
-# no reuse
-+mem: storing 0 in location 5
+:(code)
+void test_new_differing_size_no_reclaim() {
+  run(
+      "def main [\n"
+      "  1:&:num <- new number:type\n"
+      "  2:num <- deaddress 1:&:num\n"
+      "  abandon 1:&:num\n"
+      "  3:&:@:num <- new number:type, 2\n"  // different size
+      "  4:num <- deaddress 3:&:@:num\n"
+      "  5:bool <- equal 2:num, 4:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // no reuse
+      "mem: storing 0 in location 5\n"
+  );
+}
 
-:(scenario new_reclaim_array)
-def main [
-  10:&:@:num <- new number:type, 2
-  20:num <- deaddress 10:&:@:num
-  abandon 10:&:@:num
-  30:&:@:num <- new number:type, 2  # same size
-  40:num <- deaddress 30:&:@:num
-  50:bool <- equal 20:num, 40:num
-]
-# both calls to new returned identical addresses
-+mem: storing 1 in location 50
+void test_new_reclaim_array() {
+  run(
+      "def main [\n"
+      "  10:&:@:num <- new number:type, 2\n"
+      "  20:num <- deaddress 10:&:@:num\n"
+      "  abandon 10:&:@:num\n"
+      "  30:&:@:num <- new number:type, 2\n"  // same size
+      "  40:num <- deaddress 30:&:@:num\n"
+      "  50:bool <- equal 20:num, 40:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // both calls to new returned identical addresses
+      "mem: storing 1 in location 50\n"
+  );
+}
 
-:(scenario lookup_of_abandoned_address_raises_error)
-% Hide_errors = true;
-def main [
-  1:&:num <- new num:type
-  3:&:num <- copy 1:&:num
-  abandon 1:&:num
-  5:num/raw <- copy *3:&:num
-]
-+error: main: address is already abandoned in '5:num/raw <- copy *3:&:num'
+void test_lookup_of_abandoned_address_raises_error() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:&:num <- new num:type\n"
+      "  3:&:num <- copy 1:&:num\n"
+      "  abandon 1:&:num\n"
+      "  5:num/raw <- copy *3:&:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: address is already abandoned in '5:num/raw <- copy *3:&:num'\n"
+  );
+}
diff --git a/038new_text.cc b/038new_text.cc
index 10b819a3..5317d31d 100644
--- a/038new_text.cc
+++ b/038new_text.cc
@@ -4,23 +4,34 @@
 :(before "End Mu Types Initialization")
 put(Type_abbreviations, "text", new_type_tree("&:@:character"));
 
-:(scenario new_string)
-def main [
-  10:text <- new [abc def]
-  20:char <- index *10:text, 5
-]
-# number code for 'e'
-+mem: storing 101 in location 20
-
-:(scenario new_string_handles_unicode)
-def main [
-  10:text <- new [a«c]
-  20:num <- length *10:text
-  21:char <- index *10:text, 1
-]
-+mem: storing 3 in location 20
-# unicode for '«'
-+mem: storing 171 in location 21
+:(code)
+void test_new_string() {
+  run(
+      "def main [\n"
+      "  10:text <- new [abc def]\n"
+      "  20:char <- index *10:text, 5\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // number code for 'e'
+      "mem: storing 101 in location 20\n"
+  );
+}
+
+void test_new_string_handles_unicode() {
+  run(
+      "def main [\n"
+      "  10:text <- new [a«c]\n"
+      "  20:num <- length *10:text\n"
+      "  21:char <- index *10:text, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 20\n"
+      // unicode for '«'
+      "mem: storing 171 in location 21\n"
+  );
+}
 
 :(before "End NEW Check Special-cases")
 if (is_literal_text(inst.ingredients.at(0))) break;
@@ -64,21 +75,31 @@ int new_mu_text(const string& contents) {
 
 //: a new kind of typo
 
-:(scenario literal_text_without_instruction)
-% Hide_errors = true;
-def main [
-  [abc]
-]
-+error: main: instruction '[abc]' has no recipe in '[abc]'
+void test_literal_text_without_instruction() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  [abc]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: instruction '[abc]' has no recipe in '[abc]'\n"
+  );
+}
 
 //: stash recognizes texts
 
-:(scenario stash_text)
-def main [
-  1:text <- new [abc]
-  stash [foo:], 1:text
-]
-+app: foo: abc
+void test_stash_text() {
+  run(
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+      "  stash [foo:], 1:text\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo: abc\n"
+  );
+}
 
 :(before "End inspect Special-cases(r, data)")
 if (is_mu_text(r)) {
@@ -90,40 +111,62 @@ else if (is_mu_text(current_instruction().ingredients.at(i))) {
   cout << read_mu_text(ingredients.at(i).at(/*skip alloc id*/1));
 }
 
-:(scenario unicode_text)
-def main [
-  1:text <- new [♠]
-  stash [foo:], 1:text
-]
-+app: foo: ♠
+:(code)
+void test_unicode_text() {
+  run(
+      "def main [\n"
+      "  1:text <- new [♠]\n"
+      "  stash [foo:], 1:text\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo: ♠\n"
+  );
+}
 
-:(scenario stash_space_after_text)
-def main [
-  1:text <- new [abc]
-  stash 1:text, [foo]
-]
-+app: abc foo
+void test_stash_space_after_text() {
+  run(
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+      "  stash 1:text, [foo]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: abc foo\n"
+  );
+}
 
-:(scenario stash_text_as_array)
-def main [
-  1:text <- new [abc]
-  stash *1:text
-]
-+app: 3 97 98 99
+void test_stash_text_as_array() {
+  run(
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+      "  stash *1:text\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: 3 97 98 99\n"
+  );
+}
 
 //: fixes way more than just stash
 :(before "End Preprocess is_mu_text(reagent x)")
 if (!canonize_type(x)) return false;
 
 //: Allocate more to routine when initializing a literal text
-:(scenario new_text_overflow)
-% Initial_memory_per_routine = 3;
-def main [
-  10:&:num/raw <- new number:type
-  20:text/raw <- new [a]  # not enough room in initial page, if you take the array length into account
-]
-+new: routine allocated memory from 1000 to 1003
-+new: routine allocated memory from 1003 to 1006
+:(code)
+void test_new_text_overflow() {
+  Initial_memory_per_routine = 3;
+  run(
+      "def main [\n"
+      "  10:&:num/raw <- new number:type\n"
+      "  20:text/raw <- new [a]\n"  // not enough room in initial page, if you take the array length into account
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "new: routine allocated memory from 1000 to 1003\n"
+      "new: routine allocated memory from 1003 to 1006\n"
+  );
+}
 
 //: helpers
 :(code)
@@ -157,20 +200,30 @@ string read_mu_characters(int start, int length) {
 
 //: assert: perform sanity checks at runtime
 
-:(scenario assert_literal)
-% Hide_errors = true;  // '%' lines insert arbitrary C code into tests before calling 'run' with the lines below. Must be immediately after :(scenario) line.
-def main [
-  assert 0, [this is an assert in Mu]
-]
-+error: this is an assert in Mu
-
-:(scenario assert)
-% Hide_errors = true;  // '%' lines insert arbitrary C code into tests before calling 'run' with the lines below. Must be immediately after :(scenario) line.
-def main [
-  1:text <- new [this is an assert in Mu]
-  assert 0, 1:text
-]
-+error: this is an assert in Mu
+void test_assert_literal() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  assert 0, [this is an assert in Mu]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: this is an assert in Mu\n"
+  );
+}
+
+void test_assert() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:text <- new [this is an assert in Mu]\n"
+      "  assert 0, 1:text\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: this is an assert in Mu\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 ASSERT,
diff --git a/040brace.cc b/040brace.cc
index 8eead4ab..89ff7b01 100644
--- a/040brace.cc
+++ b/040brace.cc
@@ -19,17 +19,21 @@
 //: benefits of the control-flow primitives we're used to in other languages,
 //: like 'if', 'while', 'for', etc.
 
-:(scenarios transform)
-:(scenario brace_conversion)
-def main [
-  {
-    break
-    1:num <- copy 0
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: jump 1:offset
-+transform: copy ...
+void test_brace_conversion() {
+  transform(
+      "def main [\n"
+      "  {\n"
+      "    break\n"
+      "    1:num <- copy 0\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: jump 1:offset\n"
+      "transform: copy ...\n"
+  );
+}
 
 :(before "End Instruction Modifying Transforms")
 Transform.push_back(transform_braces);  // idempotent
@@ -143,244 +147,332 @@ int matching_brace(int index, const list<pair<bool, int> >& braces, recipe_ordin
   return SIZE(get(Recipe, r).steps);  // exit current routine
 }
 
-:(scenario loop)
-def main [
-  1:num <- copy 0
-  2:num <- copy 0
-  {
-    3:num <- copy 0
-    loop
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: copy ...
-+transform: copy ...
-+transform: copy ...
-+transform: jump -2:offset
+void test_loop() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "  {\n"
+      "    3:num <- copy 0\n"
+      "    loop\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: copy ...\n"
+      "transform: copy ...\n"
+      "transform: copy ...\n"
+      "transform: jump -2:offset\n"
+  );
+}
 
-:(scenario break_empty_block)
-def main [
-  1:num <- copy 0
-  {
-    break
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: copy ...
-+transform: jump 0:offset
+void test_break_empty_block() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  {\n"
+      "    break\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: copy ...\n"
+      "transform: jump 0:offset\n"
+  );
+}
 
-:(scenario break_cascading)
-def main [
-  1:num <- copy 0
-  {
-    break
-  }
-  {
-    break
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: copy ...
-+transform: jump 0:offset
-+transform: jump 0:offset
+void test_break_cascading() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  {\n"
+      "    break\n"
+      "  }\n"
+      "  {\n"
+      "    break\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: copy ...\n"
+      "transform: jump 0:offset\n"
+      "transform: jump 0:offset\n"
+  );
+}
 
-:(scenario break_cascading_2)
-def main [
-  1:num <- copy 0
-  2:num <- copy 0
-  {
-    break
-    3:num <- copy 0
-  }
-  {
-    break
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: copy ...
-+transform: copy ...
-+transform: jump 1:offset
-+transform: copy ...
-+transform: jump 0:offset
+void test_break_cascading_2() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "  {\n"
+      "    break\n"
+      "    3:num <- copy 0\n"
+      "  }\n"
+      "  {\n"
+      "    break\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: copy ...\n"
+      "transform: copy ...\n"
+      "transform: jump 1:offset\n"
+      "transform: copy ...\n"
+      "transform: jump 0:offset\n"
+  );
+}
 
-:(scenario break_if)
-def main [
-  1:num <- copy 0
-  2:num <- copy 0
-  {
-    break-if 2:num
-    3:num <- copy 0
-  }
-  {
-    break
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: copy ...
-+transform: copy ...
-+transform: jump-if 2, 1:offset
-+transform: copy ...
-+transform: jump 0:offset
+void test_break_if() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "  {\n"
+      "    break-if 2:num\n"
+      "    3:num <- copy 0\n"
+      "  }\n"
+      "  {\n"
+      "    break\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: copy ...\n"
+      "transform: copy ...\n"
+      "transform: jump-if 2, 1:offset\n"
+      "transform: copy ...\n"
+      "transform: jump 0:offset\n"
+  );
+}
 
-:(scenario break_nested)
-def main [
-  1:num <- copy 0
-  {
-    2:num <- copy 0
-    break
-    {
-      3:num <- copy 0
-    }
-    4:num <- copy 0
-  }
-]
-+transform: jump 4:offset
+void test_break_nested() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  {\n"
+      "    2:num <- copy 0\n"
+      "    break\n"
+      "    {\n"
+      "      3:num <- copy 0\n"
+      "    }\n"
+      "    4:num <- copy 0\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: jump 4:offset\n"
+  );
+}
 
-:(scenario break_nested_degenerate)
-def main [
-  1:num <- copy 0
-  {
-    2:num <- copy 0
-    break
-    {
-    }
-    4:num <- copy 0
-  }
-]
-+transform: jump 3:offset
+void test_break_nested_degenerate() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  {\n"
+      "    2:num <- copy 0\n"
+      "    break\n"
+      "    {\n"
+      "    }\n"
+      "    4:num <- copy 0\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: jump 3:offset\n"
+  );
+}
 
-:(scenario break_nested_degenerate_2)
-def main [
-  1:num <- copy 0
-  {
-    2:num <- copy 0
-    break
-    {
-    }
-  }
-]
-+transform: jump 2:offset
+void test_break_nested_degenerate_2() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  {\n"
+      "    2:num <- copy 0\n"
+      "    break\n"
+      "    {\n"
+      "    }\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: jump 2:offset\n"
+  );
+}
 
-:(scenario break_label)
-% Hide_errors = true;
-def main [
-  1:num <- copy 0
-  {
-    break +foo:offset
-  }
-]
-+transform: jump +foo:offset
+void test_break_label() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  {\n"
+      "    break +foo:offset\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: jump +foo:offset\n"
+  );
+}
 
-:(scenario break_unless)
-def main [
-  1:num <- copy 0
-  2:num <- copy 0
-  {
-    break-unless 2:num
-    3:num <- copy 0
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: copy ...
-+transform: copy ...
-+transform: jump-unless 2, 1:offset
-+transform: copy ...
+void test_break_unless() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "  {\n"
+      "    break-unless 2:num\n"
+      "    3:num <- copy 0\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: copy ...\n"
+      "transform: copy ...\n"
+      "transform: jump-unless 2, 1:offset\n"
+      "transform: copy ...\n"
+  );
+}
 
-:(scenario loop_unless)
-def main [
-  1:num <- copy 0
-  2:num <- copy 0
-  {
-    loop-unless 2:num
-    3:num <- copy 0
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: copy ...
-+transform: copy ...
-+transform: jump-unless 2, -1:offset
-+transform: copy ...
+void test_loop_unless() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "  {\n"
+      "    loop-unless 2:num\n"
+      "    3:num <- copy 0\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: copy ...\n"
+      "transform: copy ...\n"
+      "transform: jump-unless 2, -1:offset\n"
+      "transform: copy ...\n"
+  );
+}
 
-:(scenario loop_nested)
-def main [
-  1:num <- copy 0
-  {
-    2:num <- copy 0
-    {
-      3:num <- copy 0
-    }
-    loop-if 4:bool
-    5:num <- copy 0
-  }
-]
-+transform: --- transform braces for recipe main
-+transform: jump-if 4, -5:offset
+void test_loop_nested() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  {\n"
+      "    2:num <- copy 0\n"
+      "    {\n"
+      "      3:num <- copy 0\n"
+      "    }\n"
+      "    loop-if 4:bool\n"
+      "    5:num <- copy 0\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: jump-if 4, -5:offset\n"
+  );
+}
 
-:(scenario loop_label)
-def main [
-  1:num <- copy 0
-  +foo
-  2:num <- copy 0
-]
-+transform: --- transform braces for recipe main
-+transform: copy ...
-+transform: copy ...
+void test_loop_label() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  +foo\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: --- transform braces for recipe main\n"
+      "transform: copy ...\n"
+      "transform: copy ...\n"
+  );
+}
 
 //: test how things actually run
-:(scenarios run)
-:(scenario brace_conversion_and_run)
-def test-factorial [
-  1:num <- copy 5
-  2:num <- copy 1
-  {
-    3:bool <- equal 1:num, 1
-    break-if 3:bool
-#    $print 1:num
-    2:num <- multiply 2:num, 1:num
-    1:num <- subtract 1:num, 1
-    loop
-  }
-  4:num <- copy 2:num  # trigger a read
-]
-+mem: location 2 is 120
+void test_brace_conversion_and_run() {
+  run(
+      "def test-factorial [\n"
+      "  1:num <- copy 5\n"
+      "  2:num <- copy 1\n"
+      "  {\n"
+      "    3:bool <- equal 1:num, 1\n"
+      "    break-if 3:bool\n"
+      "    2:num <- multiply 2:num, 1:num\n"
+      "    1:num <- subtract 1:num, 1\n"
+      "    loop\n"
+      "  }\n"
+      "  4:num <- copy 2:num\n"  // trigger a read
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: location 2 is 120\n"
+  );
+}
 
-:(scenario break_outside_braces_fails)
-% Hide_errors = true;
-def main [
-  break
-]
-+error: main: 'break' needs a '{' before
+void test_break_outside_braces_fails() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  break\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'break' needs a '{' before\n"
+  );
+}
 
-:(scenario break_conditional_without_ingredient_fails)
-% Hide_errors = true;
-def main [
-  {
-    break-if
-  }
-]
-+error: main: 'break-if' expects 1 or 2 ingredients, but got none
+void test_break_conditional_without_ingredient_fails() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  {\n"
+      "    break-if\n"
+      "  }\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'break-if' expects 1 or 2 ingredients, but got none\n"
+  );
+}
 
 //: Using break we can now implement conditional returns.
 
-:(scenario return_if)
-def main [
-  1:num <- test1
-]
-def test1 [
-  return-if 0, 34
-  return 35
-]
-+mem: storing 35 in location 1
+void test_return_if() {
+  run(
+      "def main [\n"
+      "  1:num <- test1\n"
+      "]\n"
+      "def test1 [\n"
+      "  return-if 0, 34\n"
+      "  return 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 35 in location 1\n"
+  );
+}
 
-:(scenario return_if_2)
-def main [
-  1:num <- test1
-]
-def test1 [
-  return-if 1, 34
-  return 35
-]
-+mem: storing 34 in location 1
+void test_return_if_2() {
+  run(
+      "def main [\n"
+      "  1:num <- test1\n"
+      "]\n"
+      "def test1 [\n"
+      "  return-if 1, 34\n"
+      "  return 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
 :(before "End Rewrite Instruction(curr, recipe result)")
 // rewrite 'return-if a, b, c, ...' to
diff --git a/041jump_target.cc b/041jump_target.cc
index d8eaf68a..ec7adede 100644
--- a/041jump_target.cc
+++ b/041jump_target.cc
@@ -14,13 +14,16 @@ bool is_jump_target(const string& label) {
   return is_label_word(label);
 }
 
-:(scenario jump_to_label)
-def main [
-  jump +target:label
-  1:num <- copy 0
-  +target
-]
--mem: storing 0 in location 1
+void test_jump_to_label() {
+  run(
+      "def main [\n"
+      "  jump +target:label\n"
+      "  1:num <- copy 0\n"
+      "  +target\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1");
+}
 
 :(before "End Mu Types Initialization")
 put(Type_ordinal, "label", 0);
@@ -94,90 +97,124 @@ void replace_offset(reagent& x, /*const*/ map<string, int>& offset, const int cu
   x.set_value(get(offset, x.name) - current_offset);
 }
 
-:(scenario break_to_label)
-def main [
-  {
-    {
-      break +target:label
-      1:num <- copy 0
-    }
-  }
-  +target
-]
--mem: storing 0 in location 1
+void test_break_to_label() {
+  run(
+      "def main [\n"
+      "  {\n"
+      "    {\n"
+      "      break +target:label\n"
+      "      1:num <- copy 0\n"
+      "    }\n"
+      "  }\n"
+      "  +target\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1");
+}
 
-:(scenario jump_if_to_label)
-def main [
-  {
-    {
-      jump-if 1, +target:label
-      1:num <- copy 0
-    }
-  }
-  +target
-]
--mem: storing 0 in location 1
+void test_jump_if_to_label() {
+  run(
+      "def main [\n"
+      "  {\n"
+      "    {\n"
+      "      jump-if 1, +target:label\n"
+      "      1:num <- copy 0\n"
+      "    }\n"
+      "  }\n"
+      "  +target\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1");
+}
 
-:(scenario loop_unless_to_label)
-def main [
-  {
-    {
-      loop-unless 0, +target:label  # loop/break with a label don't care about braces
-      1:num <- copy 0
-    }
-  }
-  +target
-]
--mem: storing 0 in location 1
+void test_loop_unless_to_label() {
+  run(
+      "def main [\n"
+      "  {\n"
+      "    {\n"
+      "      loop-unless 0, +target:label\n"  // loop/break with a label don't care about braces
+      "      1:num <- copy 0\n"
+      "    }\n"
+      "  }\n"
+      "  +target\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1");
+}
 
-:(scenario jump_runs_code_after_label)
-def main [
-  # first a few lines of padding to exercise the offset computation
-  1:num <- copy 0
-  2:num <- copy 0
-  3:num <- copy 0
-  jump +target:label
-  4:num <- copy 0
-  +target
-  5:num <- copy 0
-]
-+mem: storing 0 in location 5
--mem: storing 0 in location 4
+void test_jump_runs_code_after_label() {
+  run(
+      "def main [\n"
+      // first a few lines of padding to exercise the offset computation
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "  3:num <- copy 0\n"
+      "  jump +target:label\n"
+      "  4:num <- copy 0\n"
+      "  +target\n"
+      "  5:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 5\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 4");
+}
 
-:(scenario jump_fails_without_target)
-% Hide_errors = true;
-def main [
-  jump
-]
-+error: main: 'jump' expects an ingredient but got 0
+void test_jump_fails_without_target() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  jump\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'jump' expects an ingredient but got 0\n"
+  );
+}
 
-:(scenario jump_fails_without_target_2)
-% Hide_errors = true;
-def main [
-  jump-if true
-]
-+error: main: 'jump-if true' expects 2 ingredients but got 1
+void test_jump_fails_without_target_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  jump-if true\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'jump-if true' expects 2 ingredients but got 1\n"
+  );
+}
 
-:(scenario recipe_fails_on_duplicate_jump_target)
-% Hide_errors = true;
-def main [
-  +label
-  1:num <- copy 0
-  +label
-  2:num <- copy 0
-]
-+error: main: duplicate label '+label'
+void test_recipe_fails_on_duplicate_jump_target() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  +label\n"
+      "  1:num <- copy 0\n"
+      "  +label\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: duplicate label '+label'\n"
+  );
+}
 
-:(scenario jump_ignores_nontarget_label)
-% Hide_errors = true;
-def main [
-  # first a few lines of padding to exercise the offset computation
-  1:num <- copy 0
-  2:num <- copy 0
-  3:num <- copy 0
-  jump $target:label
-  4:num <- copy 0
-  $target
-  5:num <- copy 0
-]
-+error: main: can't jump to label '$target'
+void test_jump_ignores_nontarget_label() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      // first a few lines of padding to exercise the offset computation
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "  3:num <- copy 0\n"
+      "  jump $target:label\n"
+      "  4:num <- copy 0\n"
+      "  $target\n"
+      "  5:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: can't jump to label '$target'\n"
+  );
+}
diff --git a/042name.cc b/042name.cc
index 5846b5b4..557469d0 100644
--- a/042name.cc
+++ b/042name.cc
@@ -2,21 +2,31 @@
 //: locations. In Mu, a transform called 'transform_names' provides this
 //: convenience.
 
-:(scenario transform_names)
-def main [
-  x:num <- copy 0
-]
-+name: assign x 2
-+mem: storing 0 in location 2
-
-:(scenarios transform)
-:(scenario transform_names_fails_on_use_before_define)
-% Hide_errors = true;
-def main [
-  x:num <- copy y:num
-]
-+error: main: tried to read ingredient 'y' in 'x:num <- copy y:num' but it hasn't been written to yet
-# todo: detect conditional defines
+void test_transform_names() {
+  run(
+      "def main [\n"
+      "  x:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: assign x 2\n"
+      "mem: storing 0 in location 2\n"
+  );
+}
+
+void test_transform_names_fails_on_use_before_define() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  x:num <- copy y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: tried to read ingredient 'y' in 'x:num <- copy y:num' but it hasn't been written to yet\n"
+  );
+}
+
+// todo: detect conditional defines
 
 :(after "End Type Modifying Transforms")
 Transform.push_back(transform_names);  // idempotent
@@ -180,71 +190,105 @@ bool is_raw(const reagent& r) {
   return has_property(r, "raw");
 }
 
-:(scenario transform_names_supports_containers)
-def main [
-  x:point <- merge 34, 35
-  y:num <- copy 3
-]
-+name: assign x 2
-# skip location 2 because x occupies two locations
-+name: assign y 4
-
-:(scenario transform_names_supports_static_arrays)
-def main [
-  x:@:num:3 <- create-array
-  y:num <- copy 3
-]
-+name: assign x 2
-# skip locations 2, 3, 4 because x occupies four locations
-+name: assign y 6
-
-:(scenario transform_names_passes_dummy)
-# _ is just a dummy result that never gets consumed
-def main [
-  _, x:num <- copy 0, 1
-]
-+name: assign x 2
--name: assign _ 2
+void test_transform_names_supports_containers() {
+  transform(
+      "def main [\n"
+      "  x:point <- merge 34, 35\n"
+      "  y:num <- copy 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: assign x 2\n"
+      // skip location 3 because x occupies two locations
+      "name: assign y 4\n"
+  );
+}
+
+void test_transform_names_supports_static_arrays() {
+  transform(
+      "def main [\n"
+      "  x:@:num:3 <- create-array\n"
+      "  y:num <- copy 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: assign x 2\n"
+      // skip locations 3, 4, 5 because x occupies four locations
+      "name: assign y 6\n"
+  );
+}
+
+void test_transform_names_passes_dummy() {
+  transform(
+      "def main [\n"
+      // _ is just a dummy result that never gets consumed
+      "  _, x:num <- copy 0, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: assign x 2\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("name: assign _ 2");
+}
 
 //: an escape hatch to suppress name conversion that we'll use later
-:(scenarios run)
-:(scenario transform_names_passes_raw)
-% Hide_errors = true;
-def main [
-  x:num/raw <- copy 0
-]
--name: assign x 2
-+error: can't write to location 0 in 'x:num/raw <- copy 0'
-
-:(scenarios transform)
-:(scenario transform_names_fails_when_mixing_names_and_numeric_locations)
-% Hide_errors = true;
-def main [
-  x:num <- copy 1:num
-]
-+error: main: mixing variable names and numeric addresses
-
-:(scenario transform_names_fails_when_mixing_names_and_numeric_locations_2)
-% Hide_errors = true;
-def main [
-  x:num <- copy 1
-  1:num <- copy x:num
-]
-+error: main: mixing variable names and numeric addresses
-
-:(scenario transform_names_does_not_fail_when_mixing_names_and_raw_locations)
-def main [
-  x:num <- copy 1:num/raw
-]
--error: main: mixing variable names and numeric addresses
-$error: 0
-
-:(scenario transform_names_does_not_fail_when_mixing_names_and_literals)
-def main [
-  x:num <- copy 1
-]
--error: main: mixing variable names and numeric addresses
-$error: 0
+void test_transform_names_passes_raw() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  x:num/raw <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("name: assign x 2");
+  CHECK_TRACE_CONTENTS(
+      "error: can't write to location 0 in 'x:num/raw <- copy 0'\n"
+  );
+}
+
+void test_transform_names_fails_when_mixing_names_and_numeric_locations() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  x:num <- copy 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: mixing variable names and numeric addresses\n"
+  );
+}
+
+void test_transform_names_fails_when_mixing_names_and_numeric_locations_2() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  x:num <- copy 1\n"
+      "  1:num <- copy x:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: mixing variable names and numeric addresses\n"
+  );
+}
+
+void test_transform_names_does_not_fail_when_mixing_names_and_raw_locations() {
+  transform(
+      "def main [\n"
+      "  x:num <- copy 1:num/raw\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("error: main: mixing variable names and numeric addresses");
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_transform_names_does_not_fail_when_mixing_names_and_literals() {
+  transform(
+      "def main [\n"
+      "  x:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("error: main: mixing variable names and numeric addresses");
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 //:: Support element names for containers in 'get' and 'get-location' and 'put'.
 //: (get-location is implemented later)
@@ -258,14 +302,20 @@ else {
   offset_value = offset.value;
 }
 
-:(scenario transform_names_transforms_container_elements)
-def main [
-  p:&:point <- copy null
-  a:num <- get *p:&:point, y:offset
-  b:num <- get *p:&:point, x:offset
-]
-+name: element y of type point is at offset 1
-+name: element x of type point is at offset 0
+:(code)
+void test_transform_names_transforms_container_elements() {
+  transform(
+      "def main [\n"
+      "  p:&:point <- copy null\n"
+      "  a:num <- get *p:&:point, y:offset\n"
+      "  b:num <- get *p:&:point, x:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: element y of type point is at offset 1\n"
+      "name: element x of type point is at offset 0\n"
+  );
+}
 
 :(before "End transform_names(inst) Special-cases")
 // replace element names of containers with offsets
@@ -287,35 +337,50 @@ if (inst.name == "get" || inst.name == "get-location" || inst.name == "put") {
   }
 }
 
-:(scenario missing_type_in_get)
-% Hide_errors = true;
-def main [
-  get a, x:offset
-]
-+error: main: missing type for 'a' in 'get a, x:offset'
-
-:(scenario transform_names_handles_containers)
-def main [
-  a:point <- merge 0, 0
-  b:num <- copy 0
-]
-+name: assign a 2
-+name: assign b 4
+:(code)
+void test_missing_type_in_get() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  get a, x:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: missing type for 'a' in 'get a, x:offset'\n"
+  );
+}
+
+void test_transform_names_handles_containers() {
+  transform(
+      "def main [\n"
+      "  a:point <- merge 0, 0\n"
+      "  b:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: assign a 2\n"
+      "name: assign b 4\n"
+  );
+}
 
 //:: Support variant names for exclusive containers in 'maybe-convert'.
 
-:(scenarios run)
-:(scenario transform_names_handles_exclusive_containers)
-def main [
-  12:num <- copy 1
-  13:num <- copy 35
-  14:num <- copy 36
-  20:point, 22:bool <- maybe-convert 12:number-or-point/unsafe, p:variant
-]
-+name: variant p of type number-or-point has tag 1
-+mem: storing 1 in location 22
-+mem: storing 35 in location 20
-+mem: storing 36 in location 21
+void test_transform_names_handles_exclusive_containers() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 1\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  20:point, 22:bool <- maybe-convert 12:number-or-point/unsafe, p:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: variant p of type number-or-point has tag 1\n"
+      "mem: storing 1 in location 22\n"
+      "mem: storing 35 in location 20\n"
+      "mem: storing 36 in location 21\n"
+  );
+}
 
 :(before "End transform_names(inst) Special-cases")
 // convert variant names of exclusive containers
@@ -335,9 +400,15 @@ if (inst.name == "maybe-convert") {
   }
 }
 
-:(scenario missing_type_in_maybe_convert)
-% Hide_errors = true;
-def main [
-  maybe-convert a, x:variant
-]
-+error: main: missing type for 'a' in 'maybe-convert a, x:variant'
+:(code)
+void test_missing_type_in_maybe_convert() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  maybe-convert a, x:variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: missing type for 'a' in 'maybe-convert a, x:variant'\n"
+  );
+}
diff --git a/043space.cc b/043space.cc
index 6028ef0d..bd931701 100644
--- a/043space.cc
+++ b/043space.cc
@@ -16,56 +16,70 @@
 :(before "End Mu Types Initialization")
 put(Type_abbreviations, "space", new_type_tree("address:array:location"));
 
-:(scenario set_default_space)
-def main [
-  # prepare default-space address
-  10:num/alloc-id, 11:num <- copy 0, 1000
-  # prepare default-space payload
-  1000:num <- copy 0  # alloc id of payload
-  1001:num <- copy 5  # length
-  # actual start of this recipe
-  default-space:space <- copy 10:&:@:location
-  # if default-space is 1000, then:
-  #   1000: alloc id
-  #   1001: array size
-  #   1002: location 0 (space for the chaining slot; described later; often unused)
-  #   1003: location 1 (space for the chaining slot; described later; often unused)
-  #   1004: local 2 (assuming it is a scalar)
-  2:num <- copy 93
-]
-+mem: storing 93 in location 1004
+:(code)
+void test_set_default_space() {
+  run(
+      "def main [\n"
+         // prepare default-space address
+      "  10:num/alloc-id, 11:num <- copy 0, 1000\n"
+         // prepare default-space payload
+      "  1000:num <- copy 0\n"  // alloc id of payload
+      "  1001:num <- copy 5\n"  // length
+         // actual start of this recipe
+      "  default-space:space <- copy 10:&:@:location\n"
+         // if default-space is 1000, then:
+         //   1000: alloc id
+         //   1001: array size
+         //   1002: location 0 (space for the chaining slot; described later; often unused)
+         //   1003: location 1 (space for the chaining slot; described later; often unused)
+         //   1004: local 2 (assuming it is a scalar)
+      "  2:num <- copy 93\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 93 in location 1004\n"
+  );
+}
 
-:(scenario lookup_sidesteps_default_space)
-def main [
-  # prepare default-space address
-  10:num/alloc-id, 11:num <- copy 0, 1000
-  # prepare default-space payload
-  1000:num <- copy 0  # alloc id of payload
-  1001:num <- copy 5  # length
-  # prepare payload outside the local scope
-  2000:num/alloc-id, 2001:num <- copy 0, 34
-  # actual start of this recipe
-  default-space:space <- copy 10:&:@:location
-  # a local address
-  2:num, 3:num <- copy 0, 2000
-  20:num/raw <- copy *2:&:num
-]
-+mem: storing 2000 in location 1005
-+mem: storing 34 in location 20
+void test_lookup_sidesteps_default_space() {
+  run(
+      "def main [\n"
+         // prepare default-space address
+      "  10:num/alloc-id, 11:num <- copy 0, 1000\n"
+         // prepare default-space payload
+      "  1000:num <- copy 0\n"  // alloc id of payload
+      "  1001:num <- copy 5\n"  // length
+         // prepare payload outside the local scope
+      "  2000:num/alloc-id, 2001:num <- copy 0, 34\n"
+         // actual start of this recipe
+      "  default-space:space <- copy 10:&:@:location\n"
+         // a local address
+      "  2:num, 3:num <- copy 0, 2000\n"
+      "  20:num/raw <- copy *2:&:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2000 in location 1005\n"
+      "mem: storing 34 in location 20\n"
+  );
+}
 
 //: precondition: disable name conversion for 'default-space'
 
-:(scenarios transform)
-:(scenario convert_names_passes_default_space)
-% Hide_errors = true;
-def main [
-  default-space:num <- copy 0
-  x:num <- copy 1
-]
-+name: assign x 2
--name: assign default-space 1
--name: assign default-space 2
-:(scenarios run)
+void test_convert_names_passes_default_space() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  default-space:num <- copy 0\n"
+      "  x:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: assign x 2\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("name: assign default-space 1");
+  CHECK_TRACE_DOESNT_CONTAIN("name: assign default-space 2");
+}
 
 :(before "End is_disqualified Special-cases")
 if (x.name == "default-space")
@@ -133,18 +147,23 @@ bool is_mu_space(reagent/*copy*/ x) {
   return x.type && x.type->atom && x.type->name == "location";
 }
 
-:(scenario get_default_space)
-def main [
-  # prepare default-space address
-  10:num/alloc-id, 11:num <- copy 0, 1000
-  # prepare default-space payload
-  1000:num <- copy 0  # alloc id of payload
-  1001:num <- copy 5  # length
-  # actual start of this recipe
-  default-space:space <- copy 10:space
-  2:space/raw <- copy default-space:space
-]
-+mem: storing 1000 in location 3
+void test_get_default_space() {
+  run(
+      "def main [\n"
+         // prepare default-space address
+      "  10:num/alloc-id, 11:num <- copy 0, 1000\n"
+         // prepare default-space payload
+      "  1000:num <- copy 0\n"  // alloc id of payload
+      "  1001:num <- copy 5\n"  // length
+         // actual start of this recipe
+      "  default-space:space <- copy 10:space\n"
+      "  2:space/raw <- copy default-space:space\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1000 in location 3\n"
+  );
+}
 
 :(after "Begin Preprocess read_memory(x)")
 if (x.name == "default-space") {
@@ -156,45 +175,57 @@ if (x.name == "default-space") {
 
 //:: fix 'get'
 
-:(scenario lookup_sidesteps_default_space_in_get)
-def main [
-  # prepare default-space address
-  10:num/alloc-id, 11:num <- copy 0, 1000
-  # prepare default-space payload
-  1000:num <- copy 0  # alloc id of payload
-  1001:num <- copy 5  # length
-  # prepare payload outside the local scope
-  2000:num/alloc-id, 2001:num/x, 2002:num/y <- copy 0, 34, 35
-  # actual start of this recipe
-  default-space:space <- copy 10:space
-  # a local address
-  2:num, 3:num <- copy 0, 2000
-  3000:num/raw <- get *2:&:point, 1:offset
-]
-+mem: storing 35 in location 3000
+:(code)
+void test_lookup_sidesteps_default_space_in_get() {
+  run(
+      "def main [\n"
+         // prepare default-space address
+      "  10:num/alloc-id, 11:num <- copy 0, 1000\n"
+         // prepare default-space payload
+      "  1000:num <- copy 0\n"  // alloc id of payload
+      "  1001:num <- copy 5\n"  // length
+         // prepare payload outside the local scope
+      "  2000:num/alloc-id, 2001:num/x, 2002:num/y <- copy 0, 34, 35\n"
+         // actual start of this recipe
+      "  default-space:space <- copy 10:space\n"
+         // a local address
+      "  2:num, 3:num <- copy 0, 2000\n"
+      "  3000:num/raw <- get *2:&:point, 1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 35 in location 3000\n"
+  );
+}
 
 :(before "Read element" following "case GET:")
 element.properties.push_back(pair<string, string_tree*>("raw", NULL));
 
 //:: fix 'index'
 
-:(scenario lookup_sidesteps_default_space_in_index)
-def main [
-  # prepare default-space address
-  10:num/alloc-id, 11:num <- copy 0, 1000
-  # prepare default-space payload
-  1000:num <- copy 0  # alloc id of payload
-  1001:num <- copy 5  # length
-  # prepare an array address
-  20:num/alloc-id, 21:num <- copy 0, 2000
-  # prepare an array payload
-  2000:num/alloc-id, 2001:num/length, 2002:num/index:0, 2003:num/index:1 <- copy 0, 2, 34, 35
-  # actual start of this recipe
-  default-space:space <- copy 10:&:@:location
-  1:&:@:num <- copy 20:&:@:num/raw
-  3000:num/raw <- index *1:&:@:num, 1
-]
-+mem: storing 35 in location 3000
+:(code)
+void test_lookup_sidesteps_default_space_in_index() {
+  run(
+      "def main [\n"
+         // prepare default-space address
+      "  10:num/alloc-id, 11:num <- copy 0, 1000\n"
+         // prepare default-space payload
+      "  1000:num <- copy 0\n"  // alloc id of payload
+      "  1001:num <- copy 5\n"  // length
+         // prepare an array address
+      "  20:num/alloc-id, 21:num <- copy 0, 2000\n"
+         // prepare an array payload
+      "  2000:num/alloc-id, 2001:num/length, 2002:num/index:0, 2003:num/index:1 <- copy 0, 2, 34, 35\n"
+         // actual start of this recipe
+      "  default-space:space <- copy 10:&:@:location\n"
+      "  1:&:@:num <- copy 20:&:@:num/raw\n"
+      "  3000:num/raw <- index *1:&:@:num, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 35 in location 3000\n"
+  );
+}
 
 :(before "Read element" following "case INDEX:")
 element.properties.push_back(pair<string, string_tree*>("raw", NULL));
@@ -202,14 +233,20 @@ element.properties.push_back(pair<string, string_tree*>("raw", NULL));
 //:: 'local-scope' is a convenience operation to automatically deduce
 //:: the amount of space to allocate in a default space with names
 
-:(scenario local_scope)
-def main [
-  local-scope
-  x:num <- copy 0
-  y:num <- copy 3
-]
-# allocate space for x and y, as well as the chaining slot at indices 0 and 1
-+mem: array length is 4
+:(code)
+void test_local_scope() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:num <- copy 0\n"
+      "  y:num <- copy 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // allocate space for x and y, as well as the chaining slot at indices 0 and 1
+      "mem: array length is 4\n"
+  );
+}
 
 :(before "End is_disqualified Special-cases")
 if (x.name == "number-of-locals")
diff --git a/044space_surround.cc b/044space_surround.cc
index 9957630d..5a4afb5e 100644
--- a/044space_surround.cc
+++ b/044space_surround.cc
@@ -4,36 +4,41 @@
 //:
 //: (Surrounding spaces are like lexical scopes in other languages.)
 
-:(scenario surrounding_space)
-# location 2 in space 1 (remember that locations 0 and 1 are reserved in all
-# spaces) refers to the space surrounding the default space, here 20.
-def main [
-  # prepare default-space address
-  10:num/alloc-id, 11:num <- copy 0, 1000
-  # prepare default-space payload
-  1000:num <- copy 0  # alloc id of payload
-  1001:num <- copy 5  # length
-  # prepare address of chained space
-  20:num/alloc-id, 21:num <- copy 0, 2000
-  # prepare payload of chained space
-  2000:num <- copy 0  # alloc id of payload
-  2001:num <- copy 5  # length
-  # actual start of this recipe
-  default-space:space <- copy 10:space
-  #: later layers will explain the /names: property
-  0:space/names:dummy <- copy 20:space/raw
-  2:num <- copy 94
-  2:num/space:1 <- copy 95
-]
-def dummy [  # just for the /names: property above
-]
-# chain space: 1000 + (alloc id) 1 + (length) 1
-+mem: storing 0 in location 1002
-+mem: storing 2000 in location 1003
-# store to default space: 1000 + (alloc id) 1 + (length) 1 + (index) 2
-+mem: storing 94 in location 1004
-# store to chained space: (contents of location 1003) 2000 + (alloc id) 1 + (length) 1 + (index) 2
-+mem: storing 95 in location 2004
+void test_surrounding_space() {
+  run(
+      // location 2 in space 1 (remember that locations 0 and 1 are reserved in all
+      // spaces) refers to the space surrounding the default space, here 20.
+      "def main [\n"
+         // prepare default-space address
+      "  10:num/alloc-id, 11:num <- copy 0, 1000\n"
+         // prepare default-space payload
+      "  1000:num <- copy 0\n"  // alloc id of payload
+      "  1001:num <- copy 5\n"  // length
+         // prepare address of chained space
+      "  20:num/alloc-id, 21:num <- copy 0, 2000\n"
+         // prepare payload of chained space
+      "  2000:num <- copy 0\n"  // alloc id of payload
+      "  2001:num <- copy 5\n"  // length
+         // actual start of this recipe
+      "  default-space:space <- copy 10:space\n"
+         // later layers will explain the /names: property
+      "  0:space/names:dummy <- copy 20:space/raw\n"
+      "  2:num <- copy 94\n"
+      "  2:num/space:1 <- copy 95\n"
+      "]\n"
+      "def dummy [\n"  // just for the /names: property above
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // chain space: 1000 + (alloc id) 1 + (length) 1
+      "mem: storing 0 in location 1002\n"
+      "mem: storing 2000 in location 1003\n"
+      // store to default space: 1000 + (alloc id) 1 + (length) 1 + (index) 2
+      "mem: storing 94 in location 1004\n"
+      // store to chained space: (contents of location 1003) 2000 + (alloc id) 1 + (length) 1 + (index) 2
+      "mem: storing 95 in location 2004\n"
+  );
+}
 
 //: If you think of a space as a collection of variables with a common
 //: lifetime, surrounding allows managing shorter lifetimes inside a longer
@@ -64,7 +69,11 @@ int space_index(const reagent& x) {
   return 0;
 }
 
-:(scenario permit_space_as_variable_name)
-def main [
-  space:num <- copy 0
-]
+:(code)
+void test_permit_space_as_variable_name() {
+  run(
+      "def main [\n"
+      "  space:num <- copy 0\n"
+      "]\n"
+  );
+}
diff --git a/045closure_name.cc b/045closure_name.cc
index b4a8ea24..d5f26f81 100644
--- a/045closure_name.cc
+++ b/045closure_name.cc
@@ -6,28 +6,33 @@
 //: todo: warn on default-space abuse. default-space for one recipe should
 //: never come from another, otherwise memory will be corrupted.
 
-:(scenario closure)
-def main [
-  default-space:space <- new location:type, 30
-  2:space/names:new-counter <- new-counter
-  10:num/raw <- increment-counter 2:space/names:new-counter
-  11:num/raw <- increment-counter 2:space/names:new-counter
-]
-def new-counter [
-  default-space:space <- new location:type, 30
-  x:num <- copy 23
-  y:num <- copy 13  # variable that will be incremented
-  return default-space:space
-]
-def increment-counter [
-  default-space:space <- new location:type, 30
-  0:space/names:new-counter <- next-ingredient  # outer space must be created by 'new-counter' above
-  y:num/space:1 <- add y:num/space:1, 1  # increment
-  y:num <- copy 234  # dummy
-  return y:num/space:1
-]
-+name: lexically surrounding space for recipe increment-counter comes from new-counter
-+mem: storing 15 in location 11
+void test_closure() {
+  run(
+      "def main [\n"
+      "  default-space:space <- new location:type, 30\n"
+      "  2:space/names:new-counter <- new-counter\n"
+      "  10:num/raw <- increment-counter 2:space/names:new-counter\n"
+      "  11:num/raw <- increment-counter 2:space/names:new-counter\n"
+      "]\n"
+      "def new-counter [\n"
+      "  default-space:space <- new location:type, 30\n"
+      "  x:num <- copy 23\n"
+      "  y:num <- copy 13\n"  // variable that will be incremented
+      "  return default-space:space\n"
+      "]\n"
+      "def increment-counter [\n"
+      "  default-space:space <- new location:type, 30\n"
+      "  0:space/names:new-counter <- next-ingredient\n"  // outer space must be created by 'new-counter' above
+      "  y:num/space:1 <- add y:num/space:1, 1\n"  // increment
+      "  y:num <- copy 234\n"  // dummy
+      "  return y:num/space:1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "name: lexically surrounding space for recipe increment-counter comes from new-counter\n"
+      "mem: storing 15 in location 11\n"
+  );
+}
 
 //: To make this work, compute the recipe that provides names for the
 //: surrounding space of each recipe.
@@ -142,44 +147,58 @@ bool already_transformed(const reagent& r, const map<string, int>& names) {
   return contains_key(names, r.name);
 }
 
-:(scenario missing_surrounding_space)
-% Hide_errors = true;
-def f [
-  local-scope
-  x:num/space:1 <- copy 34
-]
-+error: don't know surrounding recipe of 'f'
-+error: f: can't find a place to store 'x'
+:(code)
+void test_missing_surrounding_space() {
+  Hide_errors = true;
+  run(
+      "def f [\n"
+      "  local-scope\n"
+      "  x:num/space:1 <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: don't know surrounding recipe of 'f'\n"
+      "error: f: can't find a place to store 'x'\n"
+  );
+}
 
 //: extra test for try_reclaim_locals() from previous layers
-:(scenario local_scope_ignores_nonlocal_spaces)
-def new-scope [
-  local-scope
-  x:&:num <- new number:type
-  *x:&:num <- copy 34
-  return default-space:space
-]
-def use-scope [
-  local-scope
-  outer:space/names:new-scope <- next-ingredient
-  0:space/names:new-scope <- copy outer:space
-  return *x:&:num/space:1
-]
-def main [
-  1:space/raw <- new-scope
-  3:num/raw <- use-scope 1:space/raw
-]
-+mem: storing 34 in location 3
+void test_local_scope_ignores_nonlocal_spaces() {
+  run(
+      "def new-scope [\n"
+      "  local-scope\n"
+      "  x:&:num <- new number:type\n"
+      "  *x:&:num <- copy 34\n"
+      "  return default-space:space\n"
+      "]\n"
+      "def use-scope [\n"
+      "  local-scope\n"
+      "  outer:space/names:new-scope <- next-ingredient\n"
+      "  0:space/names:new-scope <- copy outer:space\n"
+      "  return *x:&:num/space:1\n"
+      "]\n"
+      "def main [\n"
+      "  1:space/raw <- new-scope\n"
+      "  3:num/raw <- use-scope 1:space/raw\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 3\n"
+  );
+}
 
-:(scenario recursive_transform_names)
-def foo [
-  local-scope
-  x:num <- copy 0
-  return default-space:space/names:foo
-]
-def main [
-  local-scope
-  0:space/names:foo <- foo
-  x:num/space:1 <- copy 34
-]
-$error: 0
+void test_recursive_transform_names() {
+  run(
+      "def foo [\n"
+      "  local-scope\n"
+      "  x:num <- copy 0\n"
+      "  return default-space:space/names:foo\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  0:space/names:foo <- foo\n"
+      "  x:num/space:1 <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
diff --git a/046check_type_by_name.cc b/046check_type_by_name.cc
index 6042068a..9b123d37 100644
--- a/046check_type_by_name.cc
+++ b/046check_type_by_name.cc
@@ -6,13 +6,18 @@
 //: every single time. You can't use the same name with multiple types in a
 //: single recipe.
 
-:(scenario transform_fails_on_reusing_name_with_different_type)
-% Hide_errors = true;
-def main [
-  x:num <- copy 1
-  x:bool <- copy 1
-]
-+error: main: 'x' used with multiple types
+void test_transform_fails_on_reusing_name_with_different_type() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  x:num <- copy 1\n"
+      "  x:bool <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'x' used with multiple types\n"
+  );
+}
 
 //: we need surrounding-space info for type-checking variables in other spaces
 :(after "Transform.push_back(collect_surrounding_spaces)")
@@ -110,101 +115,151 @@ recipe_ordinal owning_recipe(const reagent& x, recipe_ordinal r) {
   return r;
 }
 
-:(scenario transform_fills_in_missing_types)
-def main [
-  x:num <- copy 11
-  y:num <- add x, 1
-]
-# x is in location 2, y in location 3
-+mem: storing 12 in location 3
-
-:(scenario transform_fills_in_missing_types_in_product)
-def main [
-  x:num <- copy 11
-  x <- copy 12
-]
-# x is in location 2
-+mem: storing 12 in location 2
-
-:(scenario transform_fills_in_missing_types_in_product_and_ingredient)
-def main [
-  x:num <- copy 11
-  x <- add x, 1
-]
-# x is in location 2
-+mem: storing 12 in location 2
-
-:(scenario transform_fills_in_missing_label_type)
-def main [
-  jump +target
-  1:num <- copy 0
-  +target
-]
--mem: storing 0 in location 1
-
-:(scenario transform_fails_on_missing_types_in_first_mention)
-% Hide_errors = true;
-def main [
-  x <- copy 1
-  x:num <- copy 2
-]
-+error: main: missing type for 'x' in 'x <- copy 1'
-
-:(scenario transform_fails_on_wrong_type_for_label)
-% Hide_errors = true;
-def main [
-  +foo:num <- copy 34
-]
-+error: main: non-label '+foo' must begin with a letter
-
-:(scenario typo_in_address_type_fails)
-% Hide_errors = true;
-def main [
-  y:&:charcter <- new character:type
-  *y <- copy 67
-]
-+error: main: unknown type charcter in 'y:&:charcter <- new character:type'
-
-:(scenario array_type_without_size_fails)
-% Hide_errors = true;
-def main [
-  x:@:num <- merge 2, 12, 13
-]
-+error: main can't determine the size of array variable 'x'. Either allocate it separately and make the type of 'x' an address, or specify the length of the array in the type of 'x'.
-
-:(scenarios transform)
-:(scenario transform_checks_types_of_identical_reagents_in_multiple_spaces)
-def foo [  # dummy
-]
-def main [
-  local-scope
-  0:space/names:foo <- copy null  # specify surrounding space
-  x:bool <- copy true
-  x:num/space:1 <- copy 34
-  x/space:1 <- copy 35
-]
-$error: 0
-
-:(scenario transform_handles_empty_reagents)
-% Hide_errors = true;
-def main [
-  add *
-]
-+error: illegal name '*'
-# no crash
-
-:(scenario transform_checks_types_in_surrounding_spaces)
-% Hide_errors = true;
-# 'x' is a bool in foo's space
-def foo [
-  local-scope
-  x:bool <- copy false
-  return default-space/names:foo
-]
-# try to read 'x' as a num in foo's space
-def main [
-  local-scope
-  0:space/names:foo <- foo
-  x:num/space:1 <- copy 34
-]
-error: foo: 'x' used with multiple types
+void test_transform_fills_in_missing_types() {
+  run(
+      "def main [\n"
+      "  x:num <- copy 11\n"
+      "  y:num <- add x, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // x is in location 2, y in location 3
+      "mem: storing 12 in location 3\n"
+  );
+}
+
+void test_transform_fills_in_missing_types_in_product() {
+  run(
+      "def main [\n"
+      "  x:num <- copy 11\n"
+      "  x <- copy 12\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // x is in location 2
+      "mem: storing 12 in location 2\n"
+  );
+}
+
+void test_transform_fills_in_missing_types_in_product_and_ingredient() {
+  run(
+      "def main [\n"
+      "  x:num <- copy 11\n"
+      "  x <- add x, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // x is in location 2
+      "mem: storing 12 in location 2\n"
+  );
+}
+
+void test_transform_fills_in_missing_label_type() {
+  run(
+      "def main [\n"
+      "  jump +target\n"
+      "  1:num <- copy 0\n"
+      "  +target\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 1");
+}
+
+void test_transform_fails_on_missing_types_in_first_mention() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  x <- copy 1\n"
+      "  x:num <- copy 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: missing type for 'x' in 'x <- copy 1'\n"
+  );
+}
+
+void test_transform_fails_on_wrong_type_for_label() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  +foo:num <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: non-label '+foo' must begin with a letter\n"
+  );
+}
+
+void test_typo_in_address_type_fails() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  y:&:charcter <- new character:type\n"
+      "  *y <- copy 67\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: unknown type charcter in 'y:&:charcter <- new character:type'\n"
+  );
+}
+
+void test_array_type_without_size_fails() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  x:@:num <- merge 2, 12, 13\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main can't determine the size of array variable 'x'. Either allocate it separately and make the type of 'x' an address, or specify the length of the array in the type of 'x'.\n"
+  );
+}
+
+void test_transform_checks_types_of_identical_reagents_in_multiple_spaces() {
+  transform(
+      "def foo [\n"  // dummy function for names
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  0:space/names:foo <- copy null\n"  // specify surrounding space
+      "  x:bool <- copy true\n"
+      "  x:num/space:1 <- copy 34\n"
+      "  x/space:1 <- copy 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_transform_handles_empty_reagents() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  add *\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: illegal name '*'\n"
+  );
+  // no crash
+}
+
+void test_transform_checks_types_in_surrounding_spaces() {
+  Hide_errors = true;
+  transform(
+      // 'x' is a bool in foo's space
+      "def foo [\n"
+      "  local-scope\n"
+      "  x:bool <- copy false\n"
+      "  return default-space/names:foo\n"
+      "]\n"
+      // try to read 'x' as a num in foo's space
+      "def main [\n"
+      "  local-scope\n"
+      "  0:space/names:foo <- foo\n"
+      "  x:num/space:1 <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: 'x' used with multiple types\n"
+  );
+}
diff --git a/050scenario.cc b/050scenario.cc
index 4a4c9315..5b9dcc14 100644
--- a/050scenario.cc
+++ b/050scenario.cc
@@ -3,53 +3,62 @@
 
 //: We avoid raw numeric locations in Mu -- except in scenarios, where they're
 //: handy to check the values of specific variables
-:(scenarios run_mu_scenario)
-:(scenario scenario_block)
-scenario foo [
-  run [
-    1:num <- copy 13
-  ]
-  memory-should-contain [
-    1 <- 13
-  ]
-]
-# checks are inside scenario
-
-:(scenario scenario_multiple_blocks)
-scenario foo [
-  run [
-    1:num <- copy 13
-  ]
-  memory-should-contain [
-    1 <- 13
-  ]
-  run [
-    2:num <- copy 13
-  ]
-  memory-should-contain [
-    1 <- 13
-    2 <- 13
-  ]
-]
-# checks are inside scenario
-
-:(scenario scenario_check_memory_and_trace)
-scenario foo [
-  run [
-    1:num <- copy 13
-    trace 1, [a], [a b c]
-  ]
-  memory-should-contain [
-    1 <- 13
-  ]
-  trace-should-contain [
-    a: a b c
-  ]
-  trace-should-not-contain [
-    a: x y z
-  ]
-]
-# checks are inside scenario
+
+void test_scenario_block() {
+  run_mu_scenario(
+      "scenario foo [\n"
+      "  run [\n"
+      "    1:num <- copy 13\n"
+      "  ]\n"
+      "  memory-should-contain [\n"
+      "    1 <- 13\n"
+      "  ]\n"
+      "]\n"
+  );
+  // checks are inside scenario
+}
+
+void test_scenario_multiple_blocks() {
+  run_mu_scenario(
+      "scenario foo [\n"
+      "  run [\n"
+      "    1:num <- copy 13\n"
+      "  ]\n"
+      "  memory-should-contain [\n"
+      "    1 <- 13\n"
+      "  ]\n"
+      "  run [\n"
+      "    2:num <- copy 13\n"
+      "  ]\n"
+      "  memory-should-contain [\n"
+      "    1 <- 13\n"
+      "    2 <- 13\n"
+      "  ]\n"
+      "]\n"
+  );
+  // checks are inside scenario
+}
+
+void test_scenario_check_memory_and_trace() {
+  run_mu_scenario(
+      "scenario foo [\n"
+      "  run [\n"
+      "    1:num <- copy 13\n"
+      "    trace 1, [a], [a b c]\n"
+      "  ]\n"
+      "  memory-should-contain [\n"
+      "    1 <- 13\n"
+      "  ]\n"
+      "  trace-should-contain [\n"
+      "    a: a b c\n"
+      "  ]\n"
+      "  trace-should-not-contain [\n"
+      "    a: x y z\n"
+      "  ]\n"
+      "]\n"
+  );
+  // checks are inside scenario
+}
 
 //:: Core data structure
 
@@ -123,29 +132,43 @@ scenario parse_scenario(istream& in) {
   return result;
 }
 
-:(scenario read_scenario_with_bracket_in_comment)
-scenario foo [
-  # ']' in comment
-  1:num <- copy 0
-]
-+run: {1: "number"} <- copy {0: "literal"}
-
-:(scenario read_scenario_with_bracket_in_comment_in_nested_string)
-scenario foo [
-  1:text <- new [# not a comment]
-]
-+run: {1: ("address" "array" "character")} <- new {"# not a comment": "literal-string"}
-
-:(scenarios run)
-:(scenario duplicate_scenarios)
-% Hide_errors = true;
-scenario foo [
-  1:num <- copy 0
-]
-scenario foo [
-  2:num <- copy 0
-]
-+error: duplicate scenario name: 'foo'
+void test_read_scenario_with_bracket_in_comment() {
+  run_mu_scenario(
+      "scenario foo [\n"
+      "  # ']' in comment\n"
+      "  1:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: {1: \"number\"} <- copy {0: \"literal\"}\n"
+  );
+}
+
+void test_read_scenario_with_bracket_in_comment_in_nested_string() {
+  run_mu_scenario(
+      "scenario foo [\n"
+      "  1:text <- new [# not a comment]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: {1: (\"address\" \"array\" \"character\")} <- new {\"# not a comment\": \"literal-string\"}\n"
+  );
+}
+
+void test_duplicate_scenarios() {
+  Hide_errors = true;
+  run(
+      "scenario foo [\n"
+      "  1:num <- copy 0\n"
+      "]\n"
+      "scenario foo [\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: duplicate scenario name: 'foo'\n"
+  );
+}
 
 //:: Run scenarios when we run './mu test'.
 //: Treat the text of the scenario as a regular series of instructions.
@@ -261,7 +284,7 @@ void maybe_make_raw(reagent& r, const recipe& caller) {
   // End maybe_make_raw
 }
 
-//: Test.
+//: Test with some setup.
 :(before "End is_special_name Special-cases")
 if (s == "__maybe_make_raw_test__") return true;
 :(before "End Special Scenario Variable Names(r)")
@@ -284,33 +307,45 @@ void test_maybe_make_raw() {
 
 //: Watch out for redefinitions of scenario routines. We should never ever be
 //: doing that, regardless of anything else.
-:(scenario forbid_redefining_scenario_even_if_forced)
-% Hide_errors = true;
-% Disable_redefine_checks = true;
-def scenario-foo [
-  1:num <- copy 34
-]
-def scenario-foo [
-  1:num <- copy 35
-]
-+error: redefining recipe scenario-foo
-
-:(scenario scenario_containing_parse_error)
-% Hide_errors = true;
-scenario foo [
-  memory-should-contain [
-    1 <- 0
-  # missing ']'
-]
-# no crash
-
-:(scenario scenario_containing_transform_error)
-% Hide_errors = true;
-def main [
-  local-scope
-  add x, 1
-]
-# no crash
+
+void test_forbid_redefining_scenario_even_if_forced() {
+  Hide_errors = true;
+  Disable_redefine_checks = true;
+  run(
+      "def scenario-foo [\n"
+      "  1:num <- copy 34\n"
+      "]\n"
+      "def scenario-foo [\n"
+      "  1:num <- copy 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: redefining recipe scenario-foo\n"
+  );
+}
+
+void test_scenario_containing_parse_error() {
+  Hide_errors = true;
+  run(
+      "scenario foo [\n"
+      "  memory-should-contain [\n"
+      "    1 <- 0\n"
+         // missing ']'
+      "]\n"
+  );
+  // no crash
+}
+
+void test_scenario_containing_transform_error() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  add x, 1\n"
+      "]\n"
+  );
+  // no crash
+}
 
 :(after "bool should_check_for_redefine(const string& recipe_name)")
   if (recipe_name.find("scenario-") == 0) return true;
@@ -322,13 +357,19 @@ def main [
 //: 'run' is a purely lexical convenience to separate the code actually being
 //: tested from any setup
 
-:(scenario run)
-def main [
-  run [
-    1:num <- copy 13
-  ]
-]
-+mem: storing 13 in location 1
+:(code)
+void test_run() {
+  run(
+      "def main [\n"
+      "  run [\n"
+      "    1:num <- copy 13\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 13 in location 1\n"
+  );
+}
 
 :(before "End Rewrite Instruction(curr, recipe result)")
 if (curr.name == "run") {
@@ -339,17 +380,23 @@ if (curr.name == "run") {
   curr.clear();
 }
 
-:(scenario run_multiple)
-def main [
-  run [
-    1:num <- copy 13
-  ]
-  run [
-    2:num <- copy 13
-  ]
-]
-+mem: storing 13 in location 1
-+mem: storing 13 in location 2
+:(code)
+void test_run_multiple() {
+  run(
+      "def main [\n"
+      "  run [\n"
+      "    1:num <- copy 13\n"
+      "  ]\n"
+      "  run [\n"
+      "    2:num <- copy 13\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 13 in location 1\n"
+      "mem: storing 13 in location 2\n"
+  );
+}
 
 //: 'memory-should-contain' raises errors if specific locations aren't as expected
 //: Also includes some special support for checking Mu texts.
@@ -359,16 +406,22 @@ bool Scenario_testing_scenario = false;
 :(before "End Reset")
 Scenario_testing_scenario = false;
 
-:(scenario memory_check)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  memory-should-contain [
-    1 <- 13
-  ]
-]
-+run: checking location 1
-+error: F - main: expected location '1' to contain 13 but saw 0
+:(code)
+void test_memory_check() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  memory-should-contain [\n"
+      "    1 <- 13\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: checking location 1\n"
+      "error: F - main: expected location '1' to contain 13 but saw 0\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 MEMORY_SHOULD_CONTAIN,
@@ -488,95 +541,129 @@ void check_mu_text(int start, const string& literal) {
   }
 }
 
-:(scenario memory_check_multiple)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  memory-should-contain [
-    1 <- 0
-    1 <- 0
-  ]
-]
-+error: main: duplicate expectation for location '1'
-
-:(scenario memory_check_mu_text_length)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  1:num <- copy 3
-  2:num <- copy 97  # 'a'
-  3:num <- copy 98  # 'b'
-  4:num <- copy 99  # 'c'
-  memory-should-contain [
-    1:array:character <- [ab]
-  ]
-]
-+error: F - main: expected location '1' to contain length 2 of text [ab] but saw 3 (for text [abc])
-
-:(scenario memory_check_mu_text)
-def main [
-  1:num <- copy 3
-  2:num <- copy 97  # 'a'
-  3:num <- copy 98  # 'b'
-  4:num <- copy 99  # 'c'
-  memory-should-contain [
-    1:array:character <- [abc]
-  ]
-]
-+run: checking text length at 1
-+run: checking location 2
-+run: checking location 3
-+run: checking location 4
-
-:(scenario memory_invalid_string_check)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  memory-should-contain [
-    1 <- [abc]
-  ]
-]
-+error: F - main: location '1' can't contain non-number [abc]
-
-:(scenario memory_invalid_string_check2)
-% Hide_errors = true;
-def main [
-  1:num <- copy 3
-  2:num <- copy 97  # 'a'
-  3:num <- copy 98  # 'b'
-  4:num <- copy 99  # 'c'
-  memory-should-contain [
-    1:array:character <- 0
-  ]
-]
-+error: main: array:character types inside 'memory-should-contain' can only be compared with text literals surrounded by [], not '0'
-
-:(scenario memory_check_with_comment)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  memory-should-contain [
-    1 <- 34  # comment
-  ]
-]
--error: location 1 can't contain non-number 34  # comment
-# but there'll be an error signalled by memory-should-contain
+void test_memory_check_multiple() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  memory-should-contain [\n"
+      "    1 <- 0\n"
+      "    1 <- 0\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: duplicate expectation for location '1'\n"
+  );
+}
+
+void test_memory_check_mu_text_length() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- copy 3\n"
+      "  2:num <- copy 97  # 'a'\n"
+      "  3:num <- copy 98  # 'b'\n"
+      "  4:num <- copy 99  # 'c'\n"
+      "  memory-should-contain [\n"
+      "    1:array:character <- [ab]\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - main: expected location '1' to contain length 2 of text [ab] but saw 3 (for text [abc])\n"
+  );
+}
+
+void test_memory_check_mu_text() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 3\n"
+      "  2:num <- copy 97\n"  // 'a'
+      "  3:num <- copy 98\n"  // 'b'
+      "  4:num <- copy 99\n"  // 'c'
+      "  memory-should-contain [\n"
+      "    1:array:character <- [abc]\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: checking text length at 1\n"
+      "run: checking location 2\n"
+      "run: checking location 3\n"
+      "run: checking location 4\n"
+  );
+}
+
+void test_memory_invalid_string_check() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  memory-should-contain [\n"
+      "    1 <- [abc]\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - main: location '1' can't contain non-number [abc]\n"
+  );
+}
+
+void test_memory_invalid_string_check2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- copy 3\n"
+      "  2:num <- copy 97\n"  // 'a'
+      "  3:num <- copy 98\n"  // 'b'
+      "  4:num <- copy 99\n"  // 'c'
+      "  memory-should-contain [\n"
+      "    1:array:character <- 0\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: array:character types inside 'memory-should-contain' can only be compared with text literals surrounded by [], not '0'\n"
+  );
+}
+
+void test_memory_check_with_comment() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  memory-should-contain [\n"
+      "    1 <- 34  # comment\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("error: location 1 can't contain non-number 34  # comment");
+  // but there'll be an error signalled by memory-should-contain
+}
 
 //: 'trace-should-contain' is like the '+' lines in our scenarios so far
-// Like runs of contiguous '+' lines, order is important. The trace checks
-// that the lines are present *and* in the specified sequence. (There can be
-// other lines in between.)
-
-:(scenario trace_check_fails)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  trace-should-contain [
-    a: b
-    a: d
-  ]
-]
-+error: F - main: missing [b] in trace with label 'a'
+//: Like runs of contiguous '+' lines, order is important. The trace checks
+//: that the lines are present *and* in the specified sequence. (There can be
+//: other lines in between.)
+
+void test_trace_check_fails() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+         // nothing added to the trace
+      "  trace-should-contain [\n"
+      "    a: b\n"
+      "    a: d\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - main: missing [b] in trace with label 'a'\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 TRACE_SHOULD_CONTAIN,
@@ -635,49 +722,62 @@ vector<trace_line> parse_trace(const string& expected) {
   return result;
 }
 
-:(scenario trace_check_fails_in_nonfirst_line)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  run [
-    trace 1, [a], [b]
-  ]
-  trace-should-contain [
-    a: b
-    a: d
-  ]
-]
-+error: F - main: missing [d] in trace with label 'a'
-
-:(scenario trace_check_passes_silently)
-% Scenario_testing_scenario = true;
-def main [
-  run [
-    trace 1, [a], [b]
-  ]
-  trace-should-contain [
-    a: b
-  ]
-]
--error: missing [b] in trace with label 'a'
-$error: 0
-
-//: 'trace-should-not-contain' is like the '-' lines in our scenarios so far
-//: Each trace line is separately checked for absense. Order is *not*
-//: important, so you can't say things like "B should not exist after A."
-
-:(scenario trace_negative_check_fails)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  run [
-    trace 1, [a], [b]
-  ]
-  trace-should-not-contain [
-    a: b
-  ]
-]
-+error: F - main: unexpected [b] in trace with label 'a'
+void test_trace_check_fails_in_nonfirst_line() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  run [\n"
+      "    trace 1, [a], [b]\n"
+      "  ]\n"
+      "  trace-should-contain [\n"
+      "    a: b\n"
+      "    a: d\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - main: missing [d] in trace with label 'a'\n"
+  );
+}
+
+void test_trace_check_passes_silently() {
+  Scenario_testing_scenario = true;
+  run(
+      "def main [\n"
+      "  run [\n"
+      "    trace 1, [a], [b]\n"
+      "  ]\n"
+      "  trace-should-contain [\n"
+      "    a: b\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("error: missing [b] in trace with label 'a'");
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+//: 'trace-should-not-contain' checks each line in its argument for absence.
+//: Order is *not* important, so you can't say things like "B should not exist
+//: after A."
+
+void test_trace_negative_check_fails() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  run [\n"
+      "    trace 1, [a], [b]\n"
+      "  ]\n"
+      "  trace-should-not-contain [\n"
+      "    a: b\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - main: unexpected [b] in trace with label 'a'\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 TRACE_SHOULD_NOT_CONTAIN,
@@ -710,38 +810,49 @@ bool check_trace_missing(const string& in) {
   return true;
 }
 
-:(scenario trace_negative_check_passes_silently)
-% Scenario_testing_scenario = true;
-def main [
-  trace-should-not-contain [
-    a: b
-  ]
-]
--error: unexpected [b] in trace with label 'a'
-$error: 0
-
-:(scenario trace_negative_check_fails_on_any_unexpected_line)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  run [
-    trace 1, [a], [d]
-  ]
-  trace-should-not-contain [
-    a: b
-    a: d
-  ]
-]
-+error: F - main: unexpected [d] in trace with label 'a'
-
-:(scenario trace_count_check)
-def main [
-  run [
-    trace 1, [a], [foo]
-  ]
-  check-trace-count-for-label 1, [a]
-]
-# checks are inside scenario
+void test_trace_negative_check_passes_silently() {
+  Scenario_testing_scenario = true;
+  run(
+      "def main [\n"
+      "  trace-should-not-contain [\n"
+      "    a: b\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("error: unexpected [b] in trace with label 'a'");
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_trace_negative_check_fails_on_any_unexpected_line() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  run [\n"
+      "    trace 1, [a], [d]\n"
+      "  ]\n"
+      "  trace-should-not-contain [\n"
+      "    a: b\n"
+      "    a: d\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - main: unexpected [d] in trace with label 'a'\n"
+  );
+}
+
+void test_trace_count_check() {
+  run(
+      "def main [\n"
+      "  run [\n"
+      "    trace 1, [a], [foo]\n"
+      "  ]\n"
+      "  check-trace-count-for-label 1, [a]\n"
+      "]\n"
+  );
+  // checks are inside scenario
+}
 
 :(before "End Primitive Recipe Declarations")
 CHECK_TRACE_COUNT_FOR_LABEL,
@@ -854,16 +965,22 @@ case CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN: {
   break;
 }
 
-:(scenario trace_count_check_2)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-def main [
-  run [
-    trace 1, [a], [foo]
-  ]
-  check-trace-count-for-label 2, [a]
-]
-+error: F - main: expected 2 lines in trace with label 'a' in trace
+:(code)
+void test_trace_count_check_2() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  run [\n"
+      "    trace 1, [a], [foo]\n"
+      "  ]\n"
+      "  check-trace-count-for-label 2, [a]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - main: expected 2 lines in trace with label 'a' in trace\n"
+  );
+}
 
 //: Minor detail: ignore 'system' calls in scenarios, since anything we do
 //: with them is by definition impossible to test through Mu.
@@ -872,11 +989,17 @@ def main [
 
 //:: Warn if people use '_' manually in recipe names. They're reserved for internal use.
 
-:(scenario recipe_name_with_underscore)
-% Hide_errors = true;
-def foo_bar [
-]
-+error: foo_bar: don't create recipes with '_' in the name
+:(code)
+void test_recipe_name_with_underscore() {
+  Hide_errors = true;
+  run(
+      "def foo_bar [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo_bar: don't create recipes with '_' in the name\n"
+  );
+}
 
 :(before "End recipe Fields")
 bool is_autogenerated;
diff --git a/052tangle.cc b/052tangle.cc
index d7d1ccc3..a5332e1b 100644
--- a/052tangle.cc
+++ b/052tangle.cc
@@ -14,20 +14,25 @@ bool is_waypoint(string label) {
   return *label.begin() == '<' && *label.rbegin() == '>';
 }
 
-:(scenario tangle_before)
-def main [
-  1:num <- copy 0
-  <label1>
-  3:num <- copy 0
-]
-before <label1> [
-  2:num <- copy 0
-]
-+mem: storing 0 in location 1
-+mem: storing 0 in location 2
-+mem: storing 0 in location 3
-# nothing else
-$mem: 3
+void test_tangle_before() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  <label1>\n"
+      "  3:num <- copy 0\n"
+      "]\n"
+      "before <label1> [\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+      "mem: storing 0 in location 2\n"
+      "mem: storing 0 in location 3\n"
+  );
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 3);
+}
 
 //: while loading recipes, load before/after fragments
 
@@ -175,273 +180,328 @@ void check_insert_fragments() {
   }
 }
 
-:(scenario tangle_before_and_after)
-def main [
-  1:num <- copy 0
-  <label1>
-  4:num <- copy 0
-]
-before <label1> [
-  2:num <- copy 0
-]
-after <label1> [
-  3:num <- copy 0
-]
-+mem: storing 0 in location 1
-+mem: storing 0 in location 2
-# label1
-+mem: storing 0 in location 3
-+mem: storing 0 in location 4
-# nothing else
-$mem: 4
+void test_tangle_before_and_after() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  <label1>\n"
+      "  4:num <- copy 0\n"
+      "]\n"
+      "before <label1> [\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+      "after <label1> [\n"
+      "  3:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+      "mem: storing 0 in location 2\n"
+      // label1
+      "mem: storing 0 in location 3\n"
+      "mem: storing 0 in location 4\n"
+  );
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 4);
+}
 
-:(scenario tangle_ignores_jump_target)
-% Hide_errors = true;
-def main [
-  1:num <- copy 0
-  +label1
-  4:num <- copy 0
-]
-before +label1 [
-  2:num <- copy 0
-]
-+error: can't tangle before non-waypoint +label1
+void test_tangle_ignores_jump_target() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  +label1\n"
+      "  4:num <- copy 0\n"
+      "]\n"
+      "before +label1 [\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: can't tangle before non-waypoint +label1\n"
+  );
+}
 
-:(scenario tangle_keeps_labels_separate)
-def main [
-  1:num <- copy 0
-  <label1>
-  <label2>
-  6:num <- copy 0
-]
-before <label1> [
-  2:num <- copy 0
-]
-after <label1> [
-  3:num <- copy 0
-]
-before <label2> [
-  4:num <- copy 0
-]
-after <label2> [
-  5:num <- copy 0
-]
-+mem: storing 0 in location 1
-+mem: storing 0 in location 2
-# label1
-+mem: storing 0 in location 3
-# 'after' fragments for earlier label always go before 'before' fragments for later label
-+mem: storing 0 in location 4
-# label2
-+mem: storing 0 in location 5
-+mem: storing 0 in location 6
-# nothing else
-$mem: 6
+void test_tangle_keeps_labels_separate() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  <label1>\n"
+      "  <label2>\n"
+      "  6:num <- copy 0\n"
+      "]\n"
+      "before <label1> [\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+      "after <label1> [\n"
+      "  3:num <- copy 0\n"
+      "]\n"
+      "before <label2> [\n"
+      "  4:num <- copy 0\n"
+      "]\n"
+      "after <label2> [\n"
+      "  5:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+      "mem: storing 0 in location 2\n"
+      // label1
+      "mem: storing 0 in location 3\n"
+      // 'after' fragments for earlier label always go before 'before'
+      // fragments for later label
+      "mem: storing 0 in location 4\n"
+      // label2
+      "mem: storing 0 in location 5\n"
+      "mem: storing 0 in location 6\n"
+  );
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 6);
+}
 
-:(scenario tangle_stacks_multiple_fragments)
-def main [
-  1:num <- copy 0
-  <label1>
-  6:num <- copy 0
-]
-before <label1> [
-  2:num <- copy 0
-]
-after <label1> [
-  3:num <- copy 0
-]
-before <label1> [
-  4:num <- copy 0
-]
-after <label1> [
-  5:num <- copy 0
-]
-+mem: storing 0 in location 1
-# 'before' fragments stack in order
-+mem: storing 0 in location 2
-+mem: storing 0 in location 4
-# label1
-# 'after' fragments stack in reverse order
-+mem: storing 0 in location 5
-+mem: storing 0 in location 3
-+mem: storing 0 in location 6
-# nothing else
-$mem: 6
+void test_tangle_stacks_multiple_fragments() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  <label1>\n"
+      "  6:num <- copy 0\n"
+      "]\n"
+      "before <label1> [\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+      "after <label1> [\n"
+      "  3:num <- copy 0\n"
+      "]\n"
+      "before <label1> [\n"
+      "  4:num <- copy 0\n"
+      "]\n"
+      "after <label1> [\n"
+      "  5:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+      // 'before' fragments stack in order
+      "mem: storing 0 in location 2\n"
+      "mem: storing 0 in location 4\n"
+      // label1
+      // 'after' fragments stack in reverse order
+      "mem: storing 0 in location 5\n"
+      "mem: storing 0 in location 3\n"
+      "mem: storing 0 in location 6\n"
+  );
+  // nothing
+  CHECK_TRACE_COUNT("mem", 6);
+}
 
-:(scenario tangle_supports_fragments_with_multiple_instructions)
-def main [
-  1:num <- copy 0
-  <label1>
-  6:num <- copy 0
-]
-before <label1> [
-  2:num <- copy 0
-  3:num <- copy 0
-]
-after <label1> [
-  4:num <- copy 0
-  5:num <- copy 0
-]
-+mem: storing 0 in location 1
-+mem: storing 0 in location 2
-+mem: storing 0 in location 3
-# label1
-+mem: storing 0 in location 4
-+mem: storing 0 in location 5
-+mem: storing 0 in location 6
-# nothing else
-$mem: 6
+void test_tangle_supports_fragments_with_multiple_instructions() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"
+      "  <label1>\n"
+      "  6:num <- copy 0\n"
+      "]\n"
+      "before <label1> [\n"
+      "  2:num <- copy 0\n"
+      "  3:num <- copy 0\n"
+      "]\n"
+      "after <label1> [\n"
+      "  4:num <- copy 0\n"
+      "  5:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+      "mem: storing 0 in location 2\n"
+      "mem: storing 0 in location 3\n"
+      // label1
+      "mem: storing 0 in location 4\n"
+      "mem: storing 0 in location 5\n"
+      "mem: storing 0 in location 6\n"
+  );
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 6);
+}
 
-:(scenario tangle_tangles_into_all_labels_with_same_name)
-def main [
-  1:num <- copy 10
-  <label1>
-  4:num <- copy 10
-  recipe2
-]
-def recipe2 [
-  1:num <- copy 11
-  <label1>
-  4:num <- copy 11
-]
-before <label1> [
-  2:num <- copy 12
-]
-after <label1> [
-  3:num <- copy 12
-]
-+mem: storing 10 in location 1
-+mem: storing 12 in location 2
-# label1
-+mem: storing 12 in location 3
-+mem: storing 10 in location 4
-# recipe2
-+mem: storing 11 in location 1
-+mem: storing 12 in location 2
-# label1
-+mem: storing 12 in location 3
-+mem: storing 11 in location 4
-# nothing else
-$mem: 8
+void test_tangle_tangles_into_all_labels_with_same_name() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 10\n"
+      "  <label1>\n"
+      "  4:num <- copy 10\n"
+      "  recipe2\n"
+      "]\n"
+      "def recipe2 [\n"
+      "  1:num <- copy 11\n"
+      "  <label1>\n"
+      "  4:num <- copy 11\n"
+      "]\n"
+      "before <label1> [\n"
+      "  2:num <- copy 12\n"
+      "]\n"
+      "after <label1> [\n"
+      "  3:num <- copy 12\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 10 in location 1\n"
+      "mem: storing 12 in location 2\n"
+      // label1
+      "mem: storing 12 in location 3\n"
+      "mem: storing 10 in location 4\n"
+      // recipe2
+      "mem: storing 11 in location 1\n"
+      "mem: storing 12 in location 2\n"
+      // label1
+      "mem: storing 12 in location 3\n"
+      "mem: storing 11 in location 4\n"
+  );
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 8);
+}
 
-:(scenario tangle_tangles_into_all_labels_with_same_name_2)
-def main [
-  1:num <- copy 10
-  <label1>
-  <label1>
-  4:num <- copy 10
-]
-before <label1> [
-  2:num <- copy 12
-]
-after <label1> [
-  3:num <- copy 12
-]
-+mem: storing 10 in location 1
-+mem: storing 12 in location 2
-# label1
-+mem: storing 12 in location 3
-+mem: storing 12 in location 2
-# label1
-+mem: storing 12 in location 3
-+mem: storing 10 in location 4
-# nothing else
-$mem: 6
+void test_tangle_tangles_into_all_labels_with_same_name_2() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 10\n"
+      "  <label1>\n"
+      "  <label1>\n"
+      "  4:num <- copy 10\n"
+      "]\n"
+      "before <label1> [\n"
+      "  2:num <- copy 12\n"
+      "]\n"
+      "after <label1> [\n"
+      "  3:num <- copy 12\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 10 in location 1\n"
+      "mem: storing 12 in location 2\n"
+      // label1
+      "mem: storing 12 in location 3\n"
+      "mem: storing 12 in location 2\n"
+      // label1
+      "mem: storing 12 in location 3\n"
+      "mem: storing 10 in location 4\n"
+  );
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 6);
+}
 
-:(scenario tangle_tangles_into_all_labels_with_same_name_3)
-def main [
-  1:num <- copy 10
-  <label1>
-  <foo>
-  4:num <- copy 10
-]
-before <label1> [
-  2:num <- copy 12
-]
-after <label1> [
-  3:num <- copy 12
-]
-after <foo> [
-  <label1>
-]
-+mem: storing 10 in location 1
-+mem: storing 12 in location 2
-# label1
-+mem: storing 12 in location 3
-+mem: storing 12 in location 2
-# foo/label1
-+mem: storing 12 in location 3
-+mem: storing 10 in location 4
-# nothing else
-$mem: 6
+void test_tangle_tangles_into_all_labels_with_same_name_3() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 10\n"
+      "  <label1>\n"
+      "  <foo>\n"
+      "  4:num <- copy 10\n"
+      "]\n"
+      "before <label1> [\n"
+      "  2:num <- copy 12\n"
+      "]\n"
+      "after <label1> [\n"
+      "  3:num <- copy 12\n"
+      "]\n"
+      "after <foo> [\n"
+      "  <label1>\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 10 in location 1\n"
+      "mem: storing 12 in location 2\n"
+      // label1
+      "mem: storing 12 in location 3\n"
+      "mem: storing 12 in location 2\n"
+      // foo/label1
+      "mem: storing 12 in location 3\n"
+      "mem: storing 10 in location 4\n"
+  );
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 6);
+}
 
-:(scenario tangle_handles_jump_target_inside_fragment)
-def main [
-  1:num <- copy 10
-  <label1>
-  4:num <- copy 10
-]
-before <label1> [
-  jump +label2:label
-  2:num <- copy 12
-  +label2
-  3:num <- copy 12
-]
-+mem: storing 10 in location 1
-# label1
-+mem: storing 12 in location 3
-+mem: storing 10 in location 4
-# ignored by jump
--mem: storing 12 in label 2
-# nothing else
-$mem: 3
+void test_tangle_handles_jump_target_inside_fragment() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 10\n"
+      "  <label1>\n"
+      "  4:num <- copy 10\n"
+      "]\n"
+      "before <label1> [\n"
+      "  jump +label2:label\n"
+      "  2:num <- copy 12\n"
+      "  +label2\n"
+      "  3:num <- copy 12\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 10 in location 1\n"
+      // label1
+      "mem: storing 12 in location 3\n"
+      "mem: storing 10 in location 4\n"
+  );
+  // ignored by jump
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 12 in label 2");
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 3);
+}
 
-:(scenario tangle_renames_jump_target)
-def main [
-  1:num <- copy 10
-  <label1>
-  +label2
-  4:num <- copy 10
-]
-before <label1> [
-  jump +label2:label
-  2:num <- copy 12
-  +label2  # renamed
-  3:num <- copy 12
-]
-+mem: storing 10 in location 1
-# label1
-+mem: storing 12 in location 3
-+mem: storing 10 in location 4
-# ignored by jump
--mem: storing 12 in label 2
-# nothing else
-$mem: 3
+void test_tangle_renames_jump_target() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 10\n"
+      "  <label1>\n"
+      "  +label2\n"
+      "  4:num <- copy 10\n"
+      "]\n"
+      "before <label1> [\n"
+      "  jump +label2:label\n"
+      "  2:num <- copy 12\n"
+      "  +label2  # renamed\n"
+      "  3:num <- copy 12\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 10 in location 1\n"
+      // label1
+      "mem: storing 12 in location 3\n"
+      "mem: storing 10 in location 4\n"
+  );
+  // ignored by jump
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 12 in label 2");
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 3);
+}
 
-:(scenario tangle_jump_to_base_recipe)
-def main [
-  1:num <- copy 10
-  <label1>
-  +label2
-  4:num <- copy 10
-]
-before <label1> [
-  jump +label2:label
-  2:num <- copy 12
-  3:num <- copy 12
-]
-+mem: storing 10 in location 1
-# label1
-+mem: storing 10 in location 4
-# ignored by jump
--mem: storing 12 in label 2
--mem: storing 12 in location 3
-# nothing else
-$mem: 2
+void test_tangle_jump_to_base_recipe() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 10\n"
+      "  <label1>\n"
+      "  +label2\n"
+      "  4:num <- copy 10\n"
+      "]\n"
+      "before <label1> [\n"
+      "  jump +label2:label\n"
+      "  2:num <- copy 12\n"
+      "  3:num <- copy 12\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 10 in location 1\n"
+      // label1
+      "mem: storing 10 in location 4\n"
+  );
+  // ignored by jump
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 12 in label 2");
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 12 in location 3");
+  // nothing else
+  CHECK_TRACE_COUNT("mem", 2);
+}
 
 //: ensure that there are no new fragments created for a label after it's already been inserted to
 
-:(code)
 void test_new_fragment_after_tangle() {
   // define a recipe
   load("def foo [\n"
diff --git a/053recipe_header.cc b/053recipe_header.cc
index 5102f690..73fde510 100644
--- a/053recipe_header.cc
+++ b/053recipe_header.cc
@@ -1,17 +1,22 @@
 //: Advanced notation for the common/easy case where a recipe takes some fixed
 //: number of ingredients and yields some fixed number of products.
 
-:(scenario recipe_with_header)
-def main [
-  1:num/raw <- add2 3, 5
-]
-def add2 x:num, y:num -> z:num [
-  local-scope
-  load-ingredients
-  z:num <- add x, y
-  return z
-]
-+mem: storing 8 in location 1
+void test_recipe_with_header() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- add2 3, 5\n"
+      "]\n"
+      "def add2 x:num, y:num -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z:num <- add x, y\n"
+      "  return z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 8 in location 1\n"
+  );
+}
 
 //: When loading recipes save any header.
 
@@ -59,63 +64,93 @@ void load_recipe_header(istream& in, recipe& result) {
   // End Load Recipe Header(result)
 }
 
-:(scenario recipe_handles_stray_comma)
-def main [
-  1:num/raw <- add2 3, 5
-]
-def add2 x:num, y:num -> z:num, [
-  local-scope
-  load-ingredients
-  z:num <- add x, y
-  return z
-]
-+mem: storing 8 in location 1
-
-:(scenario recipe_handles_stray_comma_2)
-def main [
-  foo
-]
-def foo, [
-  1:num/raw <- add 2, 2
-]
-def bar [
-  1:num/raw <- add 2, 3
-]
-+mem: storing 4 in location 1
-
-:(scenario recipe_handles_wrong_arrow)
-% Hide_errors = true;
-def foo a:num <- b:num [
-]
-+error: recipe foo should say '->' and not '<-'
-
-:(scenario recipe_handles_missing_bracket)
-% Hide_errors = true;
-def main
-]
-+error: main: recipe body must begin with '['
-
-:(scenario recipe_handles_missing_bracket_2)
-% Hide_errors = true;
-def main
-  local-scope
-  {
-  }
-]
-# doesn't overflow line when reading header
--parse: header ingredient: local-scope
-+error: main: recipe body must begin with '['
-
-:(scenario recipe_handles_missing_bracket_3)
-% Hide_errors = true;
-def main  # comment
-  local-scope
-  {
-  }
-]
-# doesn't overflow line when reading header
--parse: header ingredient: local-scope
-+error: main: recipe body must begin with '['
+void test_recipe_handles_stray_comma() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- add2 3, 5\n"
+      "]\n"
+      "def add2 x:num, y:num -> z:num, [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z:num <- add x, y\n"
+      "  return z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 8 in location 1\n"
+  );
+}
+
+void test_recipe_handles_stray_comma_2() {
+  run(
+      "def main [\n"
+      "  foo\n"
+      "]\n"
+      "def foo, [\n"
+      "  1:num/raw <- add 2, 2\n"
+      "]\n"
+      "def bar [\n"
+      "  1:num/raw <- add 2, 3\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 1\n"
+  );
+}
+
+void test_recipe_handles_wrong_arrow() {
+  Hide_errors = true;
+  run(
+      "def foo a:num <- b:num [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: recipe foo should say '->' and not '<-'\n"
+  );
+}
+
+void test_recipe_handles_missing_bracket() {
+  Hide_errors = true;
+  run(
+      "def main\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: recipe body must begin with '['\n"
+  );
+}
+
+void test_recipe_handles_missing_bracket_2() {
+  Hide_errors = true;
+  run(
+      "def main\n"
+      "  local-scope\n"
+      "  {\n"
+      "  }\n"
+      "]\n"
+  );
+  // doesn't overflow line when reading header
+  CHECK_TRACE_DOESNT_CONTAIN("parse: header ingredient: local-scope");
+  CHECK_TRACE_CONTENTS(
+      "error: main: recipe body must begin with '['\n"
+  );
+}
+
+void test_recipe_handles_missing_bracket_3() {
+  Hide_errors = true;
+  run(
+      "def main  # comment\n"
+      "  local-scope\n"
+      "  {\n"
+      "  }\n"
+      "]\n"
+  );
+  // doesn't overflow line when reading header
+  CHECK_TRACE_DOESNT_CONTAIN("parse: header ingredient: local-scope");
+  CHECK_TRACE_CONTENTS(
+      "error: main: recipe body must begin with '['\n"
+  );
+}
 
 :(after "Begin debug_string(recipe x)")
 out << "ingredients:\n";
@@ -127,11 +162,17 @@ for (int i = 0;  i < SIZE(x.products);  ++i)
 
 //: If a recipe never mentions any ingredients or products, assume it has a header.
 
-:(scenario recipe_without_ingredients_or_products_has_header)
-def test [
-  1:num <- copy 34
-]
-+parse: recipe test has a header
+:(code)
+void test_recipe_without_ingredients_or_products_has_header() {
+  run(
+      "def test [\n"
+      "  1:num <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "parse: recipe test has a header\n"
+  );
+}
 
 :(before "End Recipe Body(result)")
 if (!result.has_header) {
@@ -154,18 +195,24 @@ if (result.has_header) {
 
 //: Support type abbreviations in headers.
 
-:(scenario type_abbreviations_in_recipe_headers)
-def main [
-  local-scope
-  a:text <- foo
-  1:char/raw <- index *a, 0
-]
-def foo -> a:text [  # 'text' is an abbreviation
-  local-scope
-  load-ingredients
-  a <- new [abc]
-]
-+mem: storing 97 in location 1
+:(code)
+void test_type_abbreviations_in_recipe_headers() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  a:text <- foo\n"
+      "  1:char/raw <- index *a, 0\n"
+      "]\n"
+      "def foo -> a:text [  # 'text' is an abbreviation\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  a <- new [abc]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 97 in location 1\n"
+  );
+}
 
 :(before "End Expand Type Abbreviations(caller)")
 for (long int i = 0;  i < SIZE(caller.ingredients);  ++i)
@@ -221,28 +268,39 @@ case NEXT_INGREDIENT_WITHOUT_TYPECHECKING: {
 }
 
 //: more useful error messages if someone forgets 'load-ingredients'
-
-:(scenario load_ingredients_missing_error)
-% Hide_errors = true;
-def foo a:num [
-  local-scope
-  b:num <- add a:num, 1
-]
-+error: foo: tried to read ingredient 'a' in 'b:num <- add a:num, 1' but it hasn't been written to yet
-+error:   did you forget 'load-ingredients'?
+:(code)
+void test_load_ingredients_missing_error() {
+  Hide_errors = true;
+  run(
+      "def foo a:num [\n"
+      "  local-scope\n"
+      "  b:num <- add a:num, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: tried to read ingredient 'a' in 'b:num <- add a:num, 1' but it hasn't been written to yet\n"
+      "error:   did you forget 'load-ingredients'?\n"
+  );
+}
 
 :(after "use-before-set Error")
 if (is_present_in_ingredients(caller, ingredient.name))
   raise << "  did you forget 'load-ingredients'?\n" << end();
 
-:(scenario load_ingredients_missing_error_2)
-% Hide_errors = true;
-def foo a:num [
-  local-scope
-  b:num <- add a, 1
-]
-+error: foo: missing type for 'a' in 'b:num <- add a, 1'
-+error:   did you forget 'load-ingredients'?
+:(code)
+void test_load_ingredients_missing_error_2() {
+  Hide_errors = true;
+  run(
+      "def foo a:num [\n"
+      "  local-scope\n"
+      "  b:num <- add a, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: missing type for 'a' in 'b:num <- add a, 1'\n"
+      "error:   did you forget 'load-ingredients'?\n"
+  );
+}
 
 :(after "missing-type Error 1")
 if (is_present_in_ingredients(get(Recipe, get(Recipe_ordinal, recipe_name)), x.name))
@@ -259,29 +317,39 @@ bool is_present_in_ingredients(const recipe& callee, const string& ingredient_na
 
 //:: Check all calls against headers.
 
-:(scenario show_clear_error_on_bad_call)
-% Hide_errors = true;
-def main [
-  1:num <- foo 34
-]
-def foo x:point -> y:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-+error: main: ingredient 0 has the wrong type at '1:num <- foo 34'
-
-:(scenario show_clear_error_on_bad_call_2)
-% Hide_errors = true;
-def main [
-  1:point <- foo 34
-]
-def foo x:num -> y:num [
-  local-scope
-  load-ingredients
-  return x
-]
-+error: main: product 0 has the wrong type at '1:point <- foo 34'
+void test_show_clear_error_on_bad_call() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- foo 34\n"
+      "]\n"
+      "def foo x:point -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: ingredient 0 has the wrong type at '1:num <- foo 34'\n"
+  );
+}
+
+void test_show_clear_error_on_bad_call_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:point <- foo 34\n"
+      "]\n"
+      "def foo x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: product 0 has the wrong type at '1:point <- foo 34'\n"
+  );
+}
 
 :(after "Transform.push_back(check_instruction)")
 Transform.push_back(check_calls_against_header);  // idempotent
@@ -314,16 +382,20 @@ void check_calls_against_header(const recipe_ordinal r) {
 
 //:: Check types going in and out of all recipes with headers.
 
-:(scenarios transform)
-:(scenario recipe_headers_are_checked)
-% Hide_errors = true;
-def add2 x:num, y:num -> z:num [
-  local-scope
-  load-ingredients
-  z:&:num <- copy 0/unsafe
-  return z
-]
-+error: add2: replied with the wrong type at 'return z'
+void test_recipe_headers_are_checked() {
+  Hide_errors = true;
+  transform(
+      "def add2 x:num, y:num -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z:&:num <- copy 0/unsafe\n"
+      "  return z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: add2: replied with the wrong type at 'return z'\n"
+  );
+}
 
 :(before "End Checks")
 Transform.push_back(check_return_instructions_against_header);  // idempotent
@@ -347,51 +419,76 @@ void check_return_instructions_against_header(const recipe_ordinal r) {
   }
 }
 
-:(scenario recipe_headers_are_checked_2)
-% Hide_errors = true;
-def add2 x:num, y:num [
-  local-scope
-  load-ingredients
-  z:&:num <- copy 0/unsafe
-  return z
-]
-+error: add2: replied with the wrong number of products at 'return z'
-
-:(scenario recipe_headers_are_checked_against_pre_transformed_instructions)
-% Hide_errors = true;
-def foo -> x:num [
-  local-scope
-  x:num <- copy 0
-  z:bool <- copy false
-  return-if z, z
-]
-+error: foo: replied with the wrong type at 'return-if z, z'
-
-:(scenario recipe_headers_check_for_duplicate_names)
-% Hide_errors = true;
-def foo x:num, x:num -> z:num [
-  local-scope
-  load-ingredients
-  return z
-]
-+error: foo: 'x' can't repeat in the ingredients
-
-:(scenario recipe_headers_check_for_duplicate_names_2)
-% Hide_errors = true;
-def foo x:num, x:num [  # no result
-  local-scope
-  load-ingredients
-]
-+error: foo: 'x' can't repeat in the ingredients
-
-:(scenario recipe_headers_check_for_missing_types)
-% Hide_errors = true;
-def main [
-  foo 0
-]
-def foo a [  # no type for 'a'
-]
-+error: foo: ingredient 'a' has no type
+void test_recipe_headers_are_checked_2() {
+  Hide_errors = true;
+  transform(
+      "def add2 x:num, y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z:&:num <- copy 0/unsafe\n"
+      "  return z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: add2: replied with the wrong number of products at 'return z'\n"
+  );
+}
+
+void test_recipe_headers_are_checked_against_pre_transformed_instructions() {
+  Hide_errors = true;
+  transform(
+      "def foo -> x:num [\n"
+      "  local-scope\n"
+      "  x:num <- copy 0\n"
+      "  z:bool <- copy false\n"
+      "  return-if z, z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: replied with the wrong type at 'return-if z, z'\n"
+  );
+}
+
+void test_recipe_headers_check_for_duplicate_names() {
+  Hide_errors = true;
+  transform(
+      "def foo x:num, x:num -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: 'x' can't repeat in the ingredients\n"
+  );
+}
+
+void test_recipe_headers_check_for_duplicate_names_2() {
+  Hide_errors = true;
+  transform(
+      "def foo x:num, x:num [  # no result\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: 'x' can't repeat in the ingredients\n"
+  );
+}
+
+void test_recipe_headers_check_for_missing_types() {
+  Hide_errors = true;
+  transform(
+      "def main [\n"
+      "  foo 0\n"
+      "]\n"
+      "def foo a [\n"  // no type for 'a'
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: ingredient 'a' has no type\n"
+  );
+}
 
 :(before "End recipe Fields")
 map<string, int> ingredient_index;
@@ -415,18 +512,22 @@ void check_header_ingredients(const recipe_ordinal r) {
 
 //: Deduce types from the header if possible.
 
-:(scenarios run)
-:(scenario deduce_instruction_types_from_recipe_header)
-def main [
-  1:num/raw <- add2 3, 5
-]
-def add2 x:num, y:num -> z:num [
-  local-scope
-  load-ingredients
-  z <- add x, y  # no type for z
-  return z
-]
-+mem: storing 8 in location 1
+void test_deduce_instruction_types_from_recipe_header() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- add2 3, 5\n"
+      "]\n"
+      "def add2 x:num, y:num -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z <- add x, y  # no type for z\n"
+      "  return z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 8 in location 1\n"
+  );
+}
 
 :(after "Begin Type Modifying Transforms")
 Transform.push_back(deduce_types_from_header);  // idempotent
@@ -473,17 +574,22 @@ void deduce_types_from_header(const recipe_ordinal r) {
 //: One final convenience: no need to say what to return if the information is
 //: in the header.
 
-:(scenario return_based_on_header)
-def main [
-  1:num/raw <- add2 3, 5
-]
-def add2 x:num, y:num -> z:num [
-  local-scope
-  load-ingredients
-  z <- add x, y
-  return
-]
-+mem: storing 8 in location 1
+void test_return_based_on_header() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- add2 3, 5\n"
+      "]\n"
+      "def add2 x:num, y:num -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z <- add x, y\n"
+      "  return\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 8 in location 1\n"
+  );
+}
 
 :(after "Transform.push_back(check_header_ingredients)")
 Transform.push_back(fill_in_return_ingredients);  // idempotent
@@ -526,97 +632,142 @@ void add_header_products(instruction& inst, const recipe& caller_recipe) {
   }
 }
 
-:(scenario explicit_return_ignores_header)
-def main [
-  1:num/raw, 2:num/raw <- add2 3, 5
-]
-def add2 a:num, b:num -> y:num, z:num [
-  local-scope
-  load-ingredients
-  y <- add a, b
-  z <- subtract a, b
-  return a, z
-]
-+mem: storing 3 in location 1
-+mem: storing -2 in location 2
-
-:(scenario return_on_fallthrough_based_on_header)
-def main [
-  1:num/raw <- add2 3, 5
-]
-def add2 x:num, y:num -> z:num [
-  local-scope
-  load-ingredients
-  z <- add x, y
-]
-+transform: instruction: return {z: "number"}
-+mem: storing 8 in location 1
-
-:(scenario return_on_fallthrough_already_exists)
-def main [
-  1:num/raw <- add2 3, 5
-]
-def add2 x:num, y:num -> z:num [
-  local-scope
-  load-ingredients
-  z <- add x, y  # no type for z
-  return z
-]
-+transform: instruction: return {z: ()}
--transform: instruction: return z:num
-+mem: storing 8 in location 1
-
-:(scenario return_causes_error_in_empty_recipe)
-% Hide_errors = true;
-def foo -> x:num [
-]
-+error: foo: tried to read ingredient 'x' in 'return x:num' but it hasn't been written to yet
-
-:(scenario return_after_conditional_return_based_on_header)
-def main [
-  1:num/raw <- add2 3, 5
-]
-def add2 x:num, y:num -> z:num [
-  local-scope
-  load-ingredients
-  z <- add x, y  # no type for z
-  return-if false, 34
-]
-+mem: storing 8 in location 1
-
-:(scenario recipe_headers_perform_same_ingredient_check)
-% Hide_errors = true;
-def main [
-  1:num <- copy 34
-  2:num <- copy 34
-  3:num <- add2 1:num, 2:num
-]
-def add2 x:num, y:num -> x:num [
-  local-scope
-  load-ingredients
-]
-+error: main: '3:num <- add2 1:num, 2:num' should write to '1:num' rather than '3:num'
+void test_explicit_return_ignores_header() {
+  run(
+      "def main [\n"
+      "  1:num/raw, 2:num/raw <- add2 3, 5\n"
+      "]\n"
+      "def add2 a:num, b:num -> y:num, z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- add a, b\n"
+      "  z <- subtract a, b\n"
+      "  return a, z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 1\n"
+      "mem: storing -2 in location 2\n"
+  );
+}
+
+void test_return_on_fallthrough_based_on_header() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- add2 3, 5\n"
+      "]\n"
+      "def add2 x:num, y:num -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z <- add x, y\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: instruction: return {z: \"number\"}\n"
+      "mem: storing 8 in location 1\n"
+  );
+}
+
+void test_return_on_fallthrough_already_exists() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- add2 3, 5\n"
+      "]\n"
+      "def add2 x:num, y:num -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z <- add x, y  # no type for z\n"
+      "  return z\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: instruction: return {z: ()}\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("transform: instruction: return z:num");
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 8 in location 1\n"
+  );
+}
+
+void test_return_causes_error_in_empty_recipe() {
+  Hide_errors = true;
+  run(
+      "def foo -> x:num [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: tried to read ingredient 'x' in 'return x:num' but it hasn't been written to yet\n"
+  );
+}
+
+void test_return_after_conditional_return_based_on_header() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- add2 3, 5\n"
+      "]\n"
+      "def add2 x:num, y:num -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z <- add x, y\n"  // no type for z
+      "  return-if false, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 8 in location 1\n"
+  );
+}
+
+void test_recipe_headers_perform_same_ingredient_check() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 34\n"
+      "  3:num <- add2 1:num, 2:num\n"
+      "]\n"
+      "def add2 x:num, y:num -> x:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: '3:num <- add2 1:num, 2:num' should write to '1:num' rather than '3:num'\n"
+  );
+}
 
 //: One special-case is recipe 'main'. Make sure it's only ever taking in text
 //: ingredients, and returning a single number.
 
-:(scenario recipe_header_ingredients_constrained_for_main)
-% Hide_errors = true;
-def main x:num [
-]
-+error: ingredients of recipe 'main' must all be text (address:array:character)
-
-:(scenario recipe_header_products_constrained_for_main)
-% Hide_errors = true;
-def main -> x:text [
-]
-+error: recipe 'main' must return at most a single product, a number
-
-:(scenario recipe_header_products_constrained_for_main_2)
-% Hide_errors = true;
-def main -> x:num, y:num [
-]
-+error: recipe 'main' must return at most a single product, a number
+void test_recipe_header_ingredients_constrained_for_main() {
+  Hide_errors = true;
+  run(
+      "def main x:num [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: ingredients of recipe 'main' must all be text (address:array:character)\n"
+  );
+}
+void test_recipe_header_products_constrained_for_main() {
+  Hide_errors = true;
+  run(
+      "def main -> x:text [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: recipe 'main' must return at most a single product, a number\n"
+  );
+}
+void test_recipe_header_products_constrained_for_main_2() {
+  Hide_errors = true;
+  run(
+      "def main -> x:num, y:num [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: recipe 'main' must return at most a single product, a number\n"
+  );
+}
 
 :(after "Transform.push_back(expand_type_abbreviations)")
 Transform.push_back(check_recipe_header_constraints);
diff --git a/054static_dispatch.cc b/054static_dispatch.cc
index cdd23e4f..289dce87 100644
--- a/054static_dispatch.cc
+++ b/054static_dispatch.cc
@@ -2,17 +2,22 @@
 //: number and types of the ingredients and products. Allows us to use nice
 //: names like 'print' or 'length' in many mutually extensible ways.
 
-:(scenario static_dispatch)
-def main [
-  7:num/raw <- test 3
-]
-def test a:num -> z:num [
-  z <- copy 1
-]
-def test a:num, b:num -> z:num [
-  z <- copy 2
-]
-+mem: storing 1 in location 7
+void test_static_dispatch() {
+  run(
+      "def main [\n"
+      "  7:num/raw <- test 3\n"
+      "]\n"
+      "def test a:num -> z:num [\n"
+      "  z <- copy 1\n"
+      "]\n"
+      "def test a:num, b:num -> z:num [\n"
+      "  z <- copy 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 7\n"
+  );
+}
 
 //: When loading recipes, accumulate variants if headers don't collide, and
 //: flag an error if headers collide.
@@ -120,17 +125,22 @@ string next_unused_recipe_name(const string& recipe_name) {
 //: Once all the recipes are loaded, transform their bodies to replace each
 //: call with the most suitable variant.
 
-:(scenario static_dispatch_picks_most_similar_variant)
-def main [
-  7:num/raw <- test 3, 4, 5
-]
-def test a:num -> z:num [
-  z <- copy 1
-]
-def test a:num, b:num -> z:num [
-  z <- copy 2
-]
-+mem: storing 2 in location 7
+void test_static_dispatch_picks_most_similar_variant() {
+  run(
+      "def main [\n"
+      "  7:num/raw <- test 3, 4, 5\n"
+      "]\n"
+      "def test a:num -> z:num [\n"
+      "  z <- copy 1\n"
+      "]\n"
+      "def test a:num, b:num -> z:num [\n"
+      "  z <- copy 2\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 7\n"
+  );
+}
 
 //: support recipe headers in a previous transform to fill in missing types
 :(before "End check_or_set_invalid_types")
@@ -343,144 +353,189 @@ bool next_stash(const call& c, instruction* stash_inst) {
   return false;
 }
 
-:(scenario static_dispatch_disabled_in_recipe_without_variants)
-def main [
-  1:num <- test 3
-]
-def test [
-  2:num <- next-ingredient  # ensure no header
-  return 34
-]
-+mem: storing 34 in location 1
-
-:(scenario static_dispatch_disabled_on_headerless_definition)
-% Hide_errors = true;
-def test a:num -> z:num [
-  z <- copy 1
-]
-def test [
-  return 34
-]
-+error: redefining recipe test
-
-:(scenario static_dispatch_disabled_on_headerless_definition_2)
-% Hide_errors = true;
-def test [
-  return 34
-]
-def test a:num -> z:num [
-  z <- copy 1
-]
-+error: redefining recipe test
-
-:(scenario static_dispatch_on_primitive_names)
-def main [
-  1:num <- copy 34
-  2:num <- copy 34
-  3:bool <- equal 1:num, 2:num
-  4:bool <- copy false
-  5:bool <- copy false
-  6:bool <- equal 4:bool, 5:bool
-]
-# temporarily hardcode number equality to always fail
-def equal x:num, y:num -> z:bool [
-  local-scope
-  load-ingredients
-  z <- copy false
-]
-# comparing numbers used overload
-+mem: storing 0 in location 3
-# comparing booleans continues to use primitive
-+mem: storing 1 in location 6
-
-:(scenario static_dispatch_works_with_dummy_results_for_containers)
-def main [
-  _ <- test 3, 4
-]
-def test a:num -> z:point [
-  local-scope
-  load-ingredients
-  z <- merge a, 0
-]
-def test a:num, b:num -> z:point [
-  local-scope
-  load-ingredients
-  z <- merge a, b
-]
-$error: 0
-
-:(scenario static_dispatch_works_with_compound_type_containing_container_defined_after_first_use)
-def main [
-  x:&:foo <- new foo:type
-  test x
-]
-container foo [
-  x:num
-]
-def test a:&:foo -> z:num [
-  local-scope
-  load-ingredients
-  z:num <- get *a, x:offset
-]
-$error: 0
-
-:(scenario static_dispatch_works_with_compound_type_containing_container_defined_after_second_use)
-def main [
-  x:&:foo <- new foo:type
-  test x
-]
-def test a:&:foo -> z:num [
-  local-scope
-  load-ingredients
-  z:num <- get *a, x:offset
-]
-container foo [
-  x:num
-]
-$error: 0
-
-:(scenario static_dispatch_on_non_literal_character_ignores_variant_with_numbers)
-% Hide_errors = true;
-def main [
-  local-scope
-  x:char <- copy 10/newline
-  1:num/raw <- foo x
-]
-def foo x:num -> y:num [
-  load-ingredients
-  return 34
-]
-+error: main: ingredient 0 has the wrong type at '1:num/raw <- foo x'
--mem: storing 34 in location 1
-
-:(scenario static_dispatch_dispatches_literal_to_character)
-def main [
-  1:num/raw <- foo 97
-]
-def foo x:char -> y:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-# character variant is preferred
-+mem: storing 34 in location 1
-
-:(scenario static_dispatch_dispatches_literal_to_number_if_at_all_possible)
-def main [
-  1:num/raw <- foo 97
-]
-def foo x:char -> y:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-def foo x:num -> y:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-# number variant is preferred
-+mem: storing 35 in location 1
+void test_static_dispatch_disabled_in_recipe_without_variants() {
+  run(
+      "def main [\n"
+      "  1:num <- test 3\n"
+      "]\n"
+      "def test [\n"
+      "  2:num <- next-ingredient  # ensure no header\n"
+      "  return 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
+
+void test_static_dispatch_disabled_on_headerless_definition() {
+  Hide_errors = true;
+  run(
+      "def test a:num -> z:num [\n"
+      "  z <- copy 1\n"
+      "]\n"
+      "def test [\n"
+      "  return 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: redefining recipe test\n"
+  );
+}
+
+void test_static_dispatch_disabled_on_headerless_definition_2() {
+  Hide_errors = true;
+  run(
+      "def test [\n"
+      "  return 34\n"
+      "]\n"
+      "def test a:num -> z:num [\n"
+      "  z <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: redefining recipe test\n"
+  );
+}
+
+void test_static_dispatch_on_primitive_names() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 34\n"
+      "  3:bool <- equal 1:num, 2:num\n"
+      "  4:bool <- copy false\n"
+      "  5:bool <- copy false\n"
+      "  6:bool <- equal 4:bool, 5:bool\n"
+      "]\n"
+      // temporarily hardcode number equality to always fail
+      "def equal x:num, y:num -> z:bool [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z <- copy false\n"
+      "]\n"
+      "# comparing numbers used overload\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // comparing numbers used overload
+      "mem: storing 0 in location 3\n"
+      // comparing booleans continues to use primitive
+      "mem: storing 1 in location 6\n"
+  );
+}
+
+void test_static_dispatch_works_with_dummy_results_for_containers() {
+  run(
+      "def main [\n"
+      "  _ <- test 3, 4\n"
+      "]\n"
+      "def test a:num -> z:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z <- merge a, 0\n"
+      "]\n"
+      "def test a:num, b:num -> z:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z <- merge a, b\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_static_dispatch_works_with_compound_type_containing_container_defined_after_first_use() {
+  run(
+      "def main [\n"
+      "  x:&:foo <- new foo:type\n"
+      "  test x\n"
+      "]\n"
+      "container foo [\n"
+      "  x:num\n"
+      "]\n"
+      "def test a:&:foo -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z:num <- get *a, x:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_static_dispatch_works_with_compound_type_containing_container_defined_after_second_use() {
+  run(
+      "def main [\n"
+      "  x:&:foo <- new foo:type\n"
+      "  test x\n"
+      "]\n"
+      "def test a:&:foo -> z:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  z:num <- get *a, x:offset\n"
+      "]\n"
+      "container foo [\n"
+      "  x:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_static_dispatch_on_non_literal_character_ignores_variant_with_numbers() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:char <- copy 10/newline\n"
+      "  1:num/raw <- foo x\n"
+      "]\n"
+      "def foo x:num -> y:num [\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: ingredient 0 has the wrong type at '1:num/raw <- foo x'\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 34 in location 1");
+}
+
+void test_static_dispatch_dispatches_literal_to_character() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- foo 97\n"
+      "]\n"
+      "def foo x:char -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      "# character variant is preferred\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
+
+void test_static_dispatch_dispatches_literal_to_number_if_at_all_possible() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- foo 97\n"
+      "]\n"
+      "def foo x:char -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      "def foo x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      // number variant is preferred
+      "mem: storing 35 in location 1\n"
+  );
+}
 
 :(replace{} "string header_label(const recipe_ordinal r)")
 string header_label(const recipe_ordinal r) {
@@ -509,95 +564,120 @@ string original_header_label(const recipe& caller) {
   return out.str();
 }
 
-:(scenario reload_variant_retains_other_variants)
-def main [
-  1:num <- copy 34
-  2:num <- foo 1:num
-]
-def foo x:num -> y:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-def foo x:&:num -> y:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-def! foo x:&:num -> y:num [
-  local-scope
-  load-ingredients
-  return 36
-]
-+mem: storing 34 in location 2
-$error: 0
-
-:(scenario dispatch_errors_come_after_unknown_name_errors)
-% Hide_errors = true;
-def main [
-  y:num <- foo x
-]
-def foo a:num -> b:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-def foo a:bool -> b:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-+error: main: missing type for 'x' in 'y:num <- foo x'
-+error: main: failed to find a matching call for 'y:num <- foo x'
-
-:(scenario override_methods_with_type_abbreviations)
-def main [
-  local-scope
-  s:text <- new [abc]
-  1:num/raw <- foo s
-]
-def foo a:address:array:character -> result:number [
-  return 34
-]
-# identical to previous variant once you take type abbreviations into account
-def! foo a:text -> result:num [
-  return 35
-]
-+mem: storing 35 in location 1
-
-:(scenario ignore_static_dispatch_in_type_errors_without_overloading)
-% Hide_errors = true;
-def main [
-  local-scope
-  x:&:num <- copy 0
-  foo x
-]
-def foo x:&:char [
-  local-scope
-  load-ingredients
-]
-+error: main: types don't match in call for 'foo x'
-+error:   which tries to call 'recipe foo x:&:char'
-
-:(scenario show_available_variants_in_dispatch_errors)
-% Hide_errors = true;
-def main [
-  local-scope
-  x:&:num <- copy 0
-  foo x
-]
-def foo x:&:char [
-  local-scope
-  load-ingredients
-]
-def foo x:&:bool [
-  local-scope
-  load-ingredients
-]
-+error: main: failed to find a matching call for 'foo x'
-+error:   available variants are:
-+error:     recipe foo x:&:char
-+error:     recipe foo x:&:bool
+void test_reload_variant_retains_other_variants() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- foo 1:num\n"
+      "]\n"
+      "def foo x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      "def foo x:&:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+      "def! foo x:&:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 36\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 2\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_dispatch_errors_come_after_unknown_name_errors() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  y:num <- foo x\n"
+      "]\n"
+      "def foo a:num -> b:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      "def foo a:bool -> b:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: missing type for 'x' in 'y:num <- foo x'\n"
+      "error: main: failed to find a matching call for 'y:num <- foo x'\n"
+  );
+}
+
+void test_override_methods_with_type_abbreviations() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  s:text <- new [abc]\n"
+      "  1:num/raw <- foo s\n"
+      "]\n"
+      "def foo a:address:array:character -> result:number [\n"
+      "  return 34\n"
+      "]\n"
+      // identical to previous variant once you take type abbreviations into account
+      "def! foo a:text -> result:num [\n"
+      "  return 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 35 in location 1\n"
+  );
+}
+
+void test_ignore_static_dispatch_in_type_errors_without_overloading() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:&:num <- copy 0\n"
+      "  foo x\n"
+      "]\n"
+      "def foo x:&:char [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: types don't match in call for 'foo x'\n"
+      "error:   which tries to call 'recipe foo x:&:char'\n"
+  );
+}
+
+void test_show_available_variants_in_dispatch_errors() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:&:num <- copy 0\n"
+      "  foo x\n"
+      "]\n"
+      "def foo x:&:char [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "]\n"
+      "def foo x:&:bool [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: failed to find a matching call for 'foo x'\n"
+      "error:   available variants are:\n"
+      "error:     recipe foo x:&:char\n"
+      "error:     recipe foo x:&:bool\n"
+  );
+}
 
 :(before "End Includes")
 using std::abs;
diff --git a/055shape_shifting_container.cc b/055shape_shifting_container.cc
index 1fb5f176..909e6fc5 100644
--- a/055shape_shifting_container.cc
+++ b/055shape_shifting_container.cc
@@ -24,93 +24,122 @@ const type_tree* get_base_type(const type_tree* t) {
   return result;
 }
 
-:(scenario ill_formed_container)
-% Hide_errors = true;
-def main [
-  {1: ((foo) num)} <- copy 0
-]
-# no crash
+:(code)
+void test_ill_formed_container() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  {1: ((foo) num)} <- copy 0\n"
+      "]\n"
+  );
+  // no crash
+}
 
 //: update size_of to handle non-atom container types
 
-:(scenario size_of_shape_shifting_container)
-container foo:_t [
-  x:_t
-  y:num
-]
-def main [
-  1:foo:num <- merge 12, 13
-  3:foo:point <- merge 14, 15, 16
-]
-+mem: storing 12 in location 1
-+mem: storing 13 in location 2
-+mem: storing 14 in location 3
-+mem: storing 15 in location 4
-+mem: storing 16 in location 5
-
-:(scenario size_of_shape_shifting_container_2)
-# multiple type ingredients
-container foo:_a:_b [
-  x:_a
-  y:_b
-]
-def main [
-  1:foo:num:bool <- merge 34, true
-]
-$error: 0
-
-:(scenario size_of_shape_shifting_container_3)
-container foo:_a:_b [
-  x:_a
-  y:_b
-]
-def main [
-  1:text <- new [abc]
-  # compound types for type ingredients
-  {3: (foo number (address array character))} <- merge 34/x, 1:text/y
-]
-$error: 0
-
-:(scenario size_of_shape_shifting_container_4)
-container foo:_a:_b [
-  x:_a
-  y:_b
-]
-container bar:_a:_b [
-  # dilated element
-  {data: (foo _a (address _b))}
-]
-def main [
-  1:text <- new [abc]
-  3:bar:num:@:char <- merge 34/x, 1:text/y
-]
-$error: 0
-
-:(scenario shape_shifting_container_extend)
-container foo:_a [
-  x:_a
-]
-container foo:_a [
-  y:_a
-]
-$error: 0
-
-:(scenario shape_shifting_container_extend_error)
-% Hide_errors = true;
-container foo:_a [
-  x:_a
-]
-container foo:_b [
-  y:_b
-]
-+error: headers of container 'foo' must use identical type ingredients
-
-:(scenario type_ingredient_must_start_with_underscore)
-% Hide_errors = true;
-container foo:t [
-  x:num
-]
-+error: foo: type ingredient 't' must begin with an underscore
+void test_size_of_shape_shifting_container() {
+  run(
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:num <- merge 12, 13\n"
+      "  3:foo:point <- merge 14, 15, 16\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 12 in location 1\n"
+      "mem: storing 13 in location 2\n"
+      "mem: storing 14 in location 3\n"
+      "mem: storing 15 in location 4\n"
+      "mem: storing 16 in location 5\n"
+  );
+}
+void test_size_of_shape_shifting_container_2() {
+  run(
+      // multiple type ingredients
+      "container foo:_a:_b [\n"
+      "  x:_a\n"
+      "  y:_b\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:num:bool <- merge 34, true\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+void test_size_of_shape_shifting_container_3() {
+  run(
+      "container foo:_a:_b [\n"
+      "  x:_a\n"
+      "  y:_b\n"
+      "]\n"
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+         // compound types for type ingredients
+      "  {3: (foo number (address array character))} <- merge 34/x, 1:text/y\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_size_of_shape_shifting_container_4() {
+  run(
+      "container foo:_a:_b [\n"
+      "  x:_a\n"
+      "  y:_b\n"
+      "]\n"
+      "container bar:_a:_b [\n"
+         // dilated element
+      "  {data: (foo _a (address _b))}\n"
+      "]\n"
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+      "  3:bar:num:@:char <- merge 34/x, 1:text/y\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_shape_shifting_container_extend() {
+  run(
+      "container foo:_a [\n"
+      "  x:_a\n"
+      "]\n"
+      "container foo:_a [\n"
+      "  y:_a\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_shape_shifting_container_extend_error() {
+  Hide_errors = true;
+  run(
+      "container foo:_a [\n"
+      "  x:_a\n"
+      "]\n"
+      "container foo:_b [\n"
+      "  y:_b\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: headers of container 'foo' must use identical type ingredients\n"
+  );
+}
+
+void test_type_ingredient_must_start_with_underscore() {
+  Hide_errors = true;
+  run(
+      "container foo:t [\n"
+      "  x:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: type ingredient 't' must begin with an underscore\n"
+  );
+}
 
 :(before "End Globals")
 // We'll use large type ordinals to mean "the following type of the variable".
@@ -212,97 +241,129 @@ if (type->value >= START_TYPE_INGREDIENTS
     && (type->value - START_TYPE_INGREDIENTS) < SIZE(get(Type, type->value).type_ingredient_names))
   return;
 
-:(scenario size_of_shape_shifting_exclusive_container)
-exclusive-container foo:_t [
-  x:_t
-  y:num
-]
-def main [
-  1:foo:num <- merge 0/x, 34
-  3:foo:point <- merge 0/x, 15, 16
-  6:foo:point <- merge 1/y, 23
-]
-+run: {1: ("foo" "number")} <- merge {0: "literal", "x": ()}, {34: "literal"}
-+mem: storing 0 in location 1
-+mem: storing 34 in location 2
-+run: {3: ("foo" "point")} <- merge {0: "literal", "x": ()}, {15: "literal"}, {16: "literal"}
-+mem: storing 0 in location 3
-+mem: storing 15 in location 4
-+mem: storing 16 in location 5
-+run: {6: ("foo" "point")} <- merge {1: "literal", "y": ()}, {23: "literal"}
-+mem: storing 1 in location 6
-+mem: storing 23 in location 7
-+run: return
-# no other stores
-% CHECK_EQ(trace_count_prefix("mem", "storing"), 7);
+:(code)
+void test_size_of_shape_shifting_exclusive_container() {
+  run(
+      "exclusive-container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:num <- merge 0/x, 34\n"
+      "  3:foo:point <- merge 0/x, 15, 16\n"
+      "  6:foo:point <- merge 1/y, 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: {1: (\"foo\" \"number\")} <- merge {0: \"literal\", \"x\": ()}, {34: \"literal\"}\n"
+      "mem: storing 0 in location 1\n"
+      "mem: storing 34 in location 2\n"
+      "run: {3: (\"foo\" \"point\")} <- merge {0: \"literal\", \"x\": ()}, {15: \"literal\"}, {16: \"literal\"}\n"
+      "mem: storing 0 in location 3\n"
+      "mem: storing 15 in location 4\n"
+      "mem: storing 16 in location 5\n"
+      "run: {6: (\"foo\" \"point\")} <- merge {1: \"literal\", \"y\": ()}, {23: \"literal\"}\n"
+      "mem: storing 1 in location 6\n"
+      "mem: storing 23 in location 7\n"
+      "run: return\n"
+  );
+  // no other stores
+  CHECK_EQ(trace_count_prefix("mem", "storing"), 7);
+}
 
 :(before "End variant_type Special-cases")
 if (contains_type_ingredient(element))
   replace_type_ingredients(element.type, type->right, info, " while computing variant type of exclusive-container");
 
-:(scenario get_on_shape_shifting_container)
-container foo:_t [
-  x:_t
-  y:num
-]
-def main [
-  1:foo:point <- merge 14, 15, 16
-  4:num <- get 1:foo:point, y:offset
-]
-+mem: storing 16 in location 4
-
-:(scenario get_on_shape_shifting_container_2)
-container foo:_t [
-  x:_t
-  y:num
-]
-def main [
-  1:foo:point <- merge 14, 15, 16
-  4:point <- get 1:foo:point, x:offset
-]
-+mem: storing 14 in location 4
-+mem: storing 15 in location 5
-
-:(scenario get_on_shape_shifting_container_3)
-container foo:_t [
-  x:_t
-  y:num
-]
-def main [
-  1:num/alloc-id, 2:num <- copy 0, 34
-  3:foo:&:point <- merge 1:&:point, 48
-  6:&:point <- get 1:foo:&:point, x:offset
-]
-+mem: storing 0 in location 6
-+mem: storing 34 in location 7
-
-:(scenario get_on_shape_shifting_container_inside_container)
-container foo:_t [
-  x:_t
-  y:num
-]
-container bar [
-  x:foo:point
-  y:num
-]
-def main [
-  1:bar <- merge 14, 15, 16, 17
-  5:num <- get 1:bar, 1:offset
-]
-+mem: storing 17 in location 5
-
-:(scenario get_on_complex_shape_shifting_container)
-container foo:_a:_b [
-  x:_a
-  y:_b
-]
-def main [
-  1:text <- new [abc]
-  {3: (foo number (address array character))} <- merge 34/x, 1:text/y
-  6:text <- get {3: (foo number (address array character))}, y:offset
-  8:bool <- equal 1:text, 6:text
-]
-+mem: storing 1 in location 8
+:(code)
+void test_get_on_shape_shifting_container() {
+  run(
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:point <- merge 14, 15, 16\n"
+      "  4:num <- get 1:foo:point, y:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 16 in location 4\n"
+  );
+}
+
+void test_get_on_shape_shifting_container_2() {
+  run(
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:point <- merge 14, 15, 16\n"
+      "  4:point <- get 1:foo:point, x:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 14 in location 4\n"
+      "mem: storing 15 in location 5\n"
+  );
+}
+
+void test_get_on_shape_shifting_container_3() {
+  run(
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:num/alloc-id, 2:num <- copy 0, 34\n"
+      "  3:foo:&:point <- merge 1:&:point, 48\n"
+      "  6:&:point <- get 1:foo:&:point, x:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 6\n"
+      "mem: storing 34 in location 7\n"
+  );
+}
+
+void test_get_on_shape_shifting_container_inside_container() {
+  run(
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+      "container bar [\n"
+      "  x:foo:point\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:bar <- merge 14, 15, 16, 17\n"
+      "  5:num <- get 1:bar, 1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 17 in location 5\n"
+  );
+}
+
+void test_get_on_complex_shape_shifting_container() {
+  run(
+      "container foo:_a:_b [\n"
+      "  x:_a\n"
+      "  y:_b\n"
+      "]\n"
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+      "  {3: (foo number (address array character))} <- merge 34/x, 1:text/y\n"
+      "  6:text <- get {3: (foo number (address array character))}, y:offset\n"
+      "  8:bool <- equal 1:text, 6:text\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 8\n"
+  );
+}
 
 :(before "End element_type Special-cases")
 replace_type_ingredients(element, type, info, " while computing element type of container");
@@ -341,20 +402,25 @@ if (t.kind == EXCLUSIVE_CONTAINER) {
   return result+1;
 }
 
-:(scenario complex_shape_shifting_exclusive_container)
-exclusive-container foo:_a [
-  x:_a
-  y:num
-]
-def main [
-  1:text <- new [abc]
-  3:foo:point <- merge 0/variant, 34/xx, 35/xy
-  10:point, 20:bool <- maybe-convert 3:foo:point, 0/variant
-]
-+mem: storing 1 in location 20
-+mem: storing 35 in location 11
-
 :(code)
+void test_complex_shape_shifting_exclusive_container() {
+  run(
+      "exclusive-container foo:_a [\n"
+      "  x:_a\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+      "  3:foo:point <- merge 0/variant, 34/xx, 35/xy\n"
+      "  10:point, 20:bool <- maybe-convert 3:foo:point, 0/variant\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 20\n"
+      "mem: storing 35 in location 11\n"
+  );
+}
+
 bool contains_type_ingredient(const reagent& x) {
   return contains_type_ingredient(x.type);
 }
@@ -560,114 +626,148 @@ bool has_nth_type(const type_tree* base, int n) {
   return has_nth_type(base->right, n-1);
 }
 
-:(scenario get_on_shape_shifting_container_error)
-% Hide_errors = true;
-container foo:_t [
-  x:_t
-  y:num
-]
-def main [
-  1:foo:point <- merge 14, 15, 16
-  10:num <- get 1:foo, 1:offset
-]
-# todo: improve error message
-+error: illegal type "foo" seems to be missing a type ingredient or three while computing element type of container
-
-:(scenario typos_in_container_definitions)
-% Hide_errors = true;
-container foo:_t [
-  x:adress:_t  # typo
-]
-def main [
-  local-scope
-  x:address:foo:num <- new {(foo num): type}
-]
-# no crash
-
-:(scenario typos_in_recipes)
-% Hide_errors = true;
-def foo [
-  local-scope
-  x:adress:array:number <- copy null  # typo
-]
-# shouldn't crash
+void test_get_on_shape_shifting_container_error() {
+  Hide_errors = true;
+  run(
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:point <- merge 14, 15, 16\n"
+      "  10:num <- get 1:foo, 1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: illegal type \"foo\" seems to be missing a type ingredient or three while computing element type of container\n"
+  );
+  // todo: improve error message
+}
+
+void test_typos_in_container_definitions() {
+  Hide_errors = true;
+  run(
+      "container foo:_t [\n"
+      "  x:adress:_t  # typo\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  x:address:foo:num <- new {(foo num): type}\n"
+      "]\n"
+  );
+  // no crash
+}
+
+void test_typos_in_recipes() {
+  Hide_errors = true;
+  run(
+      "def foo [\n"
+      "  local-scope\n"
+      "  x:adress:array:number <- copy null  # typo\n"
+      "]\n"
+  );
+  // shouldn't crash
+}
 
 //:: 'merge' on shape-shifting containers
 
-:(scenario merge_check_shape_shifting_container_containing_exclusive_container)
-container foo:_elem [
-  x:num
-  y:_elem
-]
-exclusive-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo:bar <- merge 23, 1/y, 34
-]
-+mem: storing 23 in location 1
-+mem: storing 1 in location 2
-+mem: storing 34 in location 3
-$error: 0
-
-:(scenario merge_check_shape_shifting_container_containing_exclusive_container_2)
-% Hide_errors = true;
-container foo:_elem [
-  x:num
-  y:_elem
-]
-exclusive-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo:bar <- merge 23, 1/y, 34, 35
-]
-+error: main: too many ingredients in '1:foo:bar <- merge 23, 1/y, 34, 35'
-
-:(scenario merge_check_shape_shifting_exclusive_container_containing_container)
-exclusive-container foo:_elem [
-  x:num
-  y:_elem
-]
-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo:bar <- merge 1/y, 23, 34
-]
-+mem: storing 1 in location 1
-+mem: storing 23 in location 2
-+mem: storing 34 in location 3
-$error: 0
-
-:(scenario merge_check_shape_shifting_exclusive_container_containing_container_2)
-exclusive-container foo:_elem [
-  x:num
-  y:_elem
-]
-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo:bar <- merge 0/x, 23
-]
-$error: 0
-
-:(scenario merge_check_shape_shifting_exclusive_container_containing_container_3)
-% Hide_errors = true;
-exclusive-container foo:_elem [
-  x:num
-  y:_elem
-]
-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo:bar <- merge 1/y, 23
-]
-+error: main: too few ingredients in '1:foo:bar <- merge 1/y, 23'
+void test_merge_check_shape_shifting_container_containing_exclusive_container() {
+  run(
+      "container foo:_elem [\n"
+      "  x:num\n"
+      "  y:_elem\n"
+      "]\n"
+      "exclusive-container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:bar <- merge 23, 1/y, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 23 in location 1\n"
+      "mem: storing 1 in location 2\n"
+      "mem: storing 34 in location 3\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_shape_shifting_container_containing_exclusive_container_2() {
+  Hide_errors = true;
+  run(
+      "container foo:_elem [\n"
+      "  x:num\n"
+      "  y:_elem\n"
+      "]\n"
+      "exclusive-container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:bar <- merge 23, 1/y, 34, 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too many ingredients in '1:foo:bar <- merge 23, 1/y, 34, 35'\n"
+  );
+}
+
+void test_merge_check_shape_shifting_exclusive_container_containing_container() {
+  run(
+      "exclusive-container foo:_elem [\n"
+      "  x:num\n"
+      "  y:_elem\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:bar <- merge 1/y, 23, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 1\n"
+      "mem: storing 23 in location 2\n"
+      "mem: storing 34 in location 3\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_shape_shifting_exclusive_container_containing_container_2() {
+  run(
+      "exclusive-container foo:_elem [\n"
+      "  x:num\n"
+      "  y:_elem\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:bar <- merge 0/x, 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_merge_check_shape_shifting_exclusive_container_containing_container_3() {
+  Hide_errors = true;
+  run(
+      "exclusive-container foo:_elem [\n"
+      "  x:num\n"
+      "  y:_elem\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo:bar <- merge 1/y, 23\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: too few ingredients in '1:foo:bar <- merge 1/y, 23'\n"
+  );
+}
diff --git a/056shape_shifting_recipe.cc b/056shape_shifting_recipe.cc
index 7eb5f0e3..3e3d9a8d 100644
--- a/056shape_shifting_recipe.cc
+++ b/056shape_shifting_recipe.cc
@@ -1,24 +1,29 @@
 //:: Like container definitions, recipes too can contain type parameters.
 
-:(scenario shape_shifting_recipe)
-def main [
-  10:point <- merge 14, 15
-  12:point <- foo 10:point
-]
-# non-matching variant
-def foo a:num -> result:num [
-  local-scope
-  load-ingredients
-  result <- copy 34
-]
-# matching shape-shifting variant
-def foo a:_t -> result:_t [
-  local-scope
-  load-ingredients
-  result <- copy a
-]
-+mem: storing 14 in location 12
-+mem: storing 15 in location 13
+void test_shape_shifting_recipe() {
+  run(
+      "def main [\n"
+      "  10:point <- merge 14, 15\n"
+      "  12:point <- foo 10:point\n"
+      "]\n"
+      // non-matching variant
+      "def foo a:num -> result:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- copy 34\n"
+      "]\n"
+      // matching shape-shifting variant
+      "def foo a:_t -> result:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- copy a\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 14 in location 12\n"
+      "mem: storing 15 in location 13\n"
+  );
+}
 
 //: Before anything else, disable transforms for shape-shifting recipes and
 //: make sure we never try to actually run a shape-shifting recipe. We should
@@ -536,138 +541,171 @@ void ensure_all_concrete_types(/*const*/ reagent& x, const recipe& exemplar) {
   }
 }
 
-:(scenario shape_shifting_recipe_2)
-def main [
-  10:point <- merge 14, 15
-  12:point <- foo 10:point
-]
-# non-matching shape-shifting variant
-def foo a:_t, b:_t -> result:num [
-  local-scope
-  load-ingredients
-  result <- copy 34
-]
-# matching shape-shifting variant
-def foo a:_t -> result:_t [
-  local-scope
-  load-ingredients
-  result <- copy a
-]
-+mem: storing 14 in location 12
-+mem: storing 15 in location 13
-
-:(scenario shape_shifting_recipe_nonroot)
-def main [
-  10:foo:point <- merge 14, 15, 16
-  20:point <- bar 10:foo:point
-]
-# shape-shifting recipe with type ingredient following some other type
-def bar a:foo:_t -> result:_t [
-  local-scope
-  load-ingredients
-  result <- get a, x:offset
-]
-container foo:_t [
-  x:_t
-  y:num
-]
-+mem: storing 14 in location 20
-+mem: storing 15 in location 21
-
-:(scenario shape_shifting_recipe_nested)
-container c:_a:_b [
-  a:_a
-  b:_b
-]
-def main [
-  s:text <- new [abc]
-  {x: (c (address array character) number)} <- merge s, 34
-  foo x
-]
-def foo x:c:_bar:_baz [
-  local-scope
-  load-ingredients
-]
-# no errors
-
-:(scenario shape_shifting_recipe_type_deduction_ignores_offsets)
-def main [
-  10:foo:point <- merge 14, 15, 16
-  20:point <- bar 10:foo:point
-]
-def bar a:foo:_t -> result:_t [
-  local-scope
-  load-ingredients
-  x:num <- copy 1
-  result <- get a, x:offset  # shouldn't collide with other variable
-]
-container foo:_t [
-  x:_t
-  y:num
-]
-+mem: storing 14 in location 20
-+mem: storing 15 in location 21
-
-:(scenario shape_shifting_recipe_empty)
-def main [
-  foo 1
-]
-# shape-shifting recipe with no body
-def foo a:_t [
-]
-# shouldn't crash
-
-:(scenario shape_shifting_recipe_handles_shape_shifting_new_ingredient)
-def main [
-  1:&:foo:point <- bar 3
-  11:foo:point <- copy *1:&:foo:point
-]
-container foo:_t [
-  x:_t
-  y:num
-]
-def bar x:num -> result:&:foo:_t [
-  local-scope
-  load-ingredients
-  # new refers to _t in its ingredient *value*
-  result <- new {(foo _t) : type}
-]
-+mem: storing 0 in location 11
-+mem: storing 0 in location 12
-+mem: storing 0 in location 13
-
-:(scenario shape_shifting_recipe_handles_shape_shifting_new_ingredient_2)
-def main [
-  1:&:foo:point <- bar 3
-  11:foo:point <- copy *1:&:foo:point
-]
-def bar x:num -> result:&:foo:_t [
-  local-scope
-  load-ingredients
-  # new refers to _t in its ingredient *value*
-  result <- new {(foo _t) : type}
-]
-# container defined after use
-container foo:_t [
-  x:_t
-  y:num
-]
-+mem: storing 0 in location 11
-+mem: storing 0 in location 12
-+mem: storing 0 in location 13
-
-:(scenario shape_shifting_recipe_called_with_dummy)
-def main [
-  _ <- bar 34
-]
-def bar x:_t -> result:&:_t [
-  local-scope
-  load-ingredients
-  result <- copy null
-]
-$error: 0
+void test_shape_shifting_recipe_2() {
+  run(
+      "def main [\n"
+      "  10:point <- merge 14, 15\n"
+      "  12:point <- foo 10:point\n"
+      "]\n"
+      // non-matching shape-shifting variant
+      "def foo a:_t, b:_t -> result:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- copy 34\n"
+      "]\n"
+      // matching shape-shifting variant
+      "def foo a:_t -> result:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- copy a\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 14 in location 12\n"
+      "mem: storing 15 in location 13\n"
+  );
+}
+
+void test_shape_shifting_recipe_nonroot() {
+  run(
+      "def main [\n"
+      "  10:foo:point <- merge 14, 15, 16\n"
+      "  20:point <- bar 10:foo:point\n"
+      "]\n"
+      // shape-shifting recipe with type ingredient following some other type
+      "def bar a:foo:_t -> result:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- get a, x:offset\n"
+      "]\n"
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 14 in location 20\n"
+      "mem: storing 15 in location 21\n"
+  );
+}
+
+void test_shape_shifting_recipe_nested() {
+  run(
+      "container c:_a:_b [\n"
+      "  a:_a\n"
+      "  b:_b\n"
+      "]\n"
+      "def main [\n"
+      "  s:text <- new [abc]\n"
+      "  {x: (c (address array character) number)} <- merge s, 34\n"
+      "  foo x\n"
+      "]\n"
+      "def foo x:c:_bar:_baz [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "]\n"
+  );
+  // no errors
+}
+
+void test_shape_shifting_recipe_type_deduction_ignores_offsets() {
+  run(
+      "def main [\n"
+      "  10:foo:point <- merge 14, 15, 16\n"
+      "  20:point <- bar 10:foo:point\n"
+      "]\n"
+      "def bar a:foo:_t -> result:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  x:num <- copy 1\n"
+      "  result <- get a, x:offset  # shouldn't collide with other variable\n"
+      "]\n"
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 14 in location 20\n"
+      "mem: storing 15 in location 21\n"
+  );
+}
+
+void test_shape_shifting_recipe_empty() {
+  run(
+      "def main [\n"
+      "  foo 1\n"
+      "]\n"
+      // shape-shifting recipe with no body
+      "def foo a:_t [\n"
+      "]\n"
+  );
+  // shouldn't crash
+}
+
+void test_shape_shifting_recipe_handles_shape_shifting_new_ingredient() {
+  run(
+      "def main [\n"
+      "  1:&:foo:point <- bar 3\n"
+      "  11:foo:point <- copy *1:&:foo:point\n"
+      "]\n"
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+      "def bar x:num -> result:&:foo:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+         // new refers to _t in its ingredient *value*
+      "  result <- new {(foo _t) : type}\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 11\n"
+      "mem: storing 0 in location 12\n"
+      "mem: storing 0 in location 13\n"
+  );
+}
+
+void test_shape_shifting_recipe_handles_shape_shifting_new_ingredient_2() {
+  run(
+      "def main [\n"
+      "  1:&:foo:point <- bar 3\n"
+      "  11:foo:point <- copy *1:&:foo:point\n"
+      "]\n"
+      "def bar x:num -> result:&:foo:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+         // new refers to _t in its ingredient *value*
+      "  result <- new {(foo _t) : type}\n"
+      "]\n"
+      // container defined after use
+      "container foo:_t [\n"
+      "  x:_t\n"
+      "  y:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 11\n"
+      "mem: storing 0 in location 12\n"
+      "mem: storing 0 in location 13\n"
+  );
+}
+
+void test_shape_shifting_recipe_called_with_dummy() {
+  run(
+      "def main [\n"
+      "  _ <- bar 34\n"
+      "]\n"
+      "def bar x:_t -> result:&:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- copy null\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(code)
 // this one needs a little more fine-grained control
 void test_shape_shifting_new_ingredient_does_not_pollute_global_namespace() {
   // if you specialize a shape-shifting recipe that allocates a type-ingredient..
@@ -695,460 +733,575 @@ void test_shape_shifting_new_ingredient_does_not_pollute_global_namespace() {
 }
 
 //: specializing a type ingredient with a compound type
-:(scenario shape_shifting_recipe_supports_compound_types)
-def main [
-  1:&:point <- new point:type
-  *1:&:point <- put *1:&:point, y:offset, 34
-  3:&:point <- bar 1:&:point  # specialize _t to address:point
-  5:point <- copy *3:&:point
-]
-def bar a:_t -> result:_t [
-  local-scope
-  load-ingredients
-  result <- copy a
-]
-+mem: storing 34 in location 6
+void test_shape_shifting_recipe_supports_compound_types() {
+  run(
+      "def main [\n"
+      "  1:&:point <- new point:type\n"
+      "  *1:&:point <- put *1:&:point, y:offset, 34\n"
+      "  3:&:point <- bar 1:&:point  # specialize _t to address:point\n"
+      "  5:point <- copy *3:&:point\n"
+      "]\n"
+      "def bar a:_t -> result:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- copy a\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 6\n"
+  );
+}
 
 //: specializing a type ingredient with a compound type -- while *inside* another compound type
-:(scenario shape_shifting_recipe_supports_compound_types_2)
-container foo:_t [
-  value:_t
-]
-def bar x:&:foo:_t -> result:_t [
-  local-scope
-  load-ingredients
-  result <- get *x, value:offset
-]
-def main [
-  1:&:foo:&:point <- new {(foo address point): type}
-  2:&:point <- bar 1:&:foo:&:point
-]
-# no errors; call to 'bar' successfully specialized
-
-:(scenario shape_shifting_recipe_error)
-% Hide_errors = true;
-def main [
-  a:num <- copy 3
-  b:&:num <- foo a
-]
-def foo a:_t -> b:_t [
-  load-ingredients
-  b <- copy a
-]
-+error: main: no call found for 'b:&:num <- foo a'
-
-:(scenario specialize_inside_recipe_without_header)
-def main [
-  foo 3
-]
-def foo [
-  local-scope
-  x:num <- next-ingredient  # ensure no header
-  1:num/raw <- bar x  # call a shape-shifting recipe
-]
-def bar x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- add x, 1
-]
-+mem: storing 4 in location 1
-
-:(scenario specialize_with_literal)
-def main [
-  local-scope
-  # permit literal to map to number
-  1:num/raw <- foo 3
-]
-def foo x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- add x, 1
-]
-+mem: storing 4 in location 1
-
-:(scenario specialize_with_literal_2)
-def main [
-  local-scope
-  # permit literal to map to character
-  1:char/raw <- foo 3
-]
-def foo x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- add x, 1
-]
-+mem: storing 4 in location 1
-
-:(scenario specialize_with_literal_3)
-def main [
-  local-scope
-  # permit '0' to map to address to shape-shifting type-ingredient
-  1:&:char/raw <- foo null
-]
-def foo x:&:_elem -> y:&:_elem [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 0 in location 1
-$error: 0
-
-:(scenario specialize_with_literal_4)
-% Hide_errors = true;
-def main [
-  local-scope
-  # ambiguous call: what's the type of its ingredient?!
-  foo 0
-]
-def foo x:&:_elem -> y:&:_elem [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+error: main: instruction 'foo' has no valid specialization
-
-:(scenario specialize_with_literal_5)
-def main [
-  foo 3, 4  # recipe mapping two variables to literals
-]
-def foo x:_elem, y:_elem [
-  local-scope
-  load-ingredients
-  1:num/raw <- add x, y
-]
-+mem: storing 7 in location 1
-
-:(scenario multiple_shape_shifting_variants)
-# try to call two different shape-shifting recipes with the same name
-def main [
-  e1:d1:num <- merge 3
-  e2:d2:num <- merge 4, 5
-  1:num/raw <- foo e1
-  2:num/raw <- foo e2
-]
-# the two shape-shifting definitions
-def foo a:d1:_elem -> b:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-def foo a:d2:_elem -> b:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-# the shape-shifting containers they use
-container d1:_elem [
-  x:_elem
-]
-container d2:_elem [
-  x:num
-  y:_elem
-]
-+mem: storing 34 in location 1
-+mem: storing 35 in location 2
-
-:(scenario multiple_shape_shifting_variants_2)
-# static dispatch between shape-shifting variants, _including pointer lookups_
-def main [
-  e1:d1:num <- merge 3
-  e2:&:d2:num <- new {(d2 number): type}
-  1:num/raw <- foo e1
-  2:num/raw <- foo *e2  # different from previous scenario
-]
-def foo a:d1:_elem -> b:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-def foo a:d2:_elem -> b:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-container d1:_elem [
-  x:_elem
-]
-container d2:_elem [
-  x:num
-  y:_elem
-]
-+mem: storing 34 in location 1
-+mem: storing 35 in location 2
-
-:(scenario missing_type_in_shape_shifting_recipe)
-% Hide_errors = true;
-def main [
-  a:d1:num <- merge 3
-  foo a
-]
-def foo a:d1:_elem -> b:num [
-  local-scope
-  load-ingredients
-  copy e  # no such variable
-  return 34
-]
-container d1:_elem [
-  x:_elem
-]
-+error: foo: unknown type for 'e' in 'copy e' (check the name for typos)
-+error: specializing foo: missing type for 'e'
-# and it doesn't crash
-
-:(scenario missing_type_in_shape_shifting_recipe_2)
-% Hide_errors = true;
-def main [
-  a:d1:num <- merge 3
-  foo a
-]
-def foo a:d1:_elem -> b:num [
-  local-scope
-  load-ingredients
-  get e, x:offset  # unknown variable in a 'get', which does some extra checking
-  return 34
-]
-container d1:_elem [
-  x:_elem
-]
-+error: foo: unknown type for 'e' in 'get e, x:offset' (check the name for typos)
-+error: specializing foo: missing type for 'e'
-# and it doesn't crash
-
-:(scenarios transform)
-:(scenario specialize_recursive_shape_shifting_recipe)
-def main [
-  1:num <- copy 34
-  2:num <- foo 1:num
-]
-def foo x:_elem -> y:num [
-  local-scope
-  load-ingredients
-  {
-    break
-    y:num <- foo x
-  }
-  return y
-]
-+transform: new specialization: foo_2
-# transform terminates
-
-:(scenarios run)
-:(scenario specialize_most_similar_variant)
-def main [
-  1:&:num <- new number:type
-  10:num <- foo 1:&:num
-]
-def foo x:_elem -> y:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-def foo x:&:_elem -> y:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-+mem: storing 35 in location 10
-
-:(scenario specialize_most_similar_variant_2)
-# version with headers padded with lots of unrelated concrete types
-def main [
-  1:num <- copy 23
-  2:&:@:num <- copy null
-  4:num <- foo 2:&:@:num, 1:num
-]
-# variant with concrete type
-def foo dummy:&:@:num, x:num -> y:num, dummy:&:@:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-# shape-shifting variant
-def foo dummy:&:@:num, x:_elem -> y:num, dummy:&:@:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-# prefer the concrete variant
-+mem: storing 34 in location 4
-
-:(scenario specialize_most_similar_variant_3)
-def main [
-  1:text <- new [abc]
-  foo 1:text
-]
-def foo x:text [
-  10:num <- copy 34
-]
-def foo x:&:_elem [
-  10:num <- copy 35
-]
-# make sure the more precise version was used
-+mem: storing 34 in location 10
-
-:(scenario specialize_literal_as_number)
-def main [
-  1:num <- foo 23
-]
-def foo x:_elem -> y:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-def foo x:char -> y:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-+mem: storing 34 in location 1
-
-:(scenario specialize_literal_as_number_2)
-# version calling with literal
-def main [
-  1:num <- foo 0
-]
-# variant with concrete type
-def foo x:num -> y:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-# shape-shifting variant
-def foo x:&:_elem -> y:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-# prefer the concrete variant, ignore concrete types in scoring the shape-shifting variant
-+mem: storing 34 in location 1
-
-:(scenario specialize_literal_as_address)
-def main [
-  1:num <- foo null
-]
-# variant with concrete address type
-def foo x:&:num -> y:num [
-  local-scope
-  load-ingredients
-  return 34
-]
-# shape-shifting variant
-def foo x:&:_elem -> y:num [
-  local-scope
-  load-ingredients
-  return 35
-]
-# prefer the concrete variant, ignore concrete types in scoring the shape-shifting variant
-+mem: storing 34 in location 1
-
-:(scenario missing_type_during_specialization)
-% Hide_errors = true;
-# define a shape-shifting recipe
-def foo a:_elem [
-]
-# define a container with field 'z'
-container foo2 [
-  z:num
-]
-def main [
-  local-scope
-  x:foo2 <- merge 34
-  y:num <- get x, z:offse  # typo in 'offset'
-  # define a variable with the same name 'z'
-  z:num <- copy 34
-  # trigger specialization of the shape-shifting recipe
-  foo z
-]
-# shouldn't crash
-
-:(scenario missing_type_during_specialization2)
-% Hide_errors = true;
-# define a shape-shifting recipe
-def foo a:_elem [
-]
-# define a container with field 'z'
-container foo2 [
-  z:num
-]
-def main [
-  local-scope
-  x:foo2 <- merge 34
-  y:num <- get x, z:offse  # typo in 'offset'
-  # define a variable with the same name 'z'
-  z:&:num <- copy 34
-  # trigger specialization of the shape-shifting recipe
-  foo *z
-]
-# shouldn't crash
-
-:(scenario tangle_shape_shifting_recipe)
-# shape-shifting recipe
-def foo a:_elem [
-  local-scope
-  load-ingredients
-  <label1>
-]
-# tangle some code that refers to the type ingredient
-after <label1> [
-  b:_elem <- copy a
-]
-# trigger specialization
-def main [
-  local-scope
-  foo 34
-]
-$error: 0
-
-:(scenario tangle_shape_shifting_recipe_with_type_abbreviation)
-# shape-shifting recipe
-def foo a:_elem [
-  local-scope
-  load-ingredients
-  <label1>
-]
-# tangle some code that refers to the type ingredient
-after <label1> [
-  b:bool <- copy false  # type abbreviation
-]
-# trigger specialization
-def main [
-  local-scope
-  foo 34
-]
-$error: 0
-
-:(scenario shape_shifting_recipe_coexists_with_primitive)
-# recipe overloading a primitive with a generic type
-def add a:&:foo:_elem [
-  assert 0, [should not get here]
-]
-def main [
-  # call primitive add with literal 0
-  add 0, 0
-]
-$error: 0
-
-:(scenario specialization_heuristic_test_1)
-# modeled on the 'buffer' container in text.mu
-container foo_buffer:_elem [
-  x:num
-]
-def main [
-  append 1:&:foo_buffer:char/raw, 2:text/raw
-]
-def append buf:&:foo_buffer:_elem, x:_elem -> buf:&:foo_buffer:_elem [
-  local-scope
-  load-ingredients
-  stash 34
-]
-def append buf:&:foo_buffer:char, x:_elem -> buf:&:foo_buffer:char [
-  local-scope
-  load-ingredients
-  stash 35
-]
-def append buf:&:foo_buffer:_elem, x:&:@:_elem -> buf:&:foo_buffer:_elem [
-  local-scope
-  load-ingredients
-  stash 36
-]
-+app: 36
+void test_shape_shifting_recipe_supports_compound_types_2() {
+  run(
+      "container foo:_t [\n"
+      "  value:_t\n"
+      "]\n"
+      "def bar x:&:foo:_t -> result:_t [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- get *x, value:offset\n"
+      "]\n"
+      "def main [\n"
+      "  1:&:foo:&:point <- new {(foo address point): type}\n"
+      "  2:&:point <- bar 1:&:foo:&:point\n"
+      "]\n"
+  );
+  // no errors; call to 'bar' successfully specialized
+}
+
+void test_shape_shifting_recipe_error() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  a:num <- copy 3\n"
+      "  b:&:num <- foo a\n"
+      "]\n"
+      "def foo a:_t -> b:_t [\n"
+      "  load-ingredients\n"
+      "  b <- copy a\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: no call found for 'b:&:num <- foo a'\n"
+  );
+}
+
+void test_specialize_inside_recipe_without_header() {
+  run(
+      "def main [\n"
+      "  foo 3\n"
+      "]\n"
+      "def foo [\n"
+      "  local-scope\n"
+      "  x:num <- next-ingredient  # ensure no header\n"
+      "  1:num/raw <- bar x  # call a shape-shifting recipe\n"
+      "]\n"
+      "def bar x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- add x, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 1\n"
+  );
+}
+
+void test_specialize_with_literal() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+         // permit literal to map to number
+      "  1:num/raw <- foo 3\n"
+      "]\n"
+      "def foo x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- add x, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 1\n"
+  );
+}
+
+void test_specialize_with_literal_2() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+         // permit literal to map to character
+      "  1:char/raw <- foo 3\n"
+      "]\n"
+      "def foo x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- add x, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 1\n"
+  );
+}
+
+void test_specialize_with_literal_3() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+         // permit '0' to map to address to shape-shifting type-ingredient
+      "  1:&:char/raw <- foo null\n"
+      "]\n"
+      "def foo x:&:_elem -> y:&:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 1\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_specialize_with_literal_4() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+         // ambiguous call: what's the type of its ingredient?!
+      "  foo 0\n"
+      "]\n"
+      "def foo x:&:_elem -> y:&:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: instruction 'foo' has no valid specialization\n"
+  );
+}
+
+void test_specialize_with_literal_5() {
+  run(
+      "def main [\n"
+      "  foo 3, 4\n"  // recipe mapping two variables to literals
+      "]\n"
+      "def foo x:_elem, y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  1:num/raw <- add x, y\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 7 in location 1\n"
+  );
+}
+
+void test_multiple_shape_shifting_variants() {
+  run(
+      // try to call two different shape-shifting recipes with the same name
+      "def main [\n"
+      "  e1:d1:num <- merge 3\n"
+      "  e2:d2:num <- merge 4, 5\n"
+      "  1:num/raw <- foo e1\n"
+      "  2:num/raw <- foo e2\n"
+      "]\n"
+      // the two shape-shifting definitions
+      "def foo a:d1:_elem -> b:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      "def foo a:d2:_elem -> b:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+      // the shape-shifting containers they use
+      "container d1:_elem [\n"
+      "  x:_elem\n"
+      "]\n"
+      "container d2:_elem [\n"
+      "  x:num\n"
+      "  y:_elem\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+      "mem: storing 35 in location 2\n"
+  );
+}
+
+void test_multiple_shape_shifting_variants_2() {
+  run(
+      // static dispatch between shape-shifting variants, _including pointer lookups_
+      "def main [\n"
+      "  e1:d1:num <- merge 3\n"
+      "  e2:&:d2:num <- new {(d2 number): type}\n"
+      "  1:num/raw <- foo e1\n"
+      "  2:num/raw <- foo *e2\n"  // different from previous scenario
+      "]\n"
+      "def foo a:d1:_elem -> b:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      "def foo a:d2:_elem -> b:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+      "container d1:_elem [\n"
+      "  x:_elem\n"
+      "]\n"
+      "container d2:_elem [\n"
+      "  x:num\n"
+      "  y:_elem\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+      "mem: storing 35 in location 2\n"
+  );
+}
+
+void test_missing_type_in_shape_shifting_recipe() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  a:d1:num <- merge 3\n"
+      "  foo a\n"
+      "]\n"
+      "def foo a:d1:_elem -> b:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  copy e\n"  // no such variable
+      "  return 34\n"
+      "]\n"
+      "container d1:_elem [\n"
+      "  x:_elem\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: unknown type for 'e' in 'copy e' (check the name for typos)\n"
+      "error: specializing foo: missing type for 'e'\n"
+  );
+  // and it doesn't crash
+}
+
+void test_missing_type_in_shape_shifting_recipe_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  a:d1:num <- merge 3\n"
+      "  foo a\n"
+      "]\n"
+      "def foo a:d1:_elem -> b:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  get e, x:offset\n"  // unknown variable in a 'get', which does some extra checking
+      "  return 34\n"
+      "]\n"
+      "container d1:_elem [\n"
+      "  x:_elem\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: unknown type for 'e' in 'get e, x:offset' (check the name for typos)\n"
+      "error: specializing foo: missing type for 'e'\n"
+  );
+  // and it doesn't crash
+}
+
+void test_specialize_recursive_shape_shifting_recipe() {
+  transform(
+      "def main [\n"
+      "  1:num <- copy 34\n"
+      "  2:num <- foo 1:num\n"
+      "]\n"
+      "def foo x:_elem -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  {\n"
+      "    break\n"
+      "    y:num <- foo x\n"
+      "  }\n"
+      "  return y\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: new specialization: foo_2\n"
+  );
+  // transform terminates
+}
+
+void test_specialize_most_similar_variant() {
+  run(
+      "def main [\n"
+      "  1:&:num <- new number:type\n"
+      "  10:num <- foo 1:&:num\n"
+      "]\n"
+      "def foo x:_elem -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      "def foo x:&:_elem -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 35 in location 10\n"
+  );
+}
+
+void test_specialize_most_similar_variant_2() {
+  run(
+      // version with headers padded with lots of unrelated concrete types
+      "def main [\n"
+      "  1:num <- copy 23\n"
+      "  2:&:@:num <- copy null\n"
+      "  4:num <- foo 2:&:@:num, 1:num\n"
+      "]\n"
+      // variant with concrete type
+      "def foo dummy:&:@:num, x:num -> y:num, dummy:&:@:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      // shape-shifting variant
+      "def foo dummy:&:@:num, x:_elem -> y:num, dummy:&:@:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+  );
+  // prefer the concrete variant
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 4\n"
+  );
+}
+
+void test_specialize_most_similar_variant_3() {
+  run(
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+      "  foo 1:text\n"
+      "]\n"
+      "def foo x:text [\n"
+      "  10:num <- copy 34\n"
+      "]\n"
+      "def foo x:&:_elem [\n"
+      "  10:num <- copy 35\n"
+      "]\n"
+  );
+  // make sure the more precise version was used
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 10\n"
+  );
+}
+
+void test_specialize_literal_as_number() {
+  run(
+      "def main [\n"
+      "  1:num <- foo 23\n"
+      "]\n"
+      "def foo x:_elem -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      "def foo x:char -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
+
+void test_specialize_literal_as_number_2() {
+  run(
+      // version calling with literal
+      "def main [\n"
+      "  1:num <- foo 0\n"
+      "]\n"
+      // variant with concrete type
+      "def foo x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      // shape-shifting variant
+      "def foo x:&:_elem -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+  );
+  // prefer the concrete variant, ignore concrete types in scoring the shape-shifting variant
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
+
+void test_specialize_literal_as_address() {
+  run(
+      "def main [\n"
+      "  1:num <- foo null\n"
+      "]\n"
+      // variant with concrete address type
+      "def foo x:&:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 34\n"
+      "]\n"
+      // shape-shifting variant
+      "def foo x:&:_elem -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return 35\n"
+      "]\n"
+  );
+  // prefer the concrete variant, ignore concrete types in scoring the shape-shifting variant
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
+
+void test_missing_type_during_specialization() {
+  Hide_errors = true;
+  run(
+      // define a shape-shifting recipe
+      "def foo a:_elem [\n"
+      "]\n"
+      // define a container with field 'z'
+      "container foo2 [\n"
+      "  z:num\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  x:foo2 <- merge 34\n"
+      "  y:num <- get x, z:offse  # typo in 'offset'\n"
+         // define a variable with the same name 'z'
+      "  z:num <- copy 34\n"
+      "  foo z\n"
+      "]\n"
+  );
+  // shouldn't crash
+}
+
+void test_missing_type_during_specialization2() {
+  Hide_errors = true;
+  run(
+      // define a shape-shifting recipe
+      "def foo a:_elem [\n"
+      "]\n"
+      // define a container with field 'z'
+      "container foo2 [\n"
+      "  z:num\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  x:foo2 <- merge 34\n"
+      "  y:num <- get x, z:offse  # typo in 'offset'\n"
+         // define a variable with the same name 'z'
+      "  z:&:num <- copy 34\n"
+         // trigger specialization of the shape-shifting recipe
+      "  foo *z\n"
+      "]\n"
+  );
+  // shouldn't crash
+}
+
+void test_tangle_shape_shifting_recipe() {
+  run(
+      // shape-shifting recipe
+      "def foo a:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  <label1>\n"
+      "]\n"
+      // tangle some code that refers to the type ingredient
+      "after <label1> [\n"
+      "  b:_elem <- copy a\n"
+      "]\n"
+      // trigger specialization
+      "def main [\n"
+      "  local-scope\n"
+      "  foo 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_tangle_shape_shifting_recipe_with_type_abbreviation() {
+  run(
+      // shape-shifting recipe
+      "def foo a:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  <label1>\n"
+      "]\n"
+      // tangle some code that refers to the type ingredient
+      "after <label1> [\n"
+      "  b:bool <- copy false\n"  // type abbreviation
+      "]\n"
+      // trigger specialization
+      "def main [\n"
+      "  local-scope\n"
+      "  foo 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_shape_shifting_recipe_coexists_with_primitive() {
+  run(
+      // recipe overloading a primitive with a generic type
+      "def add a:&:foo:_elem [\n"
+      "  assert 0, [should not get here]\n"
+      "]\n"
+      "def main [\n"
+         // call primitive add with literal 0
+      "  add 0, 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_specialization_heuristic_test_1() {
+  run(
+      // modeled on the 'buffer' container in text.mu
+      "container foo_buffer:_elem [\n"
+      "  x:num\n"
+      "]\n"
+      "def main [\n"
+      "  append 1:&:foo_buffer:char/raw, 2:text/raw\n"
+      "]\n"
+      "def append buf:&:foo_buffer:_elem, x:_elem -> buf:&:foo_buffer:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  stash 34\n"
+      "]\n"
+      "def append buf:&:foo_buffer:char, x:_elem -> buf:&:foo_buffer:char [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  stash 35\n"
+      "]\n"
+      "def append buf:&:foo_buffer:_elem, x:&:@:_elem -> buf:&:foo_buffer:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  stash 36\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: 36\n"
+  );
+}
diff --git a/057immutable.cc b/057immutable.cc
index e1feac7a..16c2a6ab 100644
--- a/057immutable.cc
+++ b/057immutable.cc
@@ -4,335 +4,410 @@
 //: One hole for now: variables in surrounding spaces are implicitly mutable.
 //: [tag: todo]
 
-:(scenario can_modify_ingredients_that_are_also_products)
-# mutable container
-def main [
-  local-scope
-  p:point <- merge 34, 35
-  p <- foo p
-]
-def foo p:point -> p:point [
-  local-scope
-  load-ingredients
-  p <- put p, x:offset, 34
-]
-$error: 0
-
-:(scenario can_modify_ingredients_that_are_also_products_2)
-def main [
-  local-scope
-  p:&:point <- new point:type
-  p <- foo p
-]
-# mutable address to container
-def foo p:&:point -> p:&:point [
-  local-scope
-  load-ingredients
-  *p <- put *p, x:offset, 34
-]
-$error: 0
-
-:(scenario can_modify_ingredients_that_are_also_products_3)
-def main [
-  local-scope
-  p:&:@:num <- new number:type, 3
-  p <- foo p
-]
-# mutable address
-def foo p:&:@:num -> p:&:@:num [
-  local-scope
-  load-ingredients
-  *p <- put-index *p, 0, 34
-]
-$error: 0
-
-:(scenario ignore_literal_ingredients_for_immutability_checks)
-def main [
-  local-scope
-  p:&:d1 <- new d1:type
-  q:num <- foo p
-]
-def foo p:&:d1 -> q:num [
-  local-scope
-  load-ingredients
-  x:&:d1 <- new d1:type
-  *x <- put *x, p:offset, 34  # ignore this 'p'
-  return 36
-]
-container d1 [
-  p:num
-  q:num
-]
-$error: 0
-
-:(scenario cannot_modify_immutable_ingredients)
-% Hide_errors = true;
-def main [
-  local-scope
-  x:&:num <- new number:type
-  foo x
-]
-# immutable address to primitive
-def foo x:&:num [
-  local-scope
-  load-ingredients
-  *x <- copy 34
-]
-+error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product
-
-:(scenario cannot_modify_immutable_containers)
-% Hide_errors = true;
-def main [
-  local-scope
-  x:point-number <- merge 34, 35, 36
-  foo x
-]
-# immutable container
-def foo x:point-number [
-  local-scope
-  load-ingredients
-  # copy an element: ok
-  y:point <- get x, xy:offset
-  # modify the element: boom
-  # This could be ok if y contains no addresses, but we're not going to try to be that smart.
-  # It also makes the rules easier to reason about. If it's just an ingredient, just don't try to change it.
-  y <- put y, x:offset, 37
-]
-+error: foo: cannot modify 'y' in instruction 'y <- put y, x:offset, 37' because that would modify 'x' which is an ingredient of recipe foo but not also a product
-
-:(scenario can_modify_immutable_pointers)
-def main [
-  local-scope
-  x:&:num <- new number:type
-  foo x
-]
-def foo x:&:num [
-  local-scope
-  load-ingredients
-  # modify the address, not the payload
-  x <- copy null
-]
-$error: 0
-
-:(scenario can_modify_immutable_pointers_but_not_their_payloads)
-% Hide_errors = true;
-def main [
-  local-scope
-  x:&:num <- new number:type
-  foo x
-]
-def foo x:&:num [
-  local-scope
-  load-ingredients
-  # modify address; ok
-  x <- new number:type
-  # modify payload: boom
-  # this could be ok, but we're not going to try to be that smart
-  *x <- copy 34
-]
-+error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product
-
-:(scenario cannot_call_mutating_recipes_on_immutable_ingredients)
-% Hide_errors = true;
-def main [
-  local-scope
-  p:&:point <- new point:type
-  foo p
-]
-def foo p:&:point [
-  local-scope
-  load-ingredients
-  bar p
-]
-def bar p:&:point -> p:&:point [
-  local-scope
-  load-ingredients
-  # p could be modified here, but it doesn't have to be, it's already marked
-  # mutable in the header
-]
-+error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product
-
-:(scenario cannot_modify_copies_of_immutable_ingredients)
-% Hide_errors = true;
-def main [
-  local-scope
-  p:&:point <- new point:type
-  foo p
-]
-def foo p:&:point [
-  local-scope
-  load-ingredients
-  q:&:point <- copy p
-  *q <- put *q, x:offset, 34
-]
-+error: foo: cannot modify 'q' in instruction '*q <- put *q, x:offset, 34' because that would modify p which is an ingredient of recipe foo but not also a product
-
-:(scenario can_modify_copies_of_mutable_ingredients)
-def main [
-  local-scope
-  p:&:point <- new point:type
-  foo p
-]
-def foo p:&:point -> p:&:point [
-  local-scope
-  load-ingredients
-  q:&:point <- copy p
-  *q <- put *q, x:offset, 34
-]
-$error: 0
-
-:(scenario cannot_modify_address_inside_immutable_ingredients)
-% Hide_errors = true;
-container foo [
-  x:&:@:num  # contains an address
-]
-def main [
-  # don't run anything
-]
-def foo a:&:foo [
-  local-scope
-  load-ingredients
-  x:&:@:num <- get *a, x:offset  # just a regular get of the container
-  *x <- put-index *x, 0, 34  # but then a put-index on the result
-]
-+error: foo: cannot modify 'x' in instruction '*x <- put-index *x, 0, 34' because that would modify a which is an ingredient of recipe foo but not also a product
-
-:(scenario cannot_modify_address_inside_immutable_ingredients_2)
-container foo [
-  x:&:@:num  # contains an address
-]
-def main [
-  # don't run anything
-]
-def foo a:&:foo [
-  local-scope
-  load-ingredients
-  b:foo <- merge null
-  # modify b, completely unrelated to immutable ingredient a
-  x:&:@:num <- get b, x:offset
-  *x <- put-index *x, 0, 34
-]
-$error: 0
-
-:(scenario cannot_modify_address_inside_immutable_ingredients_3)
-% Hide_errors = true;
-def main [
-  # don't run anything
-]
-def foo a:&:@:&:num [
-  local-scope
-  load-ingredients
-  x:&:num <- index *a, 0  # just a regular index of the array
-  *x <- copy 34  # but then modify the result
-]
-+error: foo: cannot modify 'x' in instruction '*x <- copy 34' because that would modify a which is an ingredient of recipe foo but not also a product
-
-:(scenario cannot_modify_address_inside_immutable_ingredients_4)
-def main [
-  # don't run anything
-]
-def foo a:&:@:&:num [
-  local-scope
-  load-ingredients
-  b:&:@:&:num <- new {(address number): type}, 3
-  # modify b, completely unrelated to immutable ingredient a
-  x:&:num <- index *b, 0
-  *x <- copy 34
-]
-$error: 0
-
-:(scenario latter_ingredient_of_index_is_immutable)
-def main [
-  # don't run anything
-]
-def foo a:&:@:&:@:num, b:num -> a:&:@:&:@:num [
-  local-scope
-  load-ingredients
-  x:&:@:num <- index *a, b
-  *x <- put-index *x, 0, 34
-]
-$error: 0
-
-:(scenario can_traverse_immutable_ingredients)
-container test-list [
-  next:&:test-list
-]
-def main [
-  local-scope
-  p:&:test-list <- new test-list:type
-  foo p
-]
-def foo p:&:test-list [
-  local-scope
-  load-ingredients
-  p2:&:test-list <- bar p
-]
-def bar x:&:test-list -> y:&:test-list [
-  local-scope
-  load-ingredients
-  y <- get *x, next:offset
-]
-$error: 0
-
-:(scenario treat_optional_ingredients_as_mutable)
-def main [
-  k:&:num <- new number:type
-  test k
-]
-# recipe taking an immutable address ingredient
-def test k:&:num [
-  local-scope
-  load-ingredients
-  foo k
-]
-# ..calling a recipe with an optional address ingredient
-def foo -> [
-  local-scope
-  load-ingredients
-  k:&:num, found?:bool <- next-ingredient
-  # we don't further check k for immutability, but assume it's mutable
-]
-$error: 0
-
-:(scenario treat_optional_ingredients_as_mutable_2)
-% Hide_errors = true;
-def main [
-  local-scope
-  p:&:point <- new point:type
-  foo p
-]
-def foo p:&:point [
-  local-scope
-  load-ingredients
-  bar p
-]
-def bar [
-  local-scope
-  load-ingredients
-  p:&:point <- next-ingredient  # optional ingredient; assumed to be mutable
-]
-+error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product
+void test_can_modify_ingredients_that_are_also_products() {
+  run(
+      // mutable container
+      "def main [\n"
+      "  local-scope\n"
+      "  p:point <- merge 34, 35\n"
+      "  p <- foo p\n"
+      "]\n"
+      "def foo p:point -> p:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  p <- put p, x:offset, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_can_modify_ingredients_that_are_also_products_2() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:point <- new point:type\n"
+      "  p <- foo p\n"
+      "]\n"
+      // mutable address to container
+      "def foo p:&:point -> p:&:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  *p <- put *p, x:offset, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_can_modify_ingredients_that_are_also_products_3() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:@:num <- new number:type, 3\n"
+      "  p <- foo p\n"
+      "]\n"
+      // mutable address
+      "def foo p:&:@:num -> p:&:@:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  *p <- put-index *p, 0, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_ignore_literal_ingredients_for_immutability_checks() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:d1 <- new d1:type\n"
+      "  q:num <- foo p\n"
+      "]\n"
+      "def foo p:&:d1 -> q:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  x:&:d1 <- new d1:type\n"
+      "  *x <- put *x, p:offset, 34\n"  // ignore this 'p'
+      "  return 36\n"
+      "]\n"
+      "container d1 [\n"
+      "  p:num\n"
+      "  q:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_cannot_modify_immutable_ingredients() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:&:num <- new number:type\n"
+      "  foo x\n"
+      "]\n"
+      // immutable address to primitive
+      "def foo x:&:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  *x <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product\n"
+  );
+}
+
+void test_cannot_modify_immutable_containers() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:point-number <- merge 34, 35, 36\n"
+      "  foo x\n"
+      "]\n"
+      // immutable container
+      "def foo x:point-number [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+         // copy an element: ok
+      "  y:point <- get x, xy:offset\n"
+         // modify the element: boom
+         // This could be ok if y contains no addresses, but we're not going to try to be that smart.
+         // It also makes the rules easier to reason about. If it's just an ingredient, just don't try to change it.
+      "  y <- put y, x:offset, 37\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'y' in instruction 'y <- put y, x:offset, 37' because that would modify 'x' which is an ingredient of recipe foo but not also a product\n"
+  );
+}
+
+void test_can_modify_immutable_pointers() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:&:num <- new number:type\n"
+      "  foo x\n"
+      "]\n"
+      "def foo x:&:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+         // modify the address, not the payload
+      "  x <- copy null\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_can_modify_immutable_pointers_but_not_their_payloads() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:&:num <- new number:type\n"
+      "  foo x\n"
+      "]\n"
+      "def foo x:&:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      // modify address: ok
+      "  x <- new number:type\n"
+      // modify payload: boom
+      // this could be ok, but we're not going to try to be that smart
+      "  *x <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product\n"
+  );
+}
+
+void test_cannot_call_mutating_recipes_on_immutable_ingredients() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:point <- new point:type\n"
+      "  foo p\n"
+      "]\n"
+      "def foo p:&:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  bar p\n"
+      "]\n"
+      "def bar p:&:point -> p:&:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+         // p could be modified here, but it doesn't have to be; it's already
+         // marked mutable in the header
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product\n"
+  );
+}
+
+void test_cannot_modify_copies_of_immutable_ingredients() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:point <- new point:type\n"
+      "  foo p\n"
+      "]\n"
+      "def foo p:&:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  q:&:point <- copy p\n"
+      "  *q <- put *q, x:offset, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'q' in instruction '*q <- put *q, x:offset, 34' because that would modify p which is an ingredient of recipe foo but not also a product\n"
+  );
+}
+
+void test_can_modify_copies_of_mutable_ingredients() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:point <- new point:type\n"
+      "  foo p\n"
+      "]\n"
+      "def foo p:&:point -> p:&:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  q:&:point <- copy p\n"
+      "  *q <- put *q, x:offset, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_cannot_modify_address_inside_immutable_ingredients() {
+  Hide_errors = true;
+  run(
+      "container foo [\n"
+      "  x:&:@:num\n"  // contains an address
+      "]\n"
+      "def main [\n"
+      "]\n"
+      "def foo a:&:foo [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  x:&:@:num <- get *a, x:offset\n"  // just a regular get of the container
+      "  *x <- put-index *x, 0, 34\n"  // but then a put-index on the result
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'x' in instruction '*x <- put-index *x, 0, 34' because that would modify a which is an ingredient of recipe foo but not also a product\n"
+  );
+}
+
+void test_cannot_modify_address_inside_immutable_ingredients_2() {
+  run(
+      "container foo [\n"
+      "  x:&:@:num\n"  // contains an address
+      "]\n"
+      "def main [\n"
+         // don't run anything
+      "]\n"
+      "def foo a:&:foo [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  b:foo <- merge null\n"
+         // modify b, completely unrelated to immutable ingredient a
+      "  x:&:@:num <- get b, x:offset\n"
+      "  *x <- put-index *x, 0, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_cannot_modify_address_inside_immutable_ingredients_3() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+        // don't run anything
+      "]\n"
+      "def foo a:&:@:&:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  x:&:num <- index *a, 0\n"  // just a regular index of the array
+      "  *x <- copy 34\n"  // but then modify the result
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'x' in instruction '*x <- copy 34' because that would modify a which is an ingredient of recipe foo but not also a product\n"
+  );
+}
+
+void test_cannot_modify_address_inside_immutable_ingredients_4() {
+  run(
+      "def main [\n"
+         // don't run anything
+      "]\n"
+      "def foo a:&:@:&:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  b:&:@:&:num <- new {(address number): type}, 3\n"
+         // modify b, completely unrelated to immutable ingredient a
+      "  x:&:num <- index *b, 0\n"
+      "  *x <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_latter_ingredient_of_index_is_immutable() {
+  run(
+      "def main [\n"
+         // don't run anything
+      "]\n"
+      "def foo a:&:@:&:@:num, b:num -> a:&:@:&:@:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  x:&:@:num <- index *a, b\n"
+      "  *x <- put-index *x, 0, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_can_traverse_immutable_ingredients() {
+  run(
+      "container test-list [\n"
+      "  next:&:test-list\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:test-list <- new test-list:type\n"
+      "  foo p\n"
+      "]\n"
+      "def foo p:&:test-list [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  p2:&:test-list <- bar p\n"
+      "]\n"
+      "def bar x:&:test-list -> y:&:test-list [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- get *x, next:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_treat_optional_ingredients_as_mutable() {
+  run(
+      "def main [\n"
+      "  k:&:num <- new number:type\n"
+      "  test k\n"
+      "]\n"
+      // recipe taking an immutable address ingredient
+      "def test k:&:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  foo k\n"
+      "]\n"
+      // ..calling a recipe with an optional address ingredient
+      "def foo -> [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  k:&:num, found?:bool <- next-ingredient\n"
+         // we don't further check k for immutability, but assume it's mutable
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_treat_optional_ingredients_as_mutable_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:point <- new point:type\n"
+      "  foo p\n"
+      "]\n"
+      "def foo p:&:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  bar p\n"
+      "]\n"
+      "def bar [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  p:&:point <- next-ingredient\n"  // optional ingredient; assumed to be mutable
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product\n"
+  );
+}
 
 //: when checking for immutable ingredients, remember to take space into account
-:(scenario check_space_of_reagents_in_immutability_checks)
-def main [
-  a:space/names:new-closure <- new-closure
-  b:&:num <- new number:type
-  run-closure b:&:num, a:space
-]
-def new-closure [
-  local-scope
-  x:&:num <- new number:type
-  return default-space/names:new-closure
-]
-def run-closure x:&:num, s:space/names:new-closure [
-  local-scope
-  load-ingredients
-  0:space/names:new-closure <- copy s
-  # different space; always mutable
-  *x:&:num/space:1 <- copy 34
-]
-$error: 0
+void test_check_space_of_reagents_in_immutability_checks() {
+  run(
+      "def main [\n"
+      "  a:space/names:new-closure <- new-closure\n"
+      "  b:&:num <- new number:type\n"
+      "  run-closure b:&:num, a:space\n"
+      "]\n"
+      "def new-closure [\n"
+      "  local-scope\n"
+      "  x:&:num <- new number:type\n"
+      "  return default-space/names:new-closure\n"
+      "]\n"
+      "def run-closure x:&:num, s:space/names:new-closure [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  0:space/names:new-closure <- copy s\n"
+         // different space; always mutable
+      "  *x:&:num/space:1 <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 :(before "End Transforms")
 Transform.push_back(check_immutable_ingredients);  // idempotent
@@ -449,32 +524,35 @@ bool name_and_space_lt::operator()(const reagent& a, const reagent& b) const {
   return a.name < b.name;
 }
 
-:(scenarios transform)
-:(scenario immutability_infects_contained_in_variables)
-% Hide_errors = true;
-container test-list [
-  value:num
-  next:&:test-list
-]
-def main [
-  local-scope
-  p:&:test-list <- new test-list:type
-  foo p
-]
-def foo p:&:test-list [  # p is immutable
-  local-scope
-  load-ingredients
-  p2:&:test-list <- test-next p  # p2 is immutable
-  *p2 <- put *p2, value:offset, 34
-]
-def test-next x:&:test-list -> y:&:test-list/contained-in:x [
-  local-scope
-  load-ingredients
-  y <- get *x, next:offset
-]
-+error: foo: cannot modify 'p2' in instruction '*p2 <- put *p2, value:offset, 34' because that would modify p which is an ingredient of recipe foo but not also a product
+void test_immutability_infects_contained_in_variables() {
+  Hide_errors = true;
+  transform(
+      "container test-list [\n"
+      "  value:num\n"
+      "  next:&:test-list\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:test-list <- new test-list:type\n"
+      "  foo p\n"
+      "]\n"
+      "def foo p:&:test-list [\n"  // p is immutable
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  p2:&:test-list <- test-next p\n"  // p2 is immutable
+      "  *p2 <- put *p2, value:offset, 34\n"
+      "]\n"
+      "def test-next x:&:test-list -> y:&:test-list/contained-in:x [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- get *x, next:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: cannot modify 'p2' in instruction '*p2 <- put *p2, value:offset, 34' because that would modify p which is an ingredient of recipe foo but not also a product\n"
+  );
+}
 
-:(code)
 void check_immutable_ingredient_in_instruction(const instruction& inst, const set<reagent, name_and_space_lt>& current_ingredient_and_aliases, const string& original_ingredient_name, const recipe& caller) {
   // first check if the instruction is directly modifying something it shouldn't
   for (int i = 0;  i < SIZE(inst.products);  ++i) {
@@ -562,34 +640,36 @@ set<int> ingredient_indices(const instruction& inst, const set<reagent, name_and
 //:
 //: We'll see if we end up wanting to abuse /contained-in for other reasons.
 
-:(scenarios transform)
-:(scenario can_modify_contained_in_addresses)
-container test-list [
-  value:num
-  next:&:test-list
-]
-def main [
-  local-scope
-  p:&:test-list <- new test-list:type
-  foo p
-]
-def foo p:&:test-list -> p:&:test-list [
-  local-scope
-  load-ingredients
-  p2:&:test-list <- test-next p
-  p <- test-remove p2, p
-]
-def test-next x:&:test-list -> y:&:test-list [
-  local-scope
-  load-ingredients
-  y <- get *x, next:offset
-]
-def test-remove x:&:test-list/contained-in:from, from:&:test-list -> from:&:test-list [
-  local-scope
-  load-ingredients
-  *x <- put *x, value:offset, 34  # can modify x
-]
-$error: 0
+void test_can_modify_contained_in_addresses() {
+  transform(
+      "container test-list [\n"
+      "  value:num\n"
+      "  next:&:test-list\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  p:&:test-list <- new test-list:type\n"
+      "  foo p\n"
+      "]\n"
+      "def foo p:&:test-list -> p:&:test-list [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  p2:&:test-list <- test-next p\n"
+      "  p <- test-remove p2, p\n"
+      "]\n"
+      "def test-next x:&:test-list -> y:&:test-list [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- get *x, next:offset\n"
+      "]\n"
+      "def test-remove x:&:test-list/contained-in:from, from:&:test-list -> from:&:test-list [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  *x <- put *x, value:offset, 34\n"  // can modify x
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 :(before "End Immutable Ingredients Special-cases")
 if (has_property(current_ingredient, "contained-in")) {
@@ -602,27 +682,34 @@ if (has_property(current_ingredient, "contained-in")) {
   continue;
 }
 
-:(scenario contained_in_product)
-container test-list [
-  value:num
-  next:&:test-list
-]
-def foo x:&:test-list/contained-in:result -> result:&:test-list [
-  local-scope
-  load-ingredients
-  result <- copy null
-]
-$error: 0
-
-:(scenario contained_in_is_mutable)
-container test-list [
-  value:num
-  next:&:test-list
-]
-def foo x:&:test-list/contained-in:result -> result:&:test-list [
-  local-scope
-  load-ingredients
-  result <- copy x
-  put *x, value:offset, 34
-]
-$error: 0
+:(code)
+void test_contained_in_product() {
+  transform(
+      "container test-list [\n"
+      "  value:num\n"
+      "  next:&:test-list\n"
+      "]\n"
+      "def foo x:&:test-list/contained-in:result -> result:&:test-list [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- copy null\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+void test_contained_in_is_mutable() {
+  transform(
+      "container test-list [\n"
+      "  value:num\n"
+      "  next:&:test-list\n"
+      "]\n"
+      "def foo x:&:test-list/contained-in:result -> result:&:test-list [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- copy x\n"
+      "  put *x, value:offset, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
diff --git a/060rewrite_literal_string.cc b/060rewrite_literal_string.cc
index 7bfc45cb..3e64fd7c 100644
--- a/060rewrite_literal_string.cc
+++ b/060rewrite_literal_string.cc
@@ -1,15 +1,20 @@
 //: allow using literal strings anywhere that will accept immutable strings
 
-:(scenario passing_literals_to_recipes)
-def main [
-  1:num/raw <- foo [abc]
-]
-def foo x:text -> n:num [
-  local-scope
-  load-ingredients
-  n <- length *x
-]
-+mem: storing 3 in location 1
+void test_passing_literals_to_recipes() {
+  run(
+      "def main [\n"
+      "  1:num/raw <- foo [abc]\n"
+      "]\n"
+      "def foo x:text -> n:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  n <- length *x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 1\n"
+  );
+}
 
 :(before "End Instruction Inserting/Deleting Transforms")
 initialize_transform_rewrite_literal_string_to_text();
diff --git a/062convert_ingredients_to_text.cc b/062convert_ingredients_to_text.cc
index 8739f47b..a0a74bd4 100644
--- a/062convert_ingredients_to_text.cc
+++ b/062convert_ingredients_to_text.cc
@@ -1,55 +1,79 @@
 //: make some recipes more friendly by trying to auto-convert their ingredients to text
 
-:(scenarios transform)
-:(scenario rewrite_stashes_to_text)
-def main [
-  local-scope
-  n:num <- copy 34
-  stash n
-]
-+transform: {stash_2_0: ("address" "array" "character")} <- to-text-line {n: "number"}
-+transform: stash {stash_2_0: ("address" "array" "character")}
-
-:(scenario rewrite_traces_to_text)
-def main [
-  local-scope
-  n:num <- copy 34
-  trace 2, [app], n
-]
-+transform: {trace_2_2: ("address" "array" "character")} <- to-text-line {n: "number"}
-+transform: trace {2: "literal"}, {"app": "literal-string"}, {trace_2_2: ("address" "array" "character")}
+void test_rewrite_stashes_to_text() {
+  transform(
+      "def main [\n"
+      "  local-scope\n"
+      "  n:num <- copy 34\n"
+      "  stash n\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: {stash_2_0: (\"address\" \"array\" \"character\")} <- to-text-line {n: \"number\"}\n"
+      "transform: stash {stash_2_0: (\"address\" \"array\" \"character\")}\n"
+  );
+}
+
+void test_rewrite_traces_to_text() {
+  transform(
+      "def main [\n"
+      "  local-scope\n"
+      "  n:num <- copy 34\n"
+      "  trace 2, [app], n\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: {trace_2_2: (\"address\" \"array\" \"character\")} <- to-text-line {n: \"number\"}\n"
+      "transform: trace {2: \"literal\"}, {\"app\": \"literal-string\"}, {trace_2_2: (\"address\" \"array\" \"character\")}\n"
+  );
+}
 
 //: special case: rewrite attempts to stash contents of most arrays to avoid
 //: passing addresses around
 
-:(scenario rewrite_stashes_of_arrays)
-def main [
-  local-scope
-  n:&:@:num <- new number:type, 3
-  stash *n
-]
-+transform: {stash_2_0: ("address" "array" "character")} <- array-to-text-line {n: ("address" "array" "number")}
-+transform: stash {stash_2_0: ("address" "array" "character")}
-
-:(scenario ignore_stashes_of_static_arrays)
-def main [
-  local-scope
-  n:@:num:3 <- create-array
-  stash n
-]
-+transform: stash {n: ("array" "number" "3")}
-
-:(scenario rewrite_stashes_of_recipe_header_products)
-container foo [
-  x:num
-]
-def bar -> x:foo [
-  local-scope
-  load-ingredients
-  x <- merge 34
-  stash x
-]
-+transform: stash {stash_2_0: ("address" "array" "character")}
+void test_rewrite_stashes_of_arrays() {
+  transform(
+      "def main [\n"
+      "  local-scope\n"
+      "  n:&:@:num <- new number:type, 3\n"
+      "  stash *n\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: {stash_2_0: (\"address\" \"array\" \"character\")} <- array-to-text-line {n: (\"address\" \"array\" \"number\")}\n"
+      "transform: stash {stash_2_0: (\"address\" \"array\" \"character\")}\n"
+  );
+}
+
+void test_ignore_stashes_of_static_arrays() {
+  transform(
+      "def main [\n"
+      "  local-scope\n"
+      "  n:@:num:3 <- create-array\n"
+      "  stash n\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: stash {n: (\"array\" \"number\" \"3\")}\n"
+  );
+}
+
+void test_rewrite_stashes_of_recipe_header_products() {
+  transform(
+      "container foo [\n"
+      "  x:num\n"
+      "]\n"
+      "def bar -> x:foo [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  x <- merge 34\n"
+      "  stash x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "transform: stash {stash_2_0: (\"address\" \"array\" \"character\")}\n"
+  );
+}
 
 //: misplaced; should be in instruction inserting/deleting transforms, but has
 //: prerequisites: deduce_types_from_header and check_or_set_types_by_name
@@ -152,29 +176,37 @@ name_before_rewrite.clear();
 :(before "End next_instruction(curr)")
 curr->name_before_rewrite = curr->name;
 
-:(scenarios run)
-:(scenario append_other_types_to_text)
-def main [
-  local-scope
-  n:num <- copy 11
-  c:char <- copy 111/o
-  a:text <- append [abc], 10, n, c
-  expected:text <- new [abc1011o]
-  10:bool/raw <- equal a, expected
-]
+:(code)
+void test_append_other_types_to_text() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  n:num <- copy 11\n"
+      "  c:char <- copy 111/o\n"
+      "  a:text <- append [abc], 10, n, c\n"
+      "  expected:text <- new [abc1011o]\n"
+      "  10:bool/raw <- equal a, expected\n"
+      "]\n"
+  );
+}
 
 //: Make sure that the new system is strictly better than just the 'stash'
 //: primitive by itself.
 
-:(scenario rewrite_stash_continues_to_fall_back_to_default_implementation)
-# type without a to-text implementation
-container foo [
-  x:num
-  y:num
-]
-def main [
-  local-scope
-  x:foo <- merge 34, 35
-  stash x
-]
-+app: 34 35
+void test_rewrite_stash_continues_to_fall_back_to_default_implementation() {
+  run(
+      // type without a to-text implementation
+      "container foo [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  x:foo <- merge 34, 35\n"
+      "  stash x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: 34 35\n"
+  );
+}
diff --git a/069hash.cc b/069hash.cc
index 3a371233..67897d00 100644
--- a/069hash.cc
+++ b/069hash.cc
@@ -123,207 +123,267 @@ size_t hash_iter(size_t h, size_t input) {
   return h;
 }
 
-:(scenario hash_container_checks_all_elements)
-container foo [
-  x:num
-  y:char
-]
-def main [
-  1:foo <- merge 34, 97/a
-  3:num <- hash 1:foo
-  return-unless 3:num
-  4:foo <- merge 34, 98/a
-  6:num <- hash 4:foo
-  return-unless 6:num
-  7:bool <- equal 3:num, 6:num
-]
-# hash on containers includes all elements
-+mem: storing 0 in location 7
+void test_hash_container_checks_all_elements() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:char\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 34, 97/a\n"
+      "  3:num <- hash 1:foo\n"
+      "  return-unless 3:num\n"
+      "  4:foo <- merge 34, 98/a\n"
+      "  6:num <- hash 4:foo\n"
+      "  return-unless 6:num\n"
+      "  7:bool <- equal 3:num, 6:num\n"
+      "]\n"
+  );
+  // hash on containers includes all elements
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 7\n"
+  );
+}
 
-:(scenario hash_exclusive_container_checks_all_elements)
-exclusive-container foo [
-  x:bar
-  y:num
-]
-container bar [
-  a:num
-  b:num
-]
-def main [
-  1:foo <- merge 0/x, 34, 35
-  4:num <- hash 1:foo
-  return-unless 4:num
-  5:foo <- merge 0/x, 34, 36
-  8:num <- hash 5:foo
-  return-unless 8:num
-  9:bool <- equal 4:num, 8:num
-]
-# hash on containers includes all elements
-+mem: storing 0 in location 9
+void test_hash_exclusive_container_checks_all_elements() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:bar\n"
+      "  y:num\n"
+      "]\n"
+      "container bar [\n"
+      "  a:num\n"
+      "  b:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 0/x, 34, 35\n"
+      "  4:num <- hash 1:foo\n"
+      "  return-unless 4:num\n"
+      "  5:foo <- merge 0/x, 34, 36\n"
+      "  8:num <- hash 5:foo\n"
+      "  return-unless 8:num\n"
+      "  9:bool <- equal 4:num, 8:num\n"
+      "]\n"
+  );
+  // hash on containers includes all elements
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 9\n"
+  );
+}
 
-:(scenario hash_can_ignore_container_elements)
-container foo [
-  x:num
-  y:char/ignore-for-hash
-]
-def main [
-  1:foo <- merge 34, 97/a
-  3:num <- hash 1:foo
-  return-unless 3:num
-  4:foo <- merge 34, 98/a
-  6:num <- hash 4:foo
-  return-unless 6:num
-  7:bool <- equal 3:num, 6:num
-]
-# hashes match even though y is different
-+mem: storing 1 in location 7
+void test_hash_can_ignore_container_elements() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:char/ignore-for-hash\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 34, 97/a\n"
+      "  3:num <- hash 1:foo\n"
+      "  return-unless 3:num\n"
+      "  4:foo <- merge 34, 98/a\n"
+      "  6:num <- hash 4:foo\n"
+      "  return-unless 6:num\n"
+      "  7:bool <- equal 3:num, 6:num\n"
+      "]\n"
+  );
+  // hashes match even though y is different
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 7\n"
+  );
+}
 
 //: These properties aren't necessary for hash, they just test that the
 //: current implementation works like we think it does.
 
-:(scenario hash_of_zero_address)
-def main [
-  1:&:num <- copy null
-  2:num <- hash 1:&:num
-]
-+mem: storing 0 in location 2
+void test_hash_of_zero_address() {
+  run(
+      "def main [\n"
+      "  1:&:num <- copy null\n"
+      "  2:num <- hash 1:&:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 2\n"
+  );
+}
 
 //: This is probably too aggressive, but we need some way to avoid depending
 //: on the precise bit pattern of a floating-point number.
-:(scenario hash_of_numbers_ignores_fractional_part)
-def main [
-  1:num <- hash 1.5
-  2:num <- hash 1
-  3:bool <- equal 1:num, 2:num
-]
-+mem: storing 1 in location 3
+void test_hash_of_numbers_ignores_fractional_part() {
+  run(
+      "def main [\n"
+      "  1:num <- hash 1.5\n"
+      "  2:num <- hash 1\n"
+      "  3:bool <- equal 1:num, 2:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 3\n"
+  );
+}
 
-:(scenario hash_of_array_same_as_string)
-def main [
-  10:num <- copy 3
-  11:num <- copy 97
-  12:num <- copy 98
-  13:num <- copy 99
-  2:num <- hash 10:@:num/unsafe
-  return-unless 2:num
-  3:text <- new [abc]
-  4:num <- hash 3:text
-  return-unless 4:num
-  5:bool <- equal 2:num, 4:num
-]
-+mem: storing 1 in location 5
+void test_hash_of_array_same_as_string() {
+  run(
+      "def main [\n"
+      "  10:num <- copy 3\n"
+      "  11:num <- copy 97\n"
+      "  12:num <- copy 98\n"
+      "  13:num <- copy 99\n"
+      "  2:num <- hash 10:@:num/unsafe\n"
+      "  return-unless 2:num\n"
+      "  3:text <- new [abc]\n"
+      "  4:num <- hash 3:text\n"
+      "  return-unless 4:num\n"
+      "  5:bool <- equal 2:num, 4:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 5\n"
+  );
+}
 
-:(scenario hash_ignores_address_value)
-def main [
-  1:&:num <- new number:type
-  *1:&:num <- copy 34
-  2:num <- hash 1:&:num
-  3:&:num <- new number:type
-  *3:&:num <- copy 34
-  4:num <- hash 3:&:num
-  5:bool <- equal 2:num, 4:num
-]
-# different addresses hash to the same result as long as the values the point to do so
-+mem: storing 1 in location 5
+void test_hash_ignores_address_value() {
+  run(
+      "def main [\n"
+      "  1:&:num <- new number:type\n"
+      "  *1:&:num <- copy 34\n"
+      "  2:num <- hash 1:&:num\n"
+      "  3:&:num <- new number:type\n"
+      "  *3:&:num <- copy 34\n"
+      "  4:num <- hash 3:&:num\n"
+      "  5:bool <- equal 2:num, 4:num\n"
+      "]\n"
+  );
+  // different addresses hash to the same result as long as the values the point to do so
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 5\n"
+  );
+}
 
-:(scenario hash_container_depends_only_on_elements)
-container foo [
-  x:num
-  y:char
-]
-container bar [
-  x:num
-  y:char
-]
-def main [
-  1:foo <- merge 34, 97/a
-  3:num <- hash 1:foo
-  return-unless 3:num
-  4:bar <- merge 34, 97/a
-  6:num <- hash 4:bar
-  return-unless 6:num
-  7:bool <- equal 3:num, 6:num
-]
-# containers with identical elements return identical hashes
-+mem: storing 1 in location 7
+void test_hash_container_depends_only_on_elements() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:char\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:char\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 34, 97/a\n"
+      "  3:num <- hash 1:foo\n"
+      "  return-unless 3:num\n"
+      "  4:bar <- merge 34, 97/a\n"
+      "  6:num <- hash 4:bar\n"
+      "  return-unless 6:num\n"
+      "  7:bool <- equal 3:num, 6:num\n"
+      "]\n"
+  );
+  // containers with identical elements return identical hashes
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 7\n"
+  );
+}
 
-:(scenario hash_container_depends_only_on_elements_2)
-container foo [
-  x:num
-  y:char
-  z:&:num
-]
-def main [
-  1:&:num <- new number:type
-  *1:&:num <- copy 34
-  2:foo <- merge 34, 97/a, 1:&:num
-  5:num <- hash 2:foo
-  return-unless 5:num
-  6:&:num <- new number:type
-  *6:&:num <- copy 34
-  7:foo <- merge 34, 97/a, 6:&:num
-  10:num <- hash 7:foo
-  return-unless 10:num
-  11:bool <- equal 5:num, 10:num
-]
-# containers with identical 'leaf' elements return identical hashes
-+mem: storing 1 in location 11
+void test_hash_container_depends_only_on_elements_2() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:char\n"
+      "  z:&:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:&:num <- new number:type\n"
+      "  *1:&:num <- copy 34\n"
+      "  2:foo <- merge 34, 97/a, 1:&:num\n"
+      "  5:num <- hash 2:foo\n"
+      "  return-unless 5:num\n"
+      "  6:&:num <- new number:type\n"
+      "  *6:&:num <- copy 34\n"
+      "  7:foo <- merge 34, 97/a, 6:&:num\n"
+      "  10:num <- hash 7:foo\n"
+      "  return-unless 10:num\n"
+      "  11:bool <- equal 5:num, 10:num\n"
+      "]\n"
+  );
+  // containers with identical 'leaf' elements return identical hashes
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 11\n"
+  );
+}
 
-:(scenario hash_container_depends_only_on_elements_3)
-container foo [
-  x:num
-  y:char
-  z:bar
-]
-container bar [
-  x:num
-  y:num
-]
-def main [
-  1:foo <- merge 34, 97/a, 47, 48
-  6:num <- hash 1:foo
-  return-unless 6:num
-  7:foo <- merge 34, 97/a, 47, 48
-  12:num <- hash 7:foo
-  return-unless 12:num
-  13:bool <- equal 6:num, 12:num
-]
-# containers with identical 'leaf' elements return identical hashes
-+mem: storing 1 in location 13
+void test_hash_container_depends_only_on_elements_3() {
+  run(
+      "container foo [\n"
+      "  x:num\n"
+      "  y:char\n"
+      "  z:bar\n"
+      "]\n"
+      "container bar [\n"
+      "  x:num\n"
+      "  y:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 34, 97/a, 47, 48\n"
+      "  6:num <- hash 1:foo\n"
+      "  return-unless 6:num\n"
+      "  7:foo <- merge 34, 97/a, 47, 48\n"
+      "  12:num <- hash 7:foo\n"
+      "  return-unless 12:num\n"
+      "  13:bool <- equal 6:num, 12:num\n"
+      "]\n"
+  );
+  // containers with identical 'leaf' elements return identical hashes
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 13\n"
+  );
+}
 
-:(scenario hash_exclusive_container_ignores_tag)
-exclusive-container foo [
-  x:bar
-  y:num
-]
-container bar [
-  a:num
-  b:num
-]
-def main [
-  1:foo <- merge 0/x, 34, 35
-  4:num <- hash 1:foo
-  return-unless 4:num
-  5:bar <- merge 34, 35
-  7:num <- hash 5:bar
-  return-unless 7:num
-  8:bool <- equal 4:num, 7:num
-]
-# hash on containers includes all elements
-+mem: storing 1 in location 8
+void test_hash_exclusive_container_ignores_tag() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:bar\n"
+      "  y:num\n"
+      "]\n"
+      "container bar [\n"
+      "  a:num\n"
+      "  b:num\n"
+      "]\n"
+      "def main [\n"
+      "  1:foo <- merge 0/x, 34, 35\n"
+      "  4:num <- hash 1:foo\n"
+      "  return-unless 4:num\n"
+      "  5:bar <- merge 34, 35\n"
+      "  7:num <- hash 5:bar\n"
+      "  return-unless 7:num\n"
+      "  8:bool <- equal 4:num, 7:num\n"
+      "]\n"
+  );
+  // hash on containers includes all elements
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 8\n"
+  );
+}
 
 //: An older version that supported only strings.
 //: Hash functions are subtle and easy to get wrong, so we keep the old
 //: version around and check that the new one is consistent with it.
 
-:(scenario hash_matches_old_version)
-def main [
-  1:text <- new [abc]
-  3:num <- hash 1:text
-  4:num <- hash_old 1:text
-  5:bool <- equal 3:num, 4:num
-]
-+mem: storing 1 in location 5
+void test_hash_matches_old_version() {
+  run(
+      "def main [\n"
+      "  1:text <- new [abc]\n"
+      "  3:num <- hash 1:text\n"
+      "  4:num <- hash_old 1:text\n"
+      "  5:bool <- equal 3:num, 4:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 5\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 HASH_OLD,
diff --git a/072recipe.cc b/072recipe.cc
index 43b1d013..a3bb00bf 100644
--- a/072recipe.cc
+++ b/072recipe.cc
@@ -2,16 +2,21 @@
 //: also like to make the recipe a variable, pass recipes to "higher-order"
 //: recipes, return recipes from recipes and so on.
 
-:(scenario call_literal_recipe)
-def main [
-  1:num <- call f, 34
-]
-def f x:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 34 in location 1
+void test_call_literal_recipe() {
+  run(
+      "def main [\n"
+      "  1:num <- call f, 34\n"
+      "]\n"
+      "def f x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
 :(before "End Mu Types Initialization")
 put(Type_ordinal, "recipe-literal", 0);
@@ -51,14 +56,19 @@ bool is_matching_non_recipe_literal(const reagent& x, const string& name) {
 
 //: It's confusing to use variable names that are also recipe names. Always
 //: assume variable types override recipe literals.
-:(scenario error_on_recipe_literal_used_as_a_variable)
-% Hide_errors = true;
-def main [
-  local-scope
-  a:bool <- equal break 0
-  break:bool <- copy 0
-]
-+error: main: missing type for 'break' in 'a:bool <- equal break, 0'
+void test_error_on_recipe_literal_used_as_a_variable() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  a:bool <- equal break 0\n"
+      "  break:bool <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: missing type for 'break' in 'a:bool <- equal break, 0'\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 CALL,
@@ -99,103 +109,139 @@ case CALL: {
   break;
 }
 
-:(scenario call_variable)
-def main [
-  {1: (recipe number -> number)} <- copy f
-  2:num <- call {1: (recipe number -> number)}, 34
-]
-def f x:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 34 in location 2
-
-:(scenario call_literal_recipe_repeatedly)
-def main [
-  1:num <- call f, 34
-  1:num <- call f, 35
-]
-def f x:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 34 in location 1
-+mem: storing 35 in location 1
-
-:(scenario call_shape_shifting_recipe)
-def main [
-  1:num <- call f, 34
-]
-def f x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 34 in location 1
-
-:(scenario call_shape_shifting_recipe_inside_shape_shifting_recipe)
-def main [
-  1:num <- f 34
-]
-def f x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- call g x
-]
-def g x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 34 in location 1
-
-:(scenario call_shape_shifting_recipe_repeatedly_inside_shape_shifting_recipe)
-def main [
-  1:num <- f 34
-]
-def f x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- call g x
-  y <- call g x
-]
-def g x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 34 in location 1
+:(code)
+void test_call_variable() {
+  run(
+      "def main [\n"
+      "  {1: (recipe number -> number)} <- copy f\n"
+      "  2:num <- call {1: (recipe number -> number)}, 34\n"
+      "]\n"
+      "def f x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 2\n"
+  );
+}
+
+void test_call_literal_recipe_repeatedly() {
+  run(
+      "def main [\n"
+      "  1:num <- call f, 34\n"
+      "  1:num <- call f, 35\n"
+      "]\n"
+      "def f x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+      "mem: storing 35 in location 1\n"
+  );
+}
+
+void test_call_shape_shifting_recipe() {
+  run(
+      "def main [\n"
+      "  1:num <- call f, 34\n"
+      "]\n"
+      "def f x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
+
+void test_call_shape_shifting_recipe_inside_shape_shifting_recipe() {
+  run(
+      "def main [\n"
+      "  1:num <- f 34\n"
+      "]\n"
+      "def f x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- call g x\n"
+      "]\n"
+      "def g x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
+
+void test_call_shape_shifting_recipe_repeatedly_inside_shape_shifting_recipe() {
+  run(
+      "def main [\n"
+      "  1:num <- f 34\n"
+      "]\n"
+      "def f x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- call g x\n"
+      "  y <- call g x\n"
+      "]\n"
+      "def g x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
 //:: check types for 'call' instructions
 
-:(scenario call_check_literal_recipe)
-% Hide_errors = true;
-def main [
-  1:num <- call f, 34
-]
-def f x:point -> y:point [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+error: main: ingredient 0 has the wrong type at '1:num <- call f, 34'
-+error: main: product 0 has the wrong type at '1:num <- call f, 34'
-
-:(scenario call_check_variable_recipe)
-% Hide_errors = true;
-def main [
-  {1: (recipe point -> point)} <- copy f
-  2:num <- call {1: (recipe point -> point)}, 34
-]
-def f x:point -> y:point [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+error: main: ingredient 0 has the wrong type at '2:num <- call {1: (recipe point -> point)}, 34'
-+error: main: product 0 has the wrong type at '2:num <- call {1: (recipe point -> point)}, 34'
+void test_call_check_literal_recipe() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  1:num <- call f, 34\n"
+      "]\n"
+      "def f x:point -> y:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: ingredient 0 has the wrong type at '1:num <- call f, 34'\n"
+      "error: main: product 0 has the wrong type at '1:num <- call f, 34'\n"
+  );
+}
+
+void test_call_check_variable_recipe() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  {1: (recipe point -> point)} <- copy f\n"
+      "  2:num <- call {1: (recipe point -> point)}, 34\n"
+      "]\n"
+      "def f x:point -> y:point [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: ingredient 0 has the wrong type at '2:num <- call {1: (recipe point -> point)}, 34'\n"
+      "error: main: product 0 has the wrong type at '2:num <- call {1: (recipe point -> point)}, 34'\n"
+  );
+}
 
 :(before "End resolve_ambiguous_call(r, index, inst, caller_recipe) Special-cases")
 if (inst.name == "call" && !inst.ingredients.empty() && is_recipe_literal(inst.ingredients.at(0))) {
@@ -339,31 +385,41 @@ bool is_mu_recipe(const reagent& r) {
   return r.type->left->atom && r.type->left->name == "recipe";
 }
 
-:(scenario copy_typecheck_recipe_variable)
-% Hide_errors = true;
-def main [
-  3:num <- copy 34  # abc def
-  {1: (recipe number -> number)} <- copy f  # store literal in a matching variable
-  {2: (recipe boolean -> boolean)} <- copy {1: (recipe number -> number)}  # mismatch between recipe variables
-]
-def f x:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+error: main: can't copy '{1: (recipe number -> number)}' to '{2: (recipe boolean -> boolean)}'; types don't match
-
-:(scenario copy_typecheck_recipe_variable_2)
-% Hide_errors = true;
-def main [
-  {1: (recipe number -> number)} <- copy f  # mismatch with a recipe literal
-]
-def f x:bool -> y:bool [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+error: main: can't copy 'f' to '{1: (recipe number -> number)}'; types don't match
+void test_copy_typecheck_recipe_variable() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  3:num <- copy 34\n"
+      "  {1: (recipe number -> number)} <- copy f\n"  // store literal in a matching variable
+      "  {2: (recipe boolean -> boolean)} <- copy {1: (recipe number -> number)}\n"  // mismatch between recipe variables
+      "]\n"
+      "def f x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: can't copy '{1: (recipe number -> number)}' to '{2: (recipe boolean -> boolean)}'; types don't match\n"
+  );
+}
+
+void test_copy_typecheck_recipe_variable_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  {1: (recipe number -> number)} <- copy f\n"  // mismatch with a recipe literal
+      "]\n"
+      "def f x:bool -> y:bool [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: can't copy 'f' to '{1: (recipe number -> number)}'; types don't match\n"
+  );
+}
 
 :(before "End Matching Types For Literal(to)")
 if (is_mu_recipe(to)) {
@@ -384,33 +440,42 @@ if (is_mu_recipe(to)) {
   return true;
 }
 
-:(scenario call_variable_compound_ingredient)
-def main [
-  {1: (recipe (address number) -> number)} <- copy f
-  2:&:num <- copy null
-  3:num <- call {1: (recipe (address number) -> number)}, 2:&:num
-]
-def f x:&:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- deaddress x
-]
-$error: 0
+:(code)
+void test_call_variable_compound_ingredient() {
+  run(
+      "def main [\n"
+      "  {1: (recipe (address number) -> number)} <- copy f\n"
+      "  2:&:num <- copy null\n"
+      "  3:num <- call {1: (recipe (address number) -> number)}, 2:&:num\n"
+      "]\n"
+      "def f x:&:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- deaddress x\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 //: make sure we don't accidentally break on a recipe literal
-:(scenario jump_forbidden_on_recipe_literals)
-% Hide_errors = true;
-def foo [
-  local-scope
-]
-def main [
-  local-scope
-  {
-    break-if foo
-  }
-]
-# error should be as if foo is not a recipe
-+error: main: missing type for 'foo' in 'break-if foo'
+void test_jump_forbidden_on_recipe_literals() {
+  Hide_errors = true;
+  run(
+      "def foo [\n"
+      "  local-scope\n"
+      "]\n"
+      "def main [\n"
+      "  local-scope\n"
+      "  {\n"
+      "    break-if foo\n"
+      "  }\n"
+      "]\n"
+  );
+  // error should be as if foo is not a recipe
+  CHECK_TRACE_CONTENTS(
+      "error: main: missing type for 'foo' in 'break-if foo'\n"
+  );
+}
 
 :(before "End JUMP_IF Checks")
 check_for_recipe_literals(inst, get(Recipe, r));
@@ -427,14 +492,19 @@ void check_for_recipe_literals(const instruction& inst, const recipe& caller) {
   }
 }
 
-:(scenario load_ingredients_missing_error_3)
-% Hide_errors = true;
-def foo {f: (recipe num -> num)} [
-  local-scope
-  b:num <- call f, 1
-]
-+error: foo: missing type for 'f' in 'b:num <- call f, 1'
-+error:   did you forget 'load-ingredients'?
+void test_load_ingredients_missing_error_3() {
+  Hide_errors = true;
+  run(
+      "def foo {f: (recipe num -> num)} [\n"
+      "  local-scope\n"
+      "  b:num <- call f, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: missing type for 'f' in 'b:num <- call f, 1'\n"
+      "error:   did you forget 'load-ingredients'?\n"
+  );
+}
 
 :(before "End Mu Types Initialization")
 put(Type_abbreviations, "function", new_type_tree("recipe"));
@@ -442,38 +512,49 @@ put(Type_abbreviations, "fn", new_type_tree("recipe"));
 
 //: copying functions to variables
 
-:(scenario copy_recipe_to_variable)
-def main [
-  {1: (fn number -> number)} <- copy f
-  2:num <- call {1: (function number -> number)}, 34
-]
-def f x:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 34 in location 2
-
-:(scenario copy_overloaded_recipe_to_variable)
-def main [
-  local-scope
-  {x: (fn num -> num)} <- copy f
-  1:num/raw <- call x, 34
-]
-# variant f
-def f x:bool -> y:bool [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-# variant f_2
-def f x:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-# x contains f_2
-+mem: storing 34 in location 1
+:(code)
+void test_copy_recipe_to_variable() {
+  run(
+      "def main [\n"
+      "  {1: (fn number -> number)} <- copy f\n"
+      "  2:num <- call {1: (function number -> number)}, 34\n"
+      "]\n"
+      "def f x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 2\n"
+  );
+}
+
+void test_copy_overloaded_recipe_to_variable() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  {x: (fn num -> num)} <- copy f\n"
+      "  1:num/raw <- call x, 34\n"
+      "]\n"
+      // variant f
+      "def f x:bool -> y:bool [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+      // variant f_2
+      "def f x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  // x contains f_2
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
 :(before "End resolve_ambiguous_call(r, index, inst, caller_recipe) Special-cases")
 if (inst.name == "copy") {
@@ -508,46 +589,56 @@ void construct_fake_call(const reagent& recipe_var, instruction& out) {
     out.products.push_back(copy(stem->left));
 }
 
-:(scenario copy_shape_shifting_recipe_to_variable)
-def main [
-  local-scope
-  {x: (fn num -> num)} <- copy f
-  1:num/raw <- call x, 34
-]
-def f x:_elem -> y:_elem [
-  local-scope
-  load-inputs
-  y <- copy x
-]
-+mem: storing 34 in location 1
+void test_copy_shape_shifting_recipe_to_variable() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  {x: (fn num -> num)} <- copy f\n"
+      "  1:num/raw <- call x, 34\n"
+      "]\n"
+      "def f x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-inputs\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
 //: passing function literals to (higher-order) functions
 
-:(scenario pass_overloaded_recipe_literal_to_ingredient)
-# like copy_overloaded_recipe_to_variable except we bind 'x' in the course of
-# a 'call' rather than 'copy'
-def main [
-  1:num <- g f
-]
-def g {x: (fn num -> num)} -> result:num [
-  local-scope
-  load-ingredients
-  result <- call x, 34
-]
-# variant f
-def f x:bool -> y:bool [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-# variant f_2
-def f x:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-# x contains f_2
-+mem: storing 34 in location 1
+void test_pass_overloaded_recipe_literal_to_ingredient() {
+  run(
+      // like test_copy_overloaded_recipe_to_variable, except we bind 'x' in
+      // the course of a 'call' rather than 'copy'
+      "def main [\n"
+      "  1:num <- g f\n"
+      "]\n"
+      "def g {x: (fn num -> num)} -> result:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  result <- call x, 34\n"
+      "]\n"
+      // variant f
+      "def f x:bool -> y:bool [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+      // variant f_2
+      "def f x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  // x contains f_2
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
 :(after "End resolve_ambiguous_call(r, index, inst, caller_recipe) Special-cases")
 for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
@@ -570,30 +661,36 @@ for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
   }
 }
 
-:(scenario return_overloaded_recipe_literal_to_caller)
-def main [
-  local-scope
-  {x: (fn num -> num)} <- g
-  1:num/raw <- call x, 34
-]
-def g -> {x: (fn num -> num)} [
-  local-scope
-  return f
-]
-# variant f
-def f x:bool -> y:bool [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-# variant f_2
-def f x:num -> y:num [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-# x contains f_2
-+mem: storing 34 in location 1
+:(code)
+void test_return_overloaded_recipe_literal_to_caller() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  {x: (fn num -> num)} <- g\n"
+      "  1:num/raw <- call x, 34\n"
+      "]\n"
+      "def g -> {x: (fn num -> num)} [\n"
+      "  local-scope\n"
+      "  return f\n"
+      "]\n"
+      // variant f
+      "def f x:bool -> y:bool [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+      // variant f_2
+      "def f x:num -> y:num [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  // x contains f_2
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
 :(before "End resolve_ambiguous_call(r, index, inst, caller_recipe) Special-cases")
 if (inst.name == "return" || inst.name == "reply") {
diff --git a/073scheduler.cc b/073scheduler.cc
index 4270ecb1..41240e64 100644
--- a/073scheduler.cc
+++ b/073scheduler.cc
@@ -1,19 +1,24 @@
 //: Run a second routine concurrently using 'start-running', without any
 //: guarantees on how the operations in each are interleaved with each other.
 
-:(scenario scheduler)
-def f1 [
-  start-running f2
-  # wait for f2 to run
-  {
-    jump-unless 1:num, -1
-  }
-]
-def f2 [
-  1:num <- copy 1
-]
-+schedule: f1
-+schedule: f2
+void test_scheduler() {
+  run(
+      "def f1 [\n"
+      "  start-running f2\n"
+         // wait for f2 to run
+      "  {\n"
+      "    jump-unless 1:num, -1\n"
+      "  }\n"
+      "]\n"
+      "def f2 [\n"
+      "  1:num <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "schedule: f1\n"
+      "schedule: f2\n"
+  );
+}
 
 //: first, add a deadline to run(routine)
 :(before "End Globals")
@@ -189,63 +194,84 @@ case START_RUNNING: {
   break;
 }
 
-:(scenario scheduler_runs_single_routine)
-% Scheduling_interval = 1;
-def f1 [
-  1:num <- copy 0
-  2:num <- copy 0
-]
-+schedule: f1
-+run: {1: "number"} <- copy {0: "literal"}
-+schedule: f1
-+run: {2: "number"} <- copy {0: "literal"}
-
-:(scenario scheduler_interleaves_routines)
-% Scheduling_interval = 1;
-def f1 [
-  start-running f2
-  1:num <- copy 0
-  2:num <- copy 0
-]
-def f2 [
-  3:num <- copy 0
-  4:num <- copy 0
-]
-+schedule: f1
-+run: start-running {f2: "recipe-literal"}
-+schedule: f2
-+run: {3: "number"} <- copy {0: "literal"}
-+schedule: f1
-+run: {1: "number"} <- copy {0: "literal"}
-+schedule: f2
-+run: {4: "number"} <- copy {0: "literal"}
-+schedule: f1
-+run: {2: "number"} <- copy {0: "literal"}
-
-:(scenario start_running_takes_ingredients)
-def f1 [
-  start-running f2, 3
-  # wait for f2 to run
-  {
-    jump-unless 1:num, -1
-  }
-]
-def f2 [
-  1:num <- next-ingredient
-  2:num <- add 1:num, 1
-]
-+mem: storing 4 in location 2
+:(code)
+void test_scheduler_runs_single_routine() {
+  Scheduling_interval = 1;
+  run(
+      "def f1 [\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "schedule: f1\n"
+      "run: {1: \"number\"} <- copy {0: \"literal\"}\n"
+      "schedule: f1\n"
+      "run: {2: \"number\"} <- copy {0: \"literal\"}\n"
+  );
+}
+
+void test_scheduler_interleaves_routines() {
+  Scheduling_interval = 1;
+  run(
+      "def f1 [\n"
+      "  start-running f2\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+      "def f2 [\n"
+      "  3:num <- copy 0\n"
+      "  4:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "schedule: f1\n"
+      "run: start-running {f2: \"recipe-literal\"}\n"
+      "schedule: f2\n"
+      "run: {3: \"number\"} <- copy {0: \"literal\"}\n"
+      "schedule: f1\n"
+      "run: {1: \"number\"} <- copy {0: \"literal\"}\n"
+      "schedule: f2\n"
+      "run: {4: \"number\"} <- copy {0: \"literal\"}\n"
+      "schedule: f1\n"
+      "run: {2: \"number\"} <- copy {0: \"literal\"}\n"
+  );
+}
+
+void test_start_running_takes_ingredients() {
+  run(
+      "def f1 [\n"
+      "  start-running f2, 3\n"
+         // wait for f2 to run
+      "  {\n"
+      "    jump-unless 1:num, -1\n"
+      "  }\n"
+      "]\n"
+      "def f2 [\n"
+      "  1:num <- next-ingredient\n"
+      "  2:num <- add 1:num, 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 4 in location 2\n"
+  );
+}
 
 //: type-checking for 'start-running'
 
-:(scenario start_running_checks_types)
-% Hide_errors = true;
-def f1 [
-  start-running f2, 3
-]
-def f2 n:&:num [
-]
-+error: f1: ingredient 0 has the wrong type at 'start-running f2, 3'
+void test_start_running_checks_types() {
+  Hide_errors = true;
+  run(
+      "def f1 [\n"
+      "  start-running f2, 3\n"
+      "]\n"
+      "def f2 n:&:num [\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: f1: ingredient 0 has the wrong type at 'start-running f2, 3'\n"
+  );
+}
 
 // 'start-running' only uses the ingredients of the callee, not its products
 :(before "End is_indirect_call_with_ingredients Special-cases")
@@ -253,63 +279,90 @@ if (r == START_RUNNING) return true;
 
 //: back to testing 'start-running'
 
-:(scenario start_running_returns_routine_id)
-def f1 [
-  1:num <- start-running f2
-]
-def f2 [
-  12:num <- copy 44
-]
-+mem: storing 2 in location 1
-
-//: this scenario will require some careful setup in escaped C++
-//: (straining our tangle capabilities to near-breaking point)
-:(scenario scheduler_skips_completed_routines)
-% recipe_ordinal f1 = load("recipe f1 [\n1:num <- copy 0\n]\n").front();
-% recipe_ordinal f2 = load("recipe f2 [\n2:num <- copy 0\n]\n").front();
-% Routines.push_back(new routine(f1));  // f1 meant to run
-% Routines.push_back(new routine(f2));
-% Routines.back()->state = COMPLETED;  // f2 not meant to run
-# must have at least one routine without escaping
-def f3 [
-  3:num <- copy 0
-]
-# by interleaving '+' lines with '-' lines, we allow f1 and f3 to run in any order
-+schedule: f1
-+mem: storing 0 in location 1
--schedule: f2
--mem: storing 0 in location 2
-+schedule: f3
-+mem: storing 0 in location 3
-
-:(scenario scheduler_starts_at_middle_of_routines)
-% Routines.push_back(new routine(COPY));
-% Routines.back()->state = COMPLETED;
-def f1 [
-  1:num <- copy 0
-  2:num <- copy 0
-]
-+schedule: f1
--run: idle
+:(code)
+void test_start_running_returns_routine_id() {
+  run(
+      "def f1 [\n"
+      "  1:num <- start-running f2\n"
+      "]\n"
+      "def f2 [\n"
+      "  12:num <- copy 44\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 2 in location 1\n"
+  );
+}
+
+//: this scenario requires some careful setup
+void test_scheduler_skips_completed_routines() {
+  recipe_ordinal f1 = load(
+      "recipe f1 [\n"
+      "  1:num <- copy 0\n"
+      "]\n").front();
+  recipe_ordinal f2 = load(
+      "recipe f2 [\n"
+      "  2:num <- copy 0\n"
+      "]\n").front();
+  Routines.push_back(new routine(f1));  // f1 meant to run
+  Routines.push_back(new routine(f2));
+  Routines.back()->state = COMPLETED;  // f2 not meant to run
+  run(
+      "def f3 [\n"
+      "  3:num <- copy 0\n"
+      "]\n"
+  );
+  // f1 and f3 can run in any order
+  CHECK_TRACE_CONTENTS(
+      "schedule: f1\n"
+      "mem: storing 0 in location 1\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("schedule: f2");
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 0 in location 2");
+  CHECK_TRACE_CONTENTS(
+      "schedule: f3\n"
+      "mem: storing 0 in location 3\n"
+  );
+}
+
+void test_scheduler_starts_at_middle_of_routines() {
+  Routines.push_back(new routine(COPY));
+  Routines.back()->state = COMPLETED;
+  run(
+      "def f1 [\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "schedule: f1\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: idle");
+}
 
 //:: Errors in a routine cause it to terminate.
 
-:(scenario scheduler_terminates_routines_after_errors)
-% Hide_errors = true;
-% Scheduling_interval = 2;
-def f1 [
-  start-running f2
-  1:num <- copy 0
-  2:num <- copy 0
-]
-def f2 [
-  # divide by 0 twice
-  3:num <- divide-with-remainder 4, 0
-  4:num <- divide-with-remainder 4, 0
-]
-# f2 should stop after first divide by 0
-+error: f2: divide by zero in '3:num <- divide-with-remainder 4, 0'
--error: f2: divide by zero in '4:num <- divide-with-remainder 4, 0'
+void test_scheduler_terminates_routines_after_errors() {
+  Hide_errors = true;
+  Scheduling_interval = 2;
+  run(
+      "def f1 [\n"
+      "  start-running f2\n"
+      "  1:num <- copy 0\n"
+      "  2:num <- copy 0\n"
+      "]\n"
+      "def f2 [\n"
+         // divide by 0 twice
+      "  3:num <- divide-with-remainder 4, 0\n"
+      "  4:num <- divide-with-remainder 4, 0\n"
+      "]\n"
+  );
+  // f2 should stop after first divide by 0
+  CHECK_TRACE_CONTENTS(
+      "error: f2: divide by zero in '3:num <- divide-with-remainder 4, 0'\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("error: f2: divide by zero in '4:num <- divide-with-remainder 4, 0'");
+}
 
 :(after "operator<<(ostream& os, end /*unused*/)")
   if (Trace_stream && Trace_stream->curr_label == "error" && Current_routine) {
@@ -318,15 +371,20 @@ def f2 [
 
 //:: Routines are marked completed when their parent completes.
 
-:(scenario scheduler_kills_orphans)
-def main [
-  start-running f1
-  # f1 never actually runs because its parent completes without waiting for it
-]
-def f1 [
-  1:num <- copy 0
-]
--schedule: f1
+:(code)
+void test_scheduler_kills_orphans() {
+  run(
+      "def main [\n"
+      "  start-running f1\n"
+         // f1 never actually runs because its parent completes without
+         // waiting for it
+      "]\n"
+      "def f1 [\n"
+      "  1:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("schedule: f1");
+}
 
 :(before "End Scheduler Cleanup")
 for (int i = 0;  i < SIZE(Routines);  ++i) {
@@ -349,20 +407,26 @@ bool has_completed_parent(int routine_index) {
 
 //:: 'routine-state' can tell if a given routine id is running
 
-:(scenario routine_state_test)
-% Scheduling_interval = 2;
-def f1 [
-  1:num/child-id <- start-running f2
-  12:num <- copy 0  # race condition since we don't care about location 12
-  # thanks to Scheduling_interval, f2's one instruction runs in between here and completes
-  2:num/state <- routine-state 1:num/child-id
-]
-def f2 [
-  12:num <- copy 0
-  # trying to run a second instruction marks routine as completed
-]
-# recipe f2 should be in state COMPLETED
-+mem: storing 1 in location 2
+void test_routine_state_test() {
+  Scheduling_interval = 2;
+  run(
+      "def f1 [\n"
+      "  1:num/child-id <- start-running f2\n"
+      "  12:num <- copy 0\n"  // race condition since we don't care about location 12
+         // thanks to Scheduling_interval, f2's one instruction runs in
+         // between here and completes
+      "  2:num/state <- routine-state 1:num/child-id\n"
+      "]\n"
+      "def f2 [\n"
+      "  12:num <- copy 0\n"
+         // trying to run a second instruction marks routine as completed
+      "]\n"
+  );
+  // routine f2 should be in state COMPLETED
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 2\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 ROUTINE_STATE,
@@ -443,22 +507,28 @@ case _DUMP_ROUTINES: {
 
 //: support for stopping routines after some number of cycles
 
-:(scenario routine_discontinues_past_limit)
-% Scheduling_interval = 2;
-def f1 [
-  1:num/child-id <- start-running f2
-  limit-time 1:num/child-id, 10
-  # padding loop just to make sure f2 has time to completed
-  2:num <- copy 20
-  2:num <- subtract 2:num, 1
-  jump-if 2:num, -2:offset
-]
-def f2 [
-  jump -1:offset  # run forever
-  $print [should never get here], 10/newline
-]
-# f2 terminates
-+schedule: discontinuing routine 2
+:(code)
+void test_routine_discontinues_past_limit() {
+  Scheduling_interval = 2;
+  run(
+      "def f1 [\n"
+      "  1:num/child-id <- start-running f2\n"
+      "  limit-time 1:num/child-id, 10\n"
+         // padding loop just to make sure f2 has time to complete
+      "  2:num <- copy 20\n"
+      "  2:num <- subtract 2:num, 1\n"
+      "  jump-if 2:num, -2:offset\n"
+      "]\n"
+      "def f2 [\n"
+      "  jump -1:offset\n"  // run forever
+      "  $print [should never get here], 10/newline\n"
+      "]\n"
+  );
+  // f2 terminates
+  CHECK_TRACE_CONTENTS(
+      "schedule: discontinuing routine 2\n"
+  );
+}
 
 :(before "End routine States")
 DISCONTINUED,
@@ -564,59 +634,75 @@ case NUMBER_OF_INSTRUCTIONS: {
   break;
 }
 
-:(scenario number_of_instructions)
-def f1 [
-  10:num/child-id <- start-running f2
-  {
-    loop-unless 20:num
-  }
-  11:num <- number-of-instructions 10:num
-]
-def f2 [
-  # 2 instructions worth of work
-  1:num <- copy 34
-  20:num <- copy 1
-]
-# f2 runs an extra instruction for the implicit return added by the
-# fill_in_return_ingredients transform
-+mem: storing 3 in location 11
-
-:(scenario number_of_instructions_across_multiple_scheduling_intervals)
-% Scheduling_interval = 1;
-def f1 [
-  10:num/child-id <- start-running f2
-  {
-    loop-unless 20:num
-  }
-  11:num <- number-of-instructions 10:num
-]
-def f2 [
-  # 4 instructions worth of work
-  1:num <- copy 34
-  2:num <- copy 1
-  2:num <- copy 3
-  20:num <- copy 1
-]
-# f2 runs an extra instruction for the implicit return added by the
-# fill_in_return_ingredients transform
-+mem: storing 5 in location 11
+:(code)
+void test_number_of_instructions() {
+  run(
+      "def f1 [\n"
+      "  10:num/child-id <- start-running f2\n"
+      "  {\n"
+      "    loop-unless 20:num\n"
+      "  }\n"
+      "  11:num <- number-of-instructions 10:num\n"
+      "]\n"
+      "def f2 [\n"
+         // 2 instructions worth of work
+      "  1:num <- copy 34\n"
+      "  20:num <- copy 1\n"
+      "]\n"
+  );
+  // f2 runs an extra instruction for the implicit 'return' added by the
+  // fill_in_return_ingredients transform
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 3 in location 11\n"
+  );
+}
+
+void test_number_of_instructions_across_multiple_scheduling_intervals() {
+  Scheduling_interval = 1;
+  run(
+      "def f1 [\n"
+      "  10:num/child-id <- start-running f2\n"
+      "  {\n"
+      "    loop-unless 20:num\n"
+      "  }\n"
+      "  11:num <- number-of-instructions 10:num\n"
+      "]\n"
+      "def f2 [\n"
+         // 4 instructions worth of work
+      "  1:num <- copy 34\n"
+      "  2:num <- copy 1\n"
+      "  2:num <- copy 3\n"
+      "  20:num <- copy 1\n"
+      "]\n"
+  );
+  // f2 runs an extra instruction for the implicit 'return' added by the
+  // fill_in_return_ingredients transform
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 5 in location 11\n"
+  );
+}
 
 //:: make sure that each routine gets a different alloc to start
 
-:(scenario new_concurrent)
-def f1 [
-  start-running f2
-  1:&:num/raw <- new number:type
-  # wait for f2 to complete
-  {
-    loop-unless 4:num/raw
-  }
-]
-def f2 [
-  2:&:num/raw <- new number:type
-  # hack: assumes scheduler implementation
-  3:bool/raw <- equal 1:&:num/raw, 2:&:num/raw
-  # signal f2 complete
-  4:num/raw <- copy 1
-]
-+mem: storing 0 in location 3
+void test_new_concurrent() {
+  run(
+      "def f1 [\n"
+      "  start-running f2\n"
+      "  1:&:num/raw <- new number:type\n"
+         // wait for f2 to complete
+      "  {\n"
+      "    loop-unless 4:num/raw\n"
+      "  }\n"
+      "]\n"
+      "def f2 [\n"
+      "  2:&:num/raw <- new number:type\n"
+         // hack: assumes scheduler implementation
+      "  3:bool/raw <- equal 1:&:num/raw, 2:&:num/raw\n"
+         // signal f2 complete
+      "  4:num/raw <- copy 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 3\n"
+  );
+}
diff --git a/074wait.cc b/074wait.cc
index 0a32c994..8eb4b887 100644
--- a/074wait.cc
+++ b/074wait.cc
@@ -3,24 +3,29 @@
 //: basic technique for orchestrating the order in which different routines
 //: operate.
 
-:(scenario wait_for_location)
-def f1 [
-  10:num <- copy 34
-  start-running f2
-  20:location <- copy 10/unsafe
-  wait-for-reset-then-set 20:location
-  # wait for f2 to run and reset location 1
-  30:num <- copy 10:num
-]
-def f2 [
-  10:location <- copy 0/unsafe
-]
-+schedule: f1
-+run: waiting for location 10 to reset
-+schedule: f2
-+schedule: waking up routine 1
-+schedule: f1
-+mem: storing 1 in location 30
+void test_wait_for_location() {
+  run(
+      "def f1 [\n"
+      "  10:num <- copy 34\n"
+      "  start-running f2\n"
+      "  20:location <- copy 10/unsafe\n"
+      "  wait-for-reset-then-set 20:location\n"
+         // wait for f2 to run and reset location 1
+      "  30:num <- copy 10:num\n"
+      "]\n"
+      "def f2 [\n"
+      "  10:location <- copy 0/unsafe\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "schedule: f1\n"
+      "run: waiting for location 10 to reset\n"
+      "schedule: f2\n"
+      "schedule: waking up routine 1\n"
+      "schedule: f1\n"
+      "mem: storing 1 in location 30\n"
+  );
+}
 
 //: define the new state that all routines can be in
 
@@ -58,14 +63,19 @@ void dump_waiting_routines() {
   }
 }
 
-:(scenario wait_for_location_can_deadlock)
-% Hide_errors = true;
-def main [
-  10:num <- copy 1
-  20:location <- copy 10/unsafe
-  wait-for-reset-then-set 20:location
-]
-+error: deadlock!
+void test_wait_for_location_can_deadlock() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  10:num <- copy 1\n"
+      "  20:location <- copy 10/unsafe\n"
+      "  wait-for-reset-then-set 20:location\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: deadlock!\n"
+  );
+}
 
 //: Primitive recipe to put routines in that state.
 //: This primitive is also known elsewhere as compare-and-set (CAS). Used to
@@ -143,13 +153,19 @@ for (int i = 0;  i < SIZE(Routines);  ++i) {
 //: Only supports elements immediately inside containers; no arrays or
 //: containers within containers yet.
 
-:(scenario get_location)
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  15:location <- get-location 12:point, 1:offset
-]
-+mem: storing 13 in location 15
+:(code)
+void test_get_location() {
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  15:location <- get-location 12:point, 1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 13 in location 15\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 GET_LOCATION,
@@ -226,87 +242,117 @@ bool is_mu_location(reagent/*copy*/ x) {
   return x.type->value == get(Type_ordinal, "location");
 }
 
-:(scenario get_location_out_of_bounds)
-% Hide_errors = true;
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  14:num <- copy 36
-  get-location 12:point-number/raw, 2:offset  # point-number occupies 3 locations but has only 2 fields; out of bounds
-]
-+error: main: invalid offset 2 for 'point-number'
-
-:(scenario get_location_out_of_bounds_2)
-% Hide_errors = true;
-def main [
-  12:num <- copy 34
-  13:num <- copy 35
-  14:num <- copy 36
-  get-location 12:point-number/raw, -1:offset
-]
-+error: main: invalid offset -1 for 'point-number'
-
-:(scenario get_location_product_type_mismatch)
-% Hide_errors = true;
-container boolbool [
-  x:bool
-  y:bool
-]
-def main [
-  12:bool <- copy 1
-  13:bool <- copy 0
-  15:bool <- get-location 12:boolbool, 1:offset
-]
-+error: main: 'get-location 12:boolbool, 1:offset' should write to type location but '15' has type 'boolean'
-
-:(scenario get_location_indirect)
-# 'get-location' can read from container address
-def main [
-  1:num/alloc-id, 2:num <- copy 0, 10
-  10:num/alloc-id, 11:num/x, 12:num/y <- copy 0, 34, 35
-  20:location <- get-location 1:&:point/lookup, 0:offset
-]
-+mem: storing 11 in location 20
-
-:(scenario get_location_indirect_2)
-def main [
-  1:num/alloc-id, 2:num <- copy 0, 10
-  10:num/alloc-id, 11:num/x, 12:num/y <- copy 0, 34, 35
-  4:num/alloc-id, 5:num <- copy 0, 20
-  4:&:location/lookup <- get-location 1:&:point/lookup, 0:offset
-]
-+mem: storing 11 in location 21
+void test_get_location_out_of_bounds() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  get-location 12:point-number/raw, 2:offset\n"  // point-number occupies 3 locations but has only 2 fields; out of bounds
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: invalid offset 2 for 'point-number'\n"
+  );
+}
+
+void test_get_location_out_of_bounds_2() {
+  Hide_errors = true;
+  run(
+      "def main [\n"
+      "  12:num <- copy 34\n"
+      "  13:num <- copy 35\n"
+      "  14:num <- copy 36\n"
+      "  get-location 12:point-number/raw, -1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: invalid offset -1 for 'point-number'\n"
+  );
+}
+
+void test_get_location_product_type_mismatch() {
+  Hide_errors = true;
+  run(
+      "container boolbool [\n"
+      "  x:bool\n"
+      "  y:bool\n"
+      "]\n"
+      "def main [\n"
+      "  12:bool <- copy 1\n"
+      "  13:bool <- copy 0\n"
+      "  15:bool <- get-location 12:boolbool, 1:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: main: 'get-location 12:boolbool, 1:offset' should write to type location but '15' has type 'boolean'\n"
+  );
+}
+
+void test_get_location_indirect() {
+  // 'get-location' can read from container address
+  run(
+      "def main [\n"
+      "  1:num/alloc-id, 2:num <- copy 0, 10\n"
+      "  10:num/alloc-id, 11:num/x, 12:num/y <- copy 0, 34, 35\n"
+      "  20:location <- get-location 1:&:point/lookup, 0:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 11 in location 20\n"
+  );
+}
+
+void test_get_location_indirect_2() {
+  run(
+      "def main [\n"
+      "  1:num/alloc-id, 2:num <- copy 0, 10\n"
+      "  10:num/alloc-id, 11:num/x, 12:num/y <- copy 0, 34, 35\n"
+      "  4:num/alloc-id, 5:num <- copy 0, 20\n"
+      "  4:&:location/lookup <- get-location 1:&:point/lookup, 0:offset\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 11 in location 21\n"
+  );
+}
 
 //: allow waiting on a routine to complete
 
-:(scenario wait_for_routine)
-def f1 [
-  # add a few routines to run
-  1:num/routine <- start-running f2
-  2:num/routine <- start-running f3
-  wait-for-routine 1:num/routine
-  # now wait for f2 to *complete* and modify location 13 before using its value
-  20:num <- copy 13:num
-]
-def f2 [
-  10:num <- copy 0  # just padding
-  switch  # simulate a block; routine f1 shouldn't restart at this point
-  13:num <- copy 34
-]
-def f3 [
-  # padding routine just to help simulate the block in f2 using 'switch'
-  11:num <- copy 0
-  12:num <- copy 0
-]
-+schedule: f1
-+run: waiting for routine 2
-+schedule: f2
-+schedule: f3
-+schedule: f2
-+schedule: waking up routine 1
-+schedule: f1
-# if we got the synchronization wrong we'd be storing 0 in location 20
-+mem: storing 34 in location 20
+void test_wait_for_routine() {
+  run(
+      "def f1 [\n"
+         // add a few routines to run
+      "  1:num/routine <- start-running f2\n"
+      "  2:num/routine <- start-running f3\n"
+      "  wait-for-routine 1:num/routine\n"
+         // now wait for f2 to *complete* and modify location 13 before using its value
+      "  20:num <- copy 13:num\n"
+      "]\n"
+      "def f2 [\n"
+      "  10:num <- copy 0\n"  // just padding
+      "  switch\n"  // simulate a block; routine f1 shouldn't restart at this point
+      "  13:num <- copy 34\n"
+      "]\n"
+      "def f3 [\n"
+         // padding routine just to help simulate the block in f2 using 'switch'
+      "  11:num <- copy 0\n"
+      "  12:num <- copy 0\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "schedule: f1\n"
+      "run: waiting for routine 2\n"
+      "schedule: f2\n"
+      "schedule: f3\n"
+      "schedule: f2\n"
+      "schedule: waking up routine 1\n"
+      "schedule: f1\n"
+      // if we got the synchronization wrong we'd be storing 0 in location 20
+      "mem: storing 34 in location 20\n"
+  );
+}
 
 :(before "End routine Fields")
 // only if state == WAITING
@@ -379,21 +425,27 @@ case SWITCH: {
   goto stop_running_current_routine;
 }
 
-:(scenario switch_preempts_current_routine)
-def f1 [
-  start-running f2
-  1:num <- copy 34
-  switch
-  3:num <- copy 36
-]
-def f2 [
-  2:num <- copy 35
-]
-+mem: storing 34 in location 1
-# context switch
-+mem: storing 35 in location 2
-# back to original thread
-+mem: storing 36 in location 3
+:(code)
+void test_switch_preempts_current_routine() {
+  run(
+      "def f1 [\n"
+      "  start-running f2\n"
+      "  1:num <- copy 34\n"
+      "  switch\n"
+      "  3:num <- copy 36\n"
+      "]\n"
+      "def f2 [\n"
+      "  2:num <- copy 35\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+      // context switch
+      "mem: storing 35 in location 2\n"
+      // back to original thread
+      "mem: storing 36 in location 3\n"
+  );
+}
 
 //:: helpers for manipulating routines in tests
 //:
@@ -452,23 +504,29 @@ case CURRENT_ROUTINE_IS_UNBLOCKED: {
 //: also allow waiting on a routine to block
 //: (just for tests; use wait_for_routine above wherever possible)
 
-:(scenario wait_for_routine_to_block)
-def f1 [
-  1:num/routine <- start-running f2
-  wait-for-routine-to-block 1:num/routine
-  # now wait for f2 to run and modify location 10 before using its value
-  11:num <- copy 10:num
-]
-def f2 [
-  10:num <- copy 34
-]
-+schedule: f1
-+run: waiting for routine 2 to block
-+schedule: f2
-+schedule: waking up routine 1 because routine 2 is blocked
-+schedule: f1
-# if we got the synchronization wrong we'd be storing 0 in location 11
-+mem: storing 34 in location 11
+:(code)
+void test_wait_for_routine_to_block() {
+  run(
+      "def f1 [\n"
+      "  1:num/routine <- start-running f2\n"
+      "  wait-for-routine-to-block 1:num/routine\n"
+         // now wait for f2 to run and modify location 10 before using its value
+      "  11:num <- copy 10:num\n"
+      "]\n"
+      "def f2 [\n"
+      "  10:num <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "schedule: f1\n"
+      "run: waiting for routine 2 to block\n"
+      "schedule: f2\n"
+      "schedule: waking up routine 1 because routine 2 is blocked\n"
+      "schedule: f1\n"
+      // if we got the synchronization wrong we'd be storing 0 in location 11
+      "mem: storing 34 in location 11\n"
+  );
+}
 
 :(before "End routine Fields")
 // only if state == WAITING
@@ -555,43 +613,52 @@ case RESTART: {
   break;
 }
 
-:(scenario cannot_restart_completed_routine)
-% Scheduling_interval = 1;
-def main [
-  local-scope
-  r:num/routine-id <- start-running f
-  x:num <- copy 0  # wait for f to be scheduled
-  # r is COMPLETED by this point
-  restart r  # should have no effect
-  x:num <- copy 0  # give f time to be scheduled (though it shouldn't be)
-]
-def f [
-  1:num/raw <- copy 1
-]
-# shouldn't crash
-
-:(scenario restart_blocked_routine)
-% Scheduling_interval = 1;
-def main [
-  local-scope
-  r:num/routine-id <- start-running f
-  wait-for-routine-to-block r  # get past the block in f below
-  restart r
-  wait-for-routine-to-block r  # should run f to completion
-]
-# function with one block
-def f [
-  current-routine-is-blocked
-  # 8 instructions of padding, many more than 'main' above
-  1:num <- add 1:num, 1
-  1:num <- add 1:num, 1
-  1:num <- add 1:num, 1
-  1:num <- add 1:num, 1
-  1:num <- add 1:num, 1
-  1:num <- add 1:num, 1
-  1:num <- add 1:num, 1
-  1:num <- add 1:num, 1
-  1:num <- add 1:num, 1
-]
-# make sure all of f ran
-+mem: storing 8 in location 1
+:(code)
+void test_cannot_restart_completed_routine() {
+  Scheduling_interval = 1;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  r:num/routine-id <- start-running f\n"
+      "  x:num <- copy 0\n"  // wait for f to be scheduled
+      // r is COMPLETED by this point
+      "  restart r\n"  // should have no effect
+      "  x:num <- copy 0\n"  // give f time to be scheduled (though it shouldn't be)
+      "]\n"
+      "def f [\n"
+      "  1:num/raw <- copy 1\n"
+      "]\n"
+  );
+  // shouldn't crash
+}
+
+void test_restart_blocked_routine() {
+  Scheduling_interval = 1;
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  r:num/routine-id <- start-running f\n"
+      "  wait-for-routine-to-block r\n"  // get past the block in f below
+      "  restart r\n"
+      "  wait-for-routine-to-block r\n"  // should run f to completion
+      "]\n"
+      // function with one block
+      "def f [\n"
+      "  current-routine-is-blocked\n"
+         // 8 instructions of padding, many more than 'main' above
+      "  1:num <- add 1:num, 1\n"
+      "  1:num <- add 1:num, 1\n"
+      "  1:num <- add 1:num, 1\n"
+      "  1:num <- add 1:num, 1\n"
+      "  1:num <- add 1:num, 1\n"
+      "  1:num <- add 1:num, 1\n"
+      "  1:num <- add 1:num, 1\n"
+      "  1:num <- add 1:num, 1\n"
+      "  1:num <- add 1:num, 1\n"
+      "]\n"
+  );
+  // make sure all of f ran
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 8 in location 1\n"
+  );
+}
diff --git a/076continuation.cc b/076continuation.cc
index 49916ef2..670fef2f 100644
--- a/076continuation.cc
+++ b/076continuation.cc
@@ -67,50 +67,57 @@ if (r.type->atom && r.type->name == "continuation") {
   return result_header;
 }
 
-:(scenario delimited_continuation)
-recipe main [
-  1:continuation <- call-with-continuation-mark 233/mark, f, 77  # 77 is an argument to f
-  2:num <- copy 5
-  {
-    2:num <- call 1:continuation, 2:num  # jump to 'return-continuation-until-mark' below
-    3:bool <- greater-or-equal 2:num, 8
-    break-if 3:bool
-    loop
-  }
-]
-recipe f [
-  11:num <- next-ingredient
-  12:num <- g 11:num
-  return 12:num
-]
-recipe g [
-  21:num <- next-ingredient
-  22:num <- return-continuation-until-mark 233/mark
-  23:num <- add 22:num, 1
-  return 23:num
-]
-# first call of 'g' executes the part before return-continuation-until-mark
-+mem: storing 77 in location 21
-+run: {2: "number"} <- copy {5: "literal"}
-+mem: storing 5 in location 2
-# calls of the continuation execute the part after return-continuation-until-mark
-+run: {2: "number"} <- call {1: "continuation"}, {2: "number"}
-+mem: storing 5 in location 22
-+mem: storing 6 in location 2
-+run: {2: "number"} <- call {1: "continuation"}, {2: "number"}
-+mem: storing 6 in location 22
-+mem: storing 7 in location 2
-+run: {2: "number"} <- call {1: "continuation"}, {2: "number"}
-+mem: storing 7 in location 22
-+mem: storing 8 in location 2
-# first call of 'g' does not execute the part after return-continuation-until-mark
--mem: storing 77 in location 22
-# calls of the continuation don't execute the part before return-continuation-until-mark
--mem: storing 5 in location 21
--mem: storing 6 in location 21
--mem: storing 7 in location 21
-# termination
--mem: storing 9 in location 2
+:(code)
+void test_delimited_continuation() {
+  run(
+      "recipe main [\n"
+      "  1:continuation <- call-with-continuation-mark 233/mark, f, 77\n"  // 77 is an argument to f
+      "  2:num <- copy 5\n"
+      "  {\n"
+      "    2:num <- call 1:continuation, 2:num\n"  // jump to 'return-continuation-until-mark' below
+      "    3:bool <- greater-or-equal 2:num, 8\n"
+      "    break-if 3:bool\n"
+      "    loop\n"
+      "  }\n"
+      "]\n"
+      "recipe f [\n"
+      "  11:num <- next-ingredient\n"
+      "  12:num <- g 11:num\n"
+      "  return 12:num\n"
+      "]\n"
+      "recipe g [\n"
+      "  21:num <- next-ingredient\n"
+      "  22:num <- return-continuation-until-mark 233/mark\n"
+      "  23:num <- add 22:num, 1\n"
+      "  return 23:num\n"
+      "]\n"
+  );
+  // first call of 'g' executes the part before return-continuation-until-mark
+  CHECK_TRACE_CONTENTS(
+      // first call of 'g' executes the part before return-continuation-until-mark
+      "mem: storing 77 in location 21\n"
+      "run: {2: \"number\"} <- copy {5: \"literal\"}\n"
+      "mem: storing 5 in location 2\n"
+      // calls of the continuation execute the part after return-continuation-until-mark
+      "run: {2: \"number\"} <- call {1: \"continuation\"}, {2: \"number\"}\n"
+      "mem: storing 5 in location 22\n"
+      "mem: storing 6 in location 2\n"
+      "run: {2: \"number\"} <- call {1: \"continuation\"}, {2: \"number\"}\n"
+      "mem: storing 6 in location 22\n"
+      "mem: storing 7 in location 2\n"
+      "run: {2: \"number\"} <- call {1: \"continuation\"}, {2: \"number\"}\n"
+      "mem: storing 7 in location 22\n"
+      "mem: storing 8 in location 2\n"
+  );
+  // first call of 'g' does not execute the part after return-continuation-until-mark
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 77 in location 22");
+  // calls of the continuation don't execute the part before return-continuation-until-mark
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 5 in location 21");
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 6 in location 21");
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 7 in location 21");
+  // termination
+  CHECK_TRACE_DOESNT_CONTAIN("mem: storing 9 in location 2");
+}
 
 :(before "End call Fields")
 int continuation_mark_tag;
@@ -148,24 +155,35 @@ case CALL_WITH_CONTINUATION_MARK: {
   continue;
 }
 
-:(scenario next_ingredient_inside_continuation)
-recipe main [
-  call-with-continuation-mark 233/mark, f, true
-]
-recipe f [
-  10:bool <- next-input
-]
-+mem: storing 1 in location 10
+:(code)
+void test_next_ingredient_inside_continuation() {
+  run(
+      "recipe main [\n"
+      "  call-with-continuation-mark 233/mark, f, true\n"
+      "]\n"
+      "recipe f [\n"
+      "  10:bool <- next-input\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 10\n"
+  );
+}
 
-:(scenario delimited_continuation_out_of_recipe_variable)
-recipe main [
-  x:recipe <- copy f
-  call-with-continuation-mark 233/mark, x, true
-]
-recipe f [
-  10:bool <- next-input
-]
-+mem: storing 1 in location 10
+void test_delimited_continuation_out_of_recipe_variable() {
+  run(
+      "recipe main [\n"
+      "  x:recipe <- copy f\n"
+      "  call-with-continuation-mark 233/mark, x, true\n"
+      "]\n"
+      "recipe f [\n"
+      "  10:bool <- next-input\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 10\n"
+  );
+}
 
 //: save the slice of current call stack until the 'call-with-continuation-mark'
 //: call, and return it as the result.
@@ -253,53 +271,69 @@ if (is_mu_continuation(current_instruction().ingredients.at(0))) {
   break;  // record results of resuming 'return-continuation-until-mark' instruction
 }
 
-:(scenario continuations_can_return_values)
-def main [
-  local-scope
-  k:continuation, 1:num/raw <- call-with-continuation-mark 233/mark, f
-]
-def f [
-  local-scope
-  g
-]
-def g [
-  local-scope
-  return-continuation-until-mark 233/mark, 34
-  stash [continuation called]
-]
-+mem: storing 34 in location 1
+:(code)
+void test_continuations_can_return_values() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  k:continuation, 1:num/raw <- call-with-continuation-mark 233/mark, f\n"
+      "]\n"
+      "def f [\n"
+      "  local-scope\n"
+      "  g\n"
+      "]\n"
+      "def g [\n"
+      "  local-scope\n"
+      "  return-continuation-until-mark 233/mark, 34\n"
+      "  stash [continuation called]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
-:(scenario continuations_continue_to_matching_mark)
-def main [
-  local-scope
-  k:continuation, 1:num/raw <- call-with-continuation-mark 233/mark, f
-  add 1, 1
-]
-def f [
-  local-scope
-  k2:continuation <- call-with-continuation-mark 234/mark, g
-  add 2, 2
-]
-def g [
-  local-scope
-  return-continuation-until-mark 233/mark, 34
-  stash [continuation called]
-]
-+run: add {1: "literal"}, {1: "literal"}
--run: add {2: "literal"}, {2: "literal"}
+void test_continuations_continue_to_matching_mark() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  k:continuation, 1:num/raw <- call-with-continuation-mark 233/mark, f\n"
+      "  add 1, 1\n"
+      "]\n"
+      "def f [\n"
+      "  local-scope\n"
+      "  k2:continuation <- call-with-continuation-mark 234/mark, g\n"
+      "  add 2, 2\n"
+      "]\n"
+      "def g [\n"
+      "  local-scope\n"
+      "  return-continuation-until-mark 233/mark, 34\n"
+      "  stash [continuation called]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add {1: \"literal\"}, {1: \"literal\"}\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("run: add {2: \"literal\"}, {2: \"literal\"}");
+}
 
 //: Allow shape-shifting recipes to return continuations.
 
-:(scenario call_shape_shifting_recipe_with_continuation_mark)
-def main [
-  1:num <- call-with-continuation-mark 233/mark, f, 34
-]
-def f x:_elem -> y:_elem [
-  local-scope
-  load-ingredients
-  y <- copy x
-]
-+mem: storing 34 in location 1
+void test_call_shape_shifting_recipe_with_continuation_mark() {
+  run(
+      "def main [\n"
+      "  1:num <- call-with-continuation-mark 233/mark, f, 34\n"
+      "]\n"
+      "def f x:_elem -> y:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  y <- copy x\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
 
 :(before "End resolve_ambiguous_call(r, index, inst, caller_recipe) Special-cases")
 if (inst.name == "call-with-continuation-mark") {
@@ -321,31 +355,39 @@ void resolve_indirect_continuation_call(const recipe_ordinal r, int index, instr
   inst.ingredients.at(/*skip mark*/1).set_value(get(Recipe_ordinal, inst2.name));
 }
 
-:(scenario call_shape_shifting_recipe_with_continuation_mark_and_no_outputs)
-def main [
-  1:continuation <- call-with-continuation-mark 233/mark, f, 34
-]
-def f x:_elem [
-  local-scope
-  load-ingredients
-  return-continuation-until-mark 233/mark
-]
-$error: 0
+void test_call_shape_shifting_recipe_with_continuation_mark_and_no_outputs() {
+  run(
+      "def main [\n"
+      "  1:continuation <- call-with-continuation-mark 233/mark, f, 34\n"
+      "]\n"
+      "def f x:_elem [\n"
+      "  local-scope\n"
+      "  load-ingredients\n"
+      "  return-continuation-until-mark 233/mark\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
-:(scenario continuation1)
-def main [
-  local-scope
-  k:continuation <- call-with-continuation-mark 233/mark, create-yielder
-  10:num/raw <- call k
-]
-def create-yielder -> n:num [
-  local-scope
-  load-inputs
-  return-continuation-until-mark 233/mark
-  return 1
-]
-+mem: storing 1 in location 10
-$error: 0
+void test_continuation1() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  k:continuation <- call-with-continuation-mark 233/mark, create-yielder\n"
+      "  10:num/raw <- call k\n"
+      "]\n"
+      "def create-yielder -> n:num [\n"
+      "  local-scope\n"
+      "  load-inputs\n"
+      "  return-continuation-until-mark 233/mark\n"
+      "  return 1\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 1 in location 10\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 :(code)
 bool is_mu_continuation(reagent/*copy*/ x) {
diff --git a/082scenario_screen.cc b/082scenario_screen.cc
index dc015fed..39ba76e7 100644
--- a/082scenario_screen.cc
+++ b/082scenario_screen.cc
@@ -9,127 +9,146 @@
 recipes_taking_literal_strings.insert("screen-should-contain");
 recipes_taking_literal_strings.insert("screen-should-contain-in-color");
 
-:(scenarios run_mu_scenario)
-:(scenario screen_in_scenario)
-scenario screen-in-scenario [
-  local-scope
-  assume-screen 5/width, 3/height
-  run [
-    a:char <- copy 97/a
-    screen:&:screen <- print screen:&:screen, a
-  ]
-  screen-should-contain [
-  #  01234
-    .a    .
-    .     .
-    .     .
-  ]
-]
-# checks are inside scenario
+:(code)
+void test_screen_in_scenario() {
+  run_mu_scenario(
+      "scenario screen-in-scenario [\n"
+      "  local-scope\n"
+      "  assume-screen 5/width, 3/height\n"
+      "  run [\n"
+      "    a:char <- copy 97/a\n"
+      "    screen:&:screen <- print screen:&:screen, a\n"
+      "  ]\n"
+      "  screen-should-contain [\n"
+      //    01234
+      "    .a    .\n"
+      "    .     .\n"
+      "    .     .\n"
+      "  ]\n"
+      "]\n"
+  );
+  // checks are inside scenario
+}
 
-:(scenario screen_in_scenario_unicode)
-# screen-should-contain can check unicode characters in the fake screen
-scenario screen-in-scenario-unicode [
-  local-scope
-  assume-screen 5/width, 3/height
-  run [
-    lambda:char <- copy 955/greek-small-lambda
-    screen:&:screen <- print screen:&:screen, lambda
-    a:char <- copy 97/a
-    screen:&:screen <- print screen:&:screen, a
-  ]
-  screen-should-contain [
-  #  01234
-    .λa   .
-    .     .
-    .     .
-  ]
-]
-# checks are inside scenario
+void test_screen_in_scenario_unicode() {
+  // screen-should-contain can check unicode characters in the fake screen\n"
+  run_mu_scenario(
+      "scenario screen-in-scenario-unicode [\n"
+      "  local-scope\n"
+      "  assume-screen 5/width, 3/height\n"
+      "  run [\n"
+      "    lambda:char <- copy 955/greek-small-lambda\n"
+      "    screen:&:screen <- print screen:&:screen, lambda\n"
+      "    a:char <- copy 97/a\n"
+      "    screen:&:screen <- print screen:&:screen, a\n"
+      "  ]\n"
+      "  screen-should-contain [\n"
+      //    01234
+      "    .λa   .\n"
+      "    .     .\n"
+      "    .     .\n"
+      "  ]\n"
+      "]\n"
+  );
+  // checks are inside scenario
+}
 
-:(scenario screen_in_scenario_color)
-scenario screen-in-scenario-color [
-  local-scope
-  assume-screen 5/width, 3/height
-  run [
-    lambda:char <- copy 955/greek-small-lambda
-    screen:&:screen <- print screen:&:screen, lambda, 1/red
-    a:char <- copy 97/a
-    screen:&:screen <- print screen:&:screen, a, 7/white
-  ]
-  # screen-should-contain shows everything
-  screen-should-contain [
-  #  01234
-    .λa   .
-    .     .
-    .     .
-  ]
-  # screen-should-contain-in-color filters out everything except the given
-  # color, all you see is the 'a' in white.
-  screen-should-contain-in-color 7/white, [
-  #  01234
-    . a   .
-    .     .
-    .     .
-  ]
-  # ..and the λ in red.
-  screen-should-contain-in-color 1/red, [
-  #  01234
-    .λ    .
-    .     .
-    .     .
-  ]
-]
-# checks are inside scenario
+void test_screen_in_scenario_color() {
+  run_mu_scenario(
+      "scenario screen-in-scenario-color [\n"
+      "  local-scope\n"
+      "  assume-screen 5/width, 3/height\n"
+      "  run [\n"
+      "    lambda:char <- copy 955/greek-small-lambda\n"
+      "    screen:&:screen <- print screen:&:screen, lambda, 1/red\n"
+      "    a:char <- copy 97/a\n"
+      "    screen:&:screen <- print screen:&:screen, a, 7/white\n"
+      "  ]\n"
+         // screen-should-contain shows everything
+      "  screen-should-contain [\n"
+      //    01234
+      "    .λa   .\n"
+      "    .     .\n"
+      "    .     .\n"
+      "  ]\n"
+         // screen-should-contain-in-color filters out everything except the
+         // given color, all you see is the 'a' in white.
+      "  screen-should-contain-in-color 7/white, [\n"
+      //    01234
+      "    . a   .\n"
+      "    .     .\n"
+      "    .     .\n"
+      "  ]\n"
+         // ..and the λ in red.
+      "  screen-should-contain-in-color 1/red, [\n"
+      //    01234
+      "    .λ    .\n"
+      "    .     .\n"
+      "    .     .\n"
+      "  ]\n"
+      "]\n"
+  );
+  // checks are inside scenario
+}
 
-:(scenario screen_in_scenario_error)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-scenario screen-in-scenario-error [
-  local-scope
-  assume-screen 5/width, 3/height
-  run [
-    a:char <- copy 97/a
-    screen:&:screen <- print screen:&:screen, a
-  ]
-  screen-should-contain [
-  #  01234
-    .b    .
-    .     .
-    .     .
-  ]
-]
-+error: F - screen-in-scenario-error: expected screen location (0, 0) to contain 98 ('b') instead of 97 ('a')
+void test_screen_in_scenario_error() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run_mu_scenario(
+      "scenario screen-in-scenario-error [\n"
+      "  local-scope\n"
+      "  assume-screen 5/width, 3/height\n"
+      "  run [\n"
+      "    a:char <- copy 97/a\n"
+      "    screen:&:screen <- print screen:&:screen, a\n"
+      "  ]\n"
+      "  screen-should-contain [\n"
+      //    01234
+      "    .b    .\n"
+      "    .     .\n"
+      "    .     .\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - screen-in-scenario-error: expected screen location (0, 0) to contain 98 ('b') instead of 97 ('a')\n"
+  );
+}
 
-:(scenario screen_in_scenario_color_error)
-% Scenario_testing_scenario = true;
-% Hide_errors = true;
-# screen-should-contain can check unicode characters in the fake screen
-scenario screen-in-scenario-color-error [
-  local-scope
-  assume-screen 5/width, 3/height
-  run [
-    a:char <- copy 97/a
-    screen:&:screen <- print screen:&:screen, a, 1/red
-  ]
-  screen-should-contain-in-color 2/green, [
-  #  01234
-    .a    .
-    .     .
-    .     .
-  ]
-]
-+error: F - screen-in-scenario-color-error: expected screen location (0, 0) to contain 'a' in color 2 instead of 1
+void test_screen_in_scenario_color_error() {
+  Scenario_testing_scenario = true;
+  Hide_errors = true;
+  run_mu_scenario(
+      "scenario screen-in-scenario-color-error [\n"
+      "  local-scope\n"
+      "  assume-screen 5/width, 3/height\n"
+      "  run [\n"
+      "    a:char <- copy 97/a\n"
+      "    screen:&:screen <- print screen:&:screen, a, 1/red\n"
+      "  ]\n"
+      "  screen-should-contain-in-color 2/green, [\n"
+      //    01234
+      "    .a    .\n"
+      "    .     .\n"
+      "    .     .\n"
+      "  ]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: F - screen-in-scenario-color-error: expected screen location (0, 0) to contain 'a' in color 2 instead of 1\n"
+  );
+}
 
-:(scenarios run)
-:(scenario convert_names_does_not_fail_when_mixing_special_names_and_numeric_locations)
-% Scenario_testing_scenario = true;
-def main [
-  screen:num <- copy 1:num
-]
--error: mixing variable names and numeric addresses in main
-$error: 0
-:(scenarios run_mu_scenario)
+void test_convert_names_does_not_fail_when_mixing_special_names_and_numeric_locations() {
+  Scenario_testing_scenario = true;
+  run(
+      "def main [\n"
+      "  screen:num <- copy 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_DOESNT_CONTAIN("error: mixing variable names and numeric addresses in main");
+  CHECK_TRACE_COUNT("error", 0);
+}
 
 //: It's easier to implement assume-screen and other similar scenario-only
 //: primitives if they always write to a fixed location. So we'll assign a
@@ -179,12 +198,18 @@ if (curr.name == "assume-screen") {
   }
 }
 
-:(scenario assume_screen_shows_up_in_errors)
-% Hide_errors = true;
-scenario assume-screen-shows-up-in-errors [
-  assume-screen width, 5
-]
-+error: assume-screen-shows-up-in-errors: missing type for 'width' in 'assume-screen width, 5'
+:(code)
+void test_assume_screen_shows_up_in_errors() {
+  Hide_errors = true;
+  run_mu_scenario(
+      "scenario assume-screen-shows-up-in-errors [\n"
+      "  assume-screen width, 5\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: assume-screen-shows-up-in-errors: missing type for 'width' in 'assume-screen width, 5'\n"
+  );
+}
 
 //: screen-should-contain is a regular instruction
 :(before "End Primitive Recipe Declarations")
diff --git a/085scenario_console.cc b/085scenario_console.cc
index af6e44c2..75c2a289 100644
--- a/085scenario_console.cc
+++ b/085scenario_console.cc
@@ -8,30 +8,33 @@
 :(before "End initialize_transform_rewrite_literal_string_to_text()")
 recipes_taking_literal_strings.insert("assume-console");
 
-:(scenarios run_mu_scenario)
-:(scenario keyboard_in_scenario)
-scenario keyboard-in-scenario [
-  assume-console [
-    type [abc]
-  ]
-  run [
-    1:char, 2:bool <- read-key console
-    3:char, 4:bool <- read-key console
-    5:char, 6:bool <- read-key console
-    7:char, 8:bool, 9:bool <- read-key console
-  ]
-  memory-should-contain [
-    1 <- 97  # 'a'
-    2 <- 1
-    3 <- 98  # 'b'
-    4 <- 1
-    5 <- 99  # 'c'
-    6 <- 1
-    7 <- 0  # unset
-    8 <- 1
-    9 <- 1  # end of test events
-  ]
-]
+:(code)
+void test_keyboard_in_scenario() {
+  run_mu_scenario(
+      "scenario keyboard-in-scenario [\n"
+      "  assume-console [\n"
+      "    type [abc]\n"
+      "  ]\n"
+      "  run [\n"
+      "    1:char, 2:bool <- read-key console\n"
+      "    3:char, 4:bool <- read-key console\n"
+      "    5:char, 6:bool <- read-key console\n"
+      "    7:char, 8:bool, 9:bool <- read-key console\n"
+      "  ]\n"
+      "  memory-should-contain [\n"
+      "    1 <- 97\n"  // 'a'
+      "    2 <- 1\n"
+      "    3 <- 98\n"  // 'b'
+      "    4 <- 1\n"
+      "    5 <- 99\n"  // 'c'
+      "    6 <- 1\n"
+      "    7 <- 0\n"  // unset
+      "    8 <- 1\n"
+      "    9 <- 1\n"  // end of test events
+      "  ]\n"
+      "]\n"
+  );
+}
 
 :(before "End Scenario Globals")
 extern const int CONSOLE = next_predefined_global_for_scenarios(/*size_of(address:console)*/2);
@@ -198,54 +201,57 @@ void initialize_special_name(reagent& r) {
   // End Initialize Type Of Special Name In Scenario(r)
 }
 
-:(scenario events_in_scenario)
-scenario events-in-scenario [
-  assume-console [
-    type [abc]
-    left-click 0, 1
-    press up-arrow
-    type [d]
-  ]
-  run [
-    # 3 keyboard events; each event occupies 4 locations
-    1:event <- read-event console
-    5:event <- read-event console
-    9:event <- read-event console
-    # mouse click
-    13:event <- read-event console
-    # non-character keycode
-    17:event <- read-event console
-    # final keyboard event
-    21:event <- read-event console
-  ]
-  memory-should-contain [
-    1 <- 0  # 'text'
-    2 <- 97  # 'a'
-    3 <- 0  # unused
-    4 <- 0  # unused
-    5 <- 0  # 'text'
-    6 <- 98  # 'b'
-    7 <- 0  # unused
-    8 <- 0  # unused
-    9 <- 0  # 'text'
-    10 <- 99  # 'c'
-    11 <- 0  # unused
-    12 <- 0  # unused
-    13 <- 2  # 'mouse'
-    14 <- 65513  # mouse click
-    15 <- 0  # row
-    16 <- 1  # column
-    17 <- 1  # 'keycode'
-    18 <- 65517  # up arrow
-    19 <- 0  # unused
-    20 <- 0  # unused
-    21 <- 0  # 'text'
-    22 <- 100  # 'd'
-    23 <- 0  # unused
-    24 <- 0  # unused
-    25 <- 0
-  ]
-]
+void test_events_in_scenario() {
+  run_mu_scenario(
+      "scenario events-in-scenario [\n"
+      "  assume-console [\n"
+      "    type [abc]\n"
+      "    left-click 0, 1\n"
+      "    press up-arrow\n"
+      "    type [d]\n"
+      "  ]\n"
+      "  run [\n"
+           // 3 keyboard events; each event occupies 4 locations
+      "    1:event <- read-event console\n"
+      "    5:event <- read-event console\n"
+      "    9:event <- read-event console\n"
+           // mouse click
+      "    13:event <- read-event console\n"
+           // non-character keycode
+      "    17:event <- read-event console\n"
+           // final keyboard event
+      "    21:event <- read-event console\n"
+      "  ]\n"
+      "  memory-should-contain [\n"
+      "    1 <- 0\n"  // 'text'
+      "    2 <- 97\n"  // 'a'
+      "    3 <- 0\n"  // unused
+      "    4 <- 0\n"  // unused
+      "    5 <- 0\n"  // 'text'
+      "    6 <- 98\n"  // 'b'
+      "    7 <- 0\n"  // unused
+      "    8 <- 0\n"  // unused
+      "    9 <- 0\n"  // 'text'
+      "    10 <- 99\n"  // 'c'
+      "    11 <- 0\n"  // unused
+      "    12 <- 0\n"  // unused
+      "    13 <- 2\n"  // 'mouse'
+      "    14 <- 65513\n"  // mouse click
+      "    15 <- 0\n"  // row
+      "    16 <- 1\n"  // column
+      "    17 <- 1\n"  // 'keycode'
+      "    18 <- 65517\n"  // up arrow
+      "    19 <- 0\n"  // unused
+      "    20 <- 0\n"  // unused
+      "    21 <- 0\n"  // 'text'
+      "    22 <- 100\n"  // 'd'
+      "    23 <- 0\n"  // unused
+      "    24 <- 0\n"  // unused
+      "    25 <- 0\n"
+      "  ]\n"
+      "]\n"
+  );
+}
 
 //: Deal with special keys and unmatched brackets by allowing each test to
 //: independently choose the unicode symbol to denote them.
diff --git a/089scenario_filesystem.cc b/089scenario_filesystem.cc
index b0429ea7..c49c20f8 100644
--- a/089scenario_filesystem.cc
+++ b/089scenario_filesystem.cc
@@ -2,73 +2,78 @@
 //: Instruction 'assume-resources' implicitly creates a variable called
 //: 'resources' that is accessible to later instructions in the scenario.
 
-:(scenarios run_mu_scenario)
-:(scenario simple_filesystem)
-scenario simple-filesystem [
-  local-scope
-  assume-resources [
-    # file 'a' containing two lines of data
-    [a] <- [
-      |a bc|
-      |de f|
-    ]
-    # directory 'b' containing two files, 'c' and 'd'
-    [b/c] <- []
-    [b/d] <- [
-      |xyz|
-    ]
-  ]
-  data:&:@:resource <- get *resources, data:offset
-  file1:resource <- index *data, 0
-  file1-name:text <- get file1, name:offset
-  10:@:char/raw <- copy *file1-name
-  file1-contents:text <- get file1, contents:offset
-  100:@:char/raw <- copy *file1-contents
-  file2:resource <- index *data, 1
-  file2-name:text <- get file2, name:offset
-  30:@:char/raw <- copy *file2-name
-  file2-contents:text <- get file2, contents:offset
-  40:@:char/raw <- copy *file2-contents
-  file3:resource <- index *data, 2
-  file3-name:text <- get file3, name:offset
-  50:@:char/raw <- copy *file3-name
-  file3-contents:text <- get file3, contents:offset
-  60:@:char/raw <- copy *file3-contents
-  memory-should-contain [
-    10:array:character <- [a]
-    100:array:character <- [a bc
-de f
-]
-    30:array:character <- [b/c]
-    40:array:character <- []
-    50:array:character <- [b/d]
-    60:array:character <- [xyz
-]
-  ]
-]
+void test_simple_filesystem() {
+  run_mu_scenario(
+      "scenario simple-filesystem [\n"
+      "  local-scope\n"
+      "  assume-resources [\n"
+           // file 'a' containing two lines of data
+      "    [a] <- [\n"
+      "      |a bc|\n"
+      "      |de f|\n"
+      "    ]\n"
+           // directory 'b' containing two files, 'c' and 'd'
+      "    [b/c] <- []\n"
+      "    [b/d] <- [\n"
+      "      |xyz|\n"
+      "    ]\n"
+      "  ]\n"
+      "  data:&:@:resource <- get *resources, data:offset\n"
+      "  file1:resource <- index *data, 0\n"
+      "  file1-name:text <- get file1, name:offset\n"
+      "  10:@:char/raw <- copy *file1-name\n"
+      "  file1-contents:text <- get file1, contents:offset\n"
+      "  100:@:char/raw <- copy *file1-contents\n"
+      "  file2:resource <- index *data, 1\n"
+      "  file2-name:text <- get file2, name:offset\n"
+      "  30:@:char/raw <- copy *file2-name\n"
+      "  file2-contents:text <- get file2, contents:offset\n"
+      "  40:@:char/raw <- copy *file2-contents\n"
+      "  file3:resource <- index *data, 2\n"
+      "  file3-name:text <- get file3, name:offset\n"
+      "  50:@:char/raw <- copy *file3-name\n"
+      "  file3-contents:text <- get file3, contents:offset\n"
+      "  60:@:char/raw <- copy *file3-contents\n"
+      "  memory-should-contain [\n"
+      "    10:array:character <- [a]\n"
+      "    100:array:character <- [a bc\n"
+      "de f\n"
+      "]\n"
+      "    30:array:character <- [b/c]\n"
+      "    40:array:character <- []\n"
+      "    50:array:character <- [b/d]\n"
+      "    60:array:character <- [xyz\n"
+      "]\n"
+      "  ]\n"
+      "]\n"
+  );
+}
 
-:(scenario escaping_file_contents)
-scenario escaping-file-contents [
-  local-scope
-  assume-resources [
-    # file 'a' containing a '|'
-    # need to escape '\' once for each block
-    [a] <- [
-      |x\\\\|yz|
-    ]
-  ]
-  data:&:@:resource <- get *resources, data:offset
-  file1:resource <- index *data, 0
-  file1-name:text <- get file1, name:offset
-  10:@:char/raw <- copy *file1-name
-  file1-contents:text <- get file1, contents:offset
-  20:@:char/raw <- copy *file1-contents
-  memory-should-contain [
-    10:array:character <- [a]
-    20:array:character <- [x|yz
-]
-  ]
-]
+void test_escaping_file_contents() {
+  run_mu_scenario(
+      "scenario escaping-file-contents [\n"
+      "  local-scope\n"
+      "  assume-resources [\n"
+           // file 'a' containing a '|'
+           // need to escape '\\' once for each block
+      "    [a] <- [\n"
+      "      |x\\\\\\\\|yz|\n"
+      "    ]\n"
+      "  ]\n"
+      "  data:&:@:resource <- get *resources, data:offset\n"
+      "  file1:resource <- index *data, 0\n"
+      "  file1-name:text <- get file1, name:offset\n"
+      "  10:@:char/raw <- copy *file1-name\n"
+      "  file1-contents:text <- get file1, contents:offset\n"
+      "  20:@:char/raw <- copy *file1-contents\n"
+      "  memory-should-contain [\n"
+      "    10:array:character <- [a]\n"
+      "    20:array:character <- [x|yz\n"
+      "]\n"
+      "  ]\n"
+      "]\n"
+  );
+}
 
 :(before "End Globals")
 extern const int RESOURCES = next_predefined_global_for_scenarios(/*size_of(address:resources)*/2);
diff --git a/099hardware_checks.cc b/099hardware_checks.cc
index f84837d2..c1039c1f 100644
--- a/099hardware_checks.cc
+++ b/099hardware_checks.cc
@@ -54,10 +54,14 @@ void check_for_misuse_of_real_hardware(const recipe_ordinal r) {
   }
 }
 
-:(scenarios transform)
-:(scenario warn_on_using_real_screen_directly_in_non_main_recipe)
-% Hide_errors = true;
-def foo [
-  print 0, 34
-]
-+error: foo: 'print 0, 34': only 'main' can pass 0 into a (address screen)
+void test_warn_on_using_real_screen_directly_in_non_main_recipe() {
+  Hide_errors = true;
+  transform(
+      "def foo [\n"
+      "  print 0, 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: foo: 'print 0, 34': only 'main' can pass 0 into a (address screen)\n"
+  );
+}
diff --git a/101run_sandboxed.cc b/101run_sandboxed.cc
index e196081f..e464bbe3 100644
--- a/101run_sandboxed.cc
+++ b/101run_sandboxed.cc
@@ -1,24 +1,34 @@
 //: Helper for various programming environments: run arbitrary Mu code and
 //: return some result in text form.
 
-:(scenario run_interactive_code)
-def main [
-  1:num <- copy 0  # reserve space for the sandbox
-  10:text <- new [1:num/raw <- copy 34]
-#?   $print 10:num [|] 11:num [: ] 1000:num [|] *10:text [ (] 10:text [)] 10/newline
-  run-sandboxed 10:text
-  20:num <- copy 1:num
-]
-+mem: storing 34 in location 20
-
-:(scenario run_interactive_empty)
-def main [
-  10:text <- copy null
-  20:text <- run-sandboxed 10:text
-]
-# result is null
-+mem: storing 0 in location 20
-+mem: storing 0 in location 21
+void test_run_interactive_code() {
+  run(
+      "def main [\n"
+      "  1:num <- copy 0\n"  // reserve space for the sandbox
+      "  10:text <- new [1:num/raw <- copy 34]\n"
+//?       "  $print 10:num [|] 11:num [: ] 1000:num [|] *10:text [ (] 10:text [)] 10/newline\n"
+      "  run-sandboxed 10:text\n"
+      "  20:num <- copy 1:num\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 20\n"
+  );
+}
+
+void test_run_interactive_empty() {
+  run(
+      "def main [\n"
+      "  10:text <- copy null\n"
+      "  20:text <- run-sandboxed 10:text\n"
+      "]\n"
+  );
+  // result is null
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 20\n"
+      "mem: storing 0 in location 21\n"
+  );
+}
 
 //: As the name suggests, 'run-sandboxed' will prevent certain operations that
 //: regular Mu code can perform.
@@ -251,14 +261,20 @@ load(string(
 :(before "End maybe(recipe_name) Special-cases")
 if (recipe_name == "interactive") return "";
 
-:(scenario run_interactive_comments)
-def main [
-  1:text <- new [# ab
-add 2, 2]
-  2:text <- run-sandboxed 1:text
-  3:@:char <- copy *2:text
-]
-+mem: storing 52 in location 4
+:(code)
+void test_run_interactive_comments() {
+  run(
+      "def main [\n"
+      "  1:text <- new [# ab\n"
+      "add 2, 2]\n"
+      "  2:text <- run-sandboxed 1:text\n"
+      "  3:@:char <- copy *2:text\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 52 in location 4\n"
+  );
+}
 
 :(before "End Primitive Recipe Declarations")
 _START_TRACKING_PRODUCTS,
@@ -351,111 +367,147 @@ case _CLEANUP_RUN_SANDBOXED: {
   break;
 }
 
-:(scenario "run_interactive_converts_result_to_text")
-def main [
-  # try to interactively add 2 and 2
-  10:text <- new [add 2, 2]
-  20:text <- run-sandboxed 10:text
-  30:@:char <- copy *20:text
-]
-# first letter in the output should be '4' in unicode
-+mem: storing 52 in location 31
-
-:(scenario "run_interactive_ignores_products_in_nested_functions")
-def main [
-  10:text <- new [foo]
-  20:text <- run-sandboxed 10:text
-  30:@:char <- copy *20:text
-]
-def foo [
-  40:num <- copy 1234
-  {
-    break
-    reply 5678
-  }
-]
-# no product should have been tracked
-+mem: storing 0 in location 30
-
-:(scenario "run_interactive_ignores_products_in_previous_instructions")
-def main [
-  10:text <- new [
-    add 1, 1  # generates a product
-    foo]  # no products
-  20:text <- run-sandboxed 10:text
-  30:@:char <- copy *20:text
-]
-def foo [
-  40:num <- copy 1234
-  {
-    break
-    reply 5678
-  }
-]
-# no product should have been tracked
-+mem: storing 0 in location 30
-
-:(scenario "run_interactive_remembers_products_before_final_label")
-def main [
-  10:text <- new [
-    add 1, 1  # generates a product
-    +foo]  # no products
-  20:text <- run-sandboxed 10:text
-  30:@:char <- copy *20:text
-]
-def foo [
-  40:num <- copy 1234
-  {
-    break
-    reply 5678
-  }
-]
-# product tracked
-+mem: storing 50 in location 31
-
-:(scenario "run_interactive_returns_text")
-def main [
-  # try to interactively add 2 and 2
-  1:text <- new [
-    x:text <- new [a]
-    y:text <- new [b]
-    z:text <- append x:text, y:text
-  ]
-  10:text <- run-sandboxed 1:text
-#?   $print 10:text 10/newline
-  20:@:char <- copy *10:text
-]
-# output contains "ab"
-+mem: storing 97 in location 21
-+mem: storing 98 in location 22
-
-:(scenario "run_interactive_returns_errors")
-def main [
-  # run a command that generates an error
-  10:text <- new [x:num <- copy 34
-get x:num, foo:offset]
-  20:text, 30:text <- run-sandboxed 10:text
-  40:@:char <- copy *30:text
-]
-# error should be "unknown element foo in container number"
-+mem: storing 117 in location 41
-+mem: storing 110 in location 42
-+mem: storing 107 in location 43
-+mem: storing 110 in location 44
-# ...
-
-:(scenario run_interactive_with_comment)
-def main [
-  # 2 instructions, with a comment after the first
-  10:text <- new [a:num <- copy 0  # abc
-b:num <- copy 0
-]
-  20:text, 30:text <- run-sandboxed 10:text
-]
-# no errors
-# skip alloc id
-+mem: storing 0 in location 30
-+mem: storing 0 in location 31
+:(code)
+void test_run_interactive_converts_result_to_text() {
+  // try to interactively add 2 and 2
+  run(
+      "def main [\n"
+      "  10:text <- new [add 2, 2]\n"
+      "  20:text <- run-sandboxed 10:text\n"
+      "  30:@:char <- copy *20:text\n"
+      "]\n"
+  );
+  // first letter in the output should be '4' in utf-8
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 52 in location 31\n"
+  );
+}
+
+void test_run_interactive_ignores_products_in_nested_functions() {
+  run(
+      "def main [\n"
+      "  10:text <- new [foo]\n"
+      "  20:text <- run-sandboxed 10:text\n"
+      "  30:@:char <- copy *20:text\n"
+      "]\n"
+      "def foo [\n"
+      "  40:num <- copy 1234\n"
+      "  {\n"
+      "    break\n"
+      "    reply 5678\n"
+      "  }\n"
+      "]\n"
+  );
+  // no product should have been tracked
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 30\n"
+  );
+}
+
+void test_run_interactive_ignores_products_in_previous_instructions() {
+  run(
+      "def main [\n"
+      "  10:text <- new [\n"
+      "    add 1, 1\n"  // generates a product
+      "    foo]\n"  // no products
+      "  20:text <- run-sandboxed 10:text\n"
+      "  30:@:char <- copy *20:text\n"
+      "]\n"
+      "def foo [\n"
+      "  40:num <- copy 1234\n"
+      "  {\n"
+      "    break\n"
+      "    reply 5678\n"
+      "  }\n"
+      "]\n"
+  );
+  // no product should have been tracked
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 30\n"
+  );
+}
+
+void test_run_interactive_remembers_products_before_final_label() {
+  run(
+      "def main [\n"
+      "  10:text <- new [\n"
+      "    add 1, 1\n"  // generates a product
+      "    +foo]\n"  // no products
+      "  20:text <- run-sandboxed 10:text\n"
+      "  30:@:char <- copy *20:text\n"
+      "]\n"
+      "def foo [\n"
+      "  40:num <- copy 1234\n"
+      "  {\n"
+      "    break\n"
+      "    reply 5678\n"
+      "  }\n"
+      "]\n"
+  );
+  // product tracked
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 50 in location 31\n"
+  );
+}
+
+void test_run_interactive_returns_text() {
+  // try to interactively add 2 and 2
+  run(
+      "def main [\n"
+      "  1:text <- new [\n"
+      "    x:text <- new [a]\n"
+      "    y:text <- new [b]\n"
+      "    z:text <- append x:text, y:text\n"
+      "  ]\n"
+      "  10:text <- run-sandboxed 1:text\n"
+//?       "  $print 10:text 10/newline\n"
+      "  20:@:char <- copy *10:text\n"
+      "]\n"
+  );
+  // output contains "ab"
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 97 in location 21\n"
+      "mem: storing 98 in location 22\n"
+  );
+}
+
+void test_run_interactive_returns_errors() {
+  run(
+      "def main [\n"
+         // run a command that generates an error
+      "  10:text <- new [x:num <- copy 34\n"
+      "get x:num, foo:offset]\n"
+      "  20:text, 30:text <- run-sandboxed 10:text\n"
+      "  40:@:char <- copy *30:text\n"
+      "]\n"
+  );
+  // error should be "unknown element foo in container number"
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 117 in location 41\n"
+      "mem: storing 110 in location 42\n"
+      "mem: storing 107 in location 43\n"
+      "mem: storing 110 in location 44\n"
+      // ...
+  );
+}
+
+void test_run_interactive_with_comment() {
+  run(
+      "def main [\n"
+         // 2 instructions, with a comment after the first
+      "  10:text <- new [a:num <- copy 0  # abc\n"
+      "b:num <- copy 0\n"
+      "]\n"
+      "  20:text, 30:text <- run-sandboxed 10:text\n"
+      "]\n"
+  );
+  // no errors
+  // skip alloc id
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 30\n"
+      "mem: storing 0 in location 31\n"
+  );
+}
 
 :(after "Running One Instruction")
 if (Track_most_recent_products && SIZE(Current_routine->calls) == Call_depth_to_track_most_recent_products_at
@@ -594,49 +646,66 @@ case RELOAD: {
   break;
 }
 
-:(scenario reload_loads_function_definitions)
-def main [
-  local-scope
-  x:text <- new [recipe foo [
-    1:num/raw <- copy 34
-  ]]
-  reload x
-  run-sandboxed [foo]
-  2:num/raw <- copy 1:num/raw
-]
-+mem: storing 34 in location 2
-
-:(scenario reload_continues_past_error)
-def main [
-  local-scope
-  x:text <- new [recipe foo [
-    get 1234:num, foo:offset
-  ]]
-  reload x
-  1:num/raw <- copy 34
-]
-+mem: storing 34 in location 1
-
-:(scenario reload_can_repeatedly_load_container_definitions)
-# define a container and try to create it (merge requires knowing container size)
-def main [
-  local-scope
-  x:text <- new [
-    container foo [
-      x:num
-      y:num
-    ]
-    recipe bar [
-      local-scope
-      x:foo <- merge 34, 35
-    ]
-  ]
-  # save warning addresses in locations of type 'number' to avoid spurious changes to them due to 'abandon'
-  10:text/raw <- reload x
-  20:text/raw <- reload x
-]
-# no errors on either load
-+mem: storing 0 in location 10
-+mem: storing 0 in location 11
-+mem: storing 0 in location 20
-+mem: storing 0 in location 21
+:(code)
+void test_reload_loads_function_definitions() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:text <- new [recipe foo [\n"
+      "    1:num/raw <- copy 34\n"
+      "  ]]\n"
+      "  reload x\n"
+      "  run-sandboxed [foo]\n"
+      "  2:num/raw <- copy 1:num/raw\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 2\n"
+  );
+}
+
+void test_reload_continues_past_error() {
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:text <- new [recipe foo [\n"
+      "    get 1234:num, foo:offset\n"
+      "  ]]\n"
+      "  reload x\n"
+      "  1:num/raw <- copy 34\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 34 in location 1\n"
+  );
+}
+
+void test_reload_can_repeatedly_load_container_definitions() {
+  // define a container and try to create it (merge requires knowing container size)
+  run(
+      "def main [\n"
+      "  local-scope\n"
+      "  x:text <- new [\n"
+      "    container foo [\n"
+      "      x:num\n"
+      "      y:num\n"
+      "    ]\n"
+      "    recipe bar [\n"
+      "      local-scope\n"
+      "      x:foo <- merge 34, 35\n"
+      "    ]\n"
+      "  ]\n"
+         // save warning addresses in locations of type 'number' to avoid
+         // spurious changes to them due to 'abandon'
+      "  10:text/raw <- reload x\n"
+      "  20:text/raw <- reload x\n"
+      "]\n"
+  );
+  // no errors on either load
+  CHECK_TRACE_CONTENTS(
+      "mem: storing 0 in location 10\n"
+      "mem: storing 0 in location 11\n"
+      "mem: storing 0 in location 20\n"
+      "mem: storing 0 in location 21\n"
+  );
+}
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);
diff --git a/tangle/001trace.cc b/tangle/001trace.cc
index e78c0010..520801b7 100644
--- a/tangle/001trace.cc
+++ b/tangle/001trace.cc
@@ -69,7 +69,7 @@ struct lease_tracer {
 #define START_TRACING_UNTIL_END_OF_SCOPE  lease_tracer leased_tracer;
 
 bool check_trace_contents(string FUNCTION, string FILE, int LINE, string layer, string expected) {  // empty layer == everything
-  vector<string> expected_lines = split(expected, "");
+  vector<string> expected_lines = split(expected, "\n");
   size_t curr_expected_line = 0;
   while (curr_expected_line < expected_lines.size() && expected_lines[curr_expected_line].empty())
     ++curr_expected_line;
diff --git a/tangle/001trace.test.cc b/tangle/001trace.test.cc
index 5f0a696e..2d1b54d1 100644
--- a/tangle/001trace.test.cc
+++ b/tangle/001trace.test.cc
@@ -28,18 +28,21 @@ void test_trace_check_treats_empty_layers_as_wildcards() {
 void test_trace_check_multiple_lines_at_once() {
   trace("test layer 1") << "foo";
   trace("test layer 2") << "bar";
-  CHECK_TRACE_CONTENTS("", "foobar");
+  CHECK_TRACE_CONTENTS("", "foo\n"
+                           "bar\n");
 }
 
 void test_trace_check_always_finds_empty_lines2() {
-  CHECK_TRACE_CONTENTS("test layer 1", "");
+  CHECK_TRACE_CONTENTS("test layer 1", "\n\n\n");
 }
 
 void test_trace_orders_across_layers() {
   trace("test layer 1") << "foo";
   trace("test layer 2") << "bar";
   trace("test layer 1") << "qux";
-  CHECK_TRACE_CONTENTS("", "foobarqux");
+  CHECK_TRACE_CONTENTS("", "foo\n"
+                           "bar\n"
+                           "qux\n");
 }
 
 void test_trace_supports_count() {
diff --git a/tangle/003tangle.cc b/tangle/003tangle.cc
index bcb3c315..64c841f1 100644
--- a/tangle/003tangle.cc
+++ b/tangle/003tangle.cc
@@ -100,7 +100,6 @@ void process_next_hunk(istream& in, const string& directive, const string& filen
 
   // first slurp all lines until next directive
   list<Line> hunk;
-  bool end_of_scenario_input = false;
   {
     string curr_line;
     while (!in.eof()) {
@@ -115,22 +114,6 @@ void process_next_hunk(istream& in, const string& directive, const string& filen
         ++line_number;
         continue;
       }
-      if (cmd == "scenario") {
-        // ignore mu comments in scenarios, but only after the end of input
-        if (!starts_with(curr_line, "#") && !is_input(curr_line)) {
-          // remaining lines are checks
-          end_of_scenario_input = true;
-        }
-        else if (end_of_scenario_input && starts_with(curr_line, "#")) {
-          ++line_number;
-          continue;
-        }
-        if (trim(curr_line).empty()) {
-          // ignore empty lines in scenarios, whether in input of after
-          ++line_number;
-          continue;
-        }
-      }
       hunk.push_back(Line(curr_line, filename, line_number));
       ++line_number;
     }
@@ -141,21 +124,6 @@ void process_next_hunk(istream& in, const string& directive, const string& filen
     return;
   }
 
-  if (cmd == "scenarios") {
-    Toplevel = next_tangle_token(directive_stream);
-    return;
-  }
-
-  if (cmd == "scenario") {
-    list<Line> result;
-    string name = next_tangle_token(directive_stream);
-    emit_test(name, hunk, result);
-//?     cerr << out.size() << " " << result.size() << '\n';
-    out.insert(out.end(), result.begin(), result.end());
-//?     cerr << out.size() << " " << result.size() << '\n';
-    return;
-  }
-
   if (cmd == "before" || cmd == "after" || cmd == "replace" || cmd == "replace{}" || cmd == "delete" || cmd == "delete{}") {
     list<Line>::iterator target = locate_target(out, directive_stream);
     if (target == out.end()) {
@@ -291,111 +259,6 @@ list<Line>::iterator balancing_curly(list<Line>::iterator curr) {
   return curr;
 }
 
-// A scenario is one or more sessions separated by calls to CLEAR_TRACE ('===')
-//   A session is:
-//     one or more lines of escaped setup in C/C++ ('%')
-//   followed by one or more lines of input,
-//   followed optionally by (in order):
-//     one or more lines expected in trace in order ('+') and one or more lines trace shouldn't include ('-')
-//     one or more lines expressing counts of specific layers emitted in trace ('$')
-//     a directive to print the trace just for debugging ('?')
-// Remember to update is_input below if you add to this format.
-//
-// Allowing interleaving of '+' and '-' lines is a kludgy way to indicate that
-// two sets of trace lines can occur in any order. We should come up with a
-// better way to specify order-independence.
-void emit_test(const string& name, list<Line>& lines, list<Line>& result) {
-  result.push_back(Line("void test_"+name+"() {", front(lines).filename, front(lines).line_number-1));  // use line number of directive
-//?   result.push_back("cerr << \""+name+"\\n\";");  // debug: uncomment this to print scenario names as you run them
-  while (!lines.empty()) {
-    while (!lines.empty() && starts_with(front(lines).contents, "% ")) {
-      result.push_back(Line("  "+front(lines).contents.substr(strlen("% ")), front(lines)));
-      lines.pop_front();
-    }
-    if (lines.empty()) break;
-    emit_input_lines(lines, result);
-    emit_expected_in_trace(lines, result);
-    while (!lines.empty() && !front(lines).contents.empty() && front(lines).contents.at(0) == '-') {
-      result.push_back(expected_not_in_trace(front(lines)));
-      lines.pop_front();
-    }
-    if (!lines.empty() && front(lines).contents.at(0) == '$') {
-      const string& in = front(lines).contents;
-      size_t pos = in.find(": ");
-      string layer = in.substr(1, pos-1);
-      string count = in.substr(pos+2);
-      result.push_back(Line("  CHECK_TRACE_COUNT(\""+layer+"\", "+count+");", front(lines)));
-      lines.pop_front();
-    }
-    if (!lines.empty() && front(lines).contents == "===") {
-      result.push_back(Line("  CLEAR_TRACE;", front(lines)));
-      lines.pop_front();
-    }
-    if (!lines.empty() && front(lines).contents == "?") {
-      result.push_back(Line("  DUMP(\"\");", front(lines)));
-      lines.pop_front();
-    }
-  }
-  result.push_back(Line("}"));
-}
-
-bool is_input(const string& line) {
-  if (line.empty()) return true;
-  return line != "===" && line.find_first_of("+-$?%") != 0;
-}
-
-void emit_input_lines(list<Line>& hunk, list<Line>& out) {
-  assert(!hunk.empty());
-  if (!is_input(front(hunk).contents)) return;
-  Line curr_out;
-  curr_out.line_number = hunk.front().line_number;
-  curr_out.filename = hunk.front().filename;
-  curr_out.contents = "  "+Toplevel+"(";
-  out.push_back(curr_out);
-  for (/*nada*/;  !hunk.empty() && is_input(front(hunk).contents);  hunk.pop_front()) {
-    Line curr_out;
-    curr_out.line_number = front(hunk).line_number;
-    curr_out.filename = front(hunk).filename;
-    curr_out.contents = "      \""+escape(front(hunk).contents+'')+"\"";
-    out.push_back(curr_out);
-  }
-  curr_out.line_number = out.back().line_number;
-  curr_out.filename = out.back().filename;
-  curr_out.contents = "  );";
-  out.push_back(curr_out);
-}
-
-// pull lines starting with '+' out of 'hunk', and append translated lines to 'out'
-void emit_expected_in_trace(list<Line>& hunk, list<Line>& out) {
-  if (hunk.empty()) return;
-  if (front(hunk).contents.empty()) return;
-  if (front(hunk).contents.at(0) != '+') return;
-  Line curr_out;
-  curr_out.line_number = front(hunk).line_number;
-  curr_out.filename = front(hunk).filename;
-  curr_out.contents = "  CHECK_TRACE_CONTENTS(";
-  out.push_back(curr_out);
-  for (/*nada*/;  !hunk.empty() && front(hunk).contents.at(0) == '+';  hunk.pop_front()) {
-    Line curr_out;
-    curr_out.line_number = front(hunk).line_number;
-    curr_out.filename = front(hunk).filename;
-    curr_out.contents = "      \""+escape(front(hunk).contents.substr(1))+"\"";
-    out.push_back(curr_out);
-  }
-  curr_out.line_number = out.back().line_number;
-  curr_out.filename = out.back().filename;
-  curr_out.contents = "  );";
-  out.push_back(curr_out);
-}
-
-Line expected_not_in_trace(const Line& line) {
-  Line result;
-  result.line_number = line.line_number;
-  result.filename = line.filename;
-  result.contents = "  CHECK_TRACE_DOESNT_CONTAIN(\""+escape(line.contents.substr(1))+"\");";
-  return result;
-}
-
 list<Line>::iterator find_substr(list<Line>& in, const string& pat) {
   for (list<Line>::iterator p = in.begin(); p != in.end(); ++p)
     if (p->contents.find(pat) != string::npos)
@@ -430,18 +293,6 @@ string replace_all(string s, const string& a, const string& b) {
   return s;
 }
 
-bool any_line_starts_with(const list<Line>& lines, const string& pat) {
-  for (list<Line>::const_iterator p = lines.begin(); p != lines.end(); ++p)
-    if (starts_with(p->contents, pat)) return true;
-  return false;
-}
-
-bool any_non_input_line(const list<Line>& lines) {
-  for (list<Line>::const_iterator p = lines.begin(); p != lines.end(); ++p)
-    if (!is_input(p->contents)) return true;
-  return false;
-}
-
 // does s start with pat, after skipping whitespace?
 // pat can't start with whitespace
 bool starts_with(const string& s, const string& pat) {
diff --git a/tangle/003tangle.test.cc b/tangle/003tangle.test.cc
index 732c8f2e..c7cc96b4 100644
--- a/tangle/003tangle.test.cc
+++ b/tangle/003tangle.test.cc
@@ -6,10 +6,10 @@ void test_tangle() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "d"
-                                 "b"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "d\n"
+                                 "b\n"
+                                 "c\n");
 }
 
 void test_tangle_with_linenumber() {
@@ -20,13 +20,13 @@ void test_tangle_with_linenumber() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "#line 1"
-                                 "a"
-                                 "#line 5"
-                                 "d"
-                                 "#line 2"
-                                 "b"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "#line 1\n"
+                                 "a\n"
+                                 "#line 5\n"
+                                 "d\n"
+                                 "#line 2\n"
+                                 "b\n"
+                                 "c\n");
   // no other #line directives
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "#line 3");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "#line 4");
@@ -40,11 +40,11 @@ void test_tangle_linenumbers_with_filename() {
                    "d\n");
   list<Line> dummy;
   tangle(in, "foo", dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "#line 5 \"foo\""
-                                 "d"
-                                 "b"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "#line 5 \"foo\"\n"
+                                 "d\n"
+                                 "b\n"
+                                 "c\n");
 }
 
 void test_tangle_line_numbers_with_multiple_filenames() {
@@ -57,12 +57,12 @@ void test_tangle_line_numbers_with_multiple_filenames() {
   istringstream in2(":(before b)\n"
                     "d\n");
   tangle(in2, "bar", dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "#line 2 \"bar\""
-                                 "d"
-                                 "#line 2 \"foo\""
-                                 "b"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "#line 2 \"bar\"\n"
+                                 "d\n"
+                                 "#line 2 \"foo\"\n"
+                                 "b\n"
+                                 "c\n");
 }
 
 void test_tangle_linenumbers_with_multiple_directives() {
@@ -77,15 +77,15 @@ void test_tangle_linenumbers_with_multiple_directives() {
                     ":(before c)\n"
                     "e");
   tangle(in2, "bar", dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "#line 2 \"bar\""
-                                 "d"
-                                 "#line 2 \"foo\""
-                                 "b"
-                                 "#line 4 \"bar\""
-                                 "e"
-                                 "#line 3 \"foo\""
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "#line 2 \"bar\"\n"
+                                 "d\n"
+                                 "#line 2 \"foo\"\n"
+                                 "b\n"
+                                 "#line 4 \"bar\"\n"
+                                 "e\n"
+                                 "#line 3 \"foo\"\n"
+                                 "c\n");
 }
 
 void test_tangle_with_multiple_filenames_after() {
@@ -98,12 +98,12 @@ void test_tangle_with_multiple_filenames_after() {
   istringstream in2(":(after b)\n"
                     "d\n");
   tangle(in2, "bar", dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "b"
-                                 "#line 2 \"bar\""
-                                 "d"
-                                 "#line 3 \"foo\""
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "b\n"
+                                 "#line 2 \"bar\"\n"
+                                 "d\n"
+                                 "#line 3 \"foo\"\n"
+                                 "c\n");
 }
 
 void test_tangle_skip_tanglecomments() {
@@ -115,12 +115,12 @@ void test_tangle_skip_tanglecomments() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "b"
-                                 "c"
-                                 ""
-                                 ""
-                                 "d");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "b\n"
+                                 "c\n"
+                                 "\n"
+                                 "\n"
+                                 "d\n");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "//: 1");
 }
 
@@ -135,14 +135,14 @@ void test_tangle_with_tanglecomments_and_directive() {
                    "e\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "#line 6"
-                                 "d"
-                                 "#line 3"
-                                 "b"
-                                 "c"
-                                 "#line 8"
-                                 "e");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "#line 6\n"
+                                 "d\n"
+                                 "#line 3\n"
+                                 "b\n"
+                                 "c\n"
+                                 "#line 8\n"
+                                 "e\n");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "//: 1");
 }
 
@@ -158,14 +158,14 @@ void test_tangle_with_tanglecomments_inside_directive() {
                    "e\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "#line 7"
-                                 "d"
-                                 "#line 3"
-                                 "b"
-                                 "c"
-                                 "#line 9"
-                                 "e");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "#line 7\n"
+                                 "d\n"
+                                 "#line 3\n"
+                                 "b\n"
+                                 "c\n"
+                                 "#line 9\n"
+                                 "e\n");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "//: 1");
 }
 
@@ -176,9 +176,9 @@ void test_tangle_with_multiword_directives() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a b"
-                                 "d"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a b\n"
+                                 "d\n"
+                                 "c\n");
 }
 
 void test_tangle_with_quoted_multiword_directives() {
@@ -188,9 +188,9 @@ void test_tangle_with_quoted_multiword_directives() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a \"b\""
-                                 "d"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a \"b\"\n"
+                                 "d\n"
+                                 "c\n");
 }
 
 void test_tangle2() {
@@ -201,10 +201,10 @@ void test_tangle2() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "b"
-                                 "d"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "b\n"
+                                 "d\n"
+                                 "c\n");
 }
 
 void test_tangle_at_end() {
@@ -215,10 +215,10 @@ void test_tangle_at_end() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "b"
-                                 "c"
-                                 "d");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "b\n"
+                                 "c\n"
+                                 "d\n");
 }
 
 void test_tangle_indents_hunks_correctly() {
@@ -229,10 +229,10 @@ void test_tangle_indents_hunks_correctly() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "  b"
-                                 "  d"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "  b\n"
+                                 "  d\n"
+                                 "c\n");
 }
 
 void test_tangle_warns_on_missing_target() {
@@ -261,7 +261,7 @@ void test_tangle_delete_range_of_lines() {
                    ":(delete{} \"b\")\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a");
+  CHECK_TRACE_CONTENTS("tangle", "a\n");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "b");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "c");
 }
@@ -274,9 +274,9 @@ void test_tangle_replace() {
                    "d\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "d"
-                                 "c");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "d\n"
+                                 "c\n");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "b");
 }
 
@@ -290,9 +290,9 @@ void test_tangle_replace_range_of_lines() {
                    "e\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "d"
-                                 "e");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "d\n"
+                                 "e\n");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "b {");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "c");
 }
@@ -308,10 +308,10 @@ void test_tangle_replace_tracks_old_lines() {
                    "e\n");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "d"
-                                 "c"
-                                 "e");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "d\n"
+                                 "c\n"
+                                 "e\n");
   CHECK_TRACE_DOESNT_CONTAIN("tangle", "b {");
 }
 
@@ -325,12 +325,12 @@ void test_tangle_nested_patterns() {
                    "e");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "c"
-                                 "b"
-                                 "c"
-                                 "e"
-                                 "d");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "c\n"
+                                 "b\n"
+                                 "c\n"
+                                 "e\n"
+                                 "d\n");
 }
 
 void test_tangle_nested_patterns2() {
@@ -343,346 +343,16 @@ void test_tangle_nested_patterns2() {
                    "e");
   list<Line> dummy;
   tangle(in, dummy);
-  CHECK_TRACE_CONTENTS("tangle", "a"
-                                 "c"
-                                 "b"
-                                 "c"
-                                 "e"
-                                 "d");
+  CHECK_TRACE_CONTENTS("tangle", "a\n"
+                                 "c\n"
+                                 "b\n"
+                                 "c\n"
+                                 "e\n"
+                                 "d\n");
 }
 
 // todo: include line numbers in tangle errors
 
-//// scenarios
-
-void test_tangle_supports_scenarios() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "+layer1: pqr\n"
-                   "+layer2: xyz");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: xyz\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_ignores_empty_lines_in_scenarios() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "+layer1: pqr\n"
-                   "  \n"
-                   "+layer2: xyz");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: xyz\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_handles_empty_lines_in_scenarios() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "\n"
-                   "+layer1: pqr\n"
-                   "+layer2: xyz");
-  list<Line> lines;
-  tangle(in, lines);
-  // no infinite loop
-}
-
-void test_tangle_supports_configurable_toplevel() {
-  istringstream in(":(scenarios foo)\n"
-                   ":(scenario does_bar)\n"
-                   "abc def\n"
-                   "+layer1: pqr");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  foo(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-
-  istringstream cleanup(":(scenarios run)\n");
-  tangle(cleanup, lines);
-}
-
-void test_tangle_can_hide_warnings_in_scenarios() {
-  istringstream in(":(scenario does_bar)\n"
-                   "% Hide_warnings = true;\n"
-                   "abc def\n"
-                   "+layer1: pqr\n"
-                   "+layer2: xyz");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  Hide_warnings = true;");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: xyz\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_can_include_c_code_at_end_of_scenario() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "+layer1: pqr\n"
-                   "+layer2: xyz\n"
-                   "% int x = 1;");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: xyz\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  int x = 1;");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_can_include_c_code_at_end_of_scenario_without_trace_expectations() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "% int x = 1;");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  int x = 1;");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_supports_strings_in_scenarios() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc \"def\"\n"
-                   "+layer1: pqr\n"
-                   "+layer2: \"xyz\"");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc \\\"def\\\"\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: \\\"xyz\\\"\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_supports_strings_in_scenarios2() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc \"\"\n"
-                   "+layer1: pqr\n"
-                   "+layer2: \"\"");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc \\\"\\\"\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: \\\"\\\"\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_supports_multiline_input_in_scenarios() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "  efg\n"
-                   "+layer1: pqr\n"
-                   "+layer2: \"\"");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"  efg\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: \\\"\\\"\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_supports_reset_in_scenarios() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "===\n"
-                   "efg\n"
-                   "+layer1: pqr\n"
-                   "+layer2: \"\"");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CLEAR_TRACE;");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"efg\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: \\\"\\\"\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_can_check_for_absence_at_end_of_scenarios() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "  efg\n"
-                   "+layer1: pqr\n"
-                   "-layer1: xyz");
-  list<Line> lines;
-  tangle(in, lines);
-//?   for (list<Line>::iterator p = lines.begin();  p != lines.end();  ++p)
-//?     cerr << p->contents << '\n';
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"  efg\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_DOESNT_CONTAIN(\"layer1: xyz\");");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_can_check_for_absence_at_end_of_scenarios2() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "  efg\n"
-                   "-layer1: pqr\n"
-                   "-layer1: xyz");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"  efg\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_DOESNT_CONTAIN(\"layer1: pqr\");");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_DOESNT_CONTAIN(\"layer1: xyz\");");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_can_check_for_count_in_scenario() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "  efg\n"
-                   "$layer1: 2");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"  efg\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_COUNT(\"layer1\", 2);");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_can_handle_mu_comments_in_scenario() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "# comment1\n"
-                   "  efg\n"
-                   "  # indented comment 2\n"
-                   "+layer1: pqr\n"
-                   "# comment inside expected_trace\n"
-                   "+layer1: xyz\n"
-                   "# comment after expected trace\n"
-                   "-layer1: z\n"
-                   "# comment before trace count\n"
-                   "$layer1: 2\n"
-                   "# comment at end\n"
-                   "\n");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"# comment1\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"  efg\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"  # indented comment 2\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: xyz\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_DOESNT_CONTAIN(\"layer1: z\");");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_COUNT(\"layer1\", 2);");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
-void test_tangle_can_interleave_present_and_absent_lines_to_kludgily_avoid_specifying_order() {
-  istringstream in(":(scenario does_bar)\n"
-                   "abc def\n"
-                   "+layer1: pqr\n"
-                   "-absent\n"
-                   "+layer2: xyz");
-  list<Line> lines;
-  tangle(in, lines);
-  CHECK_EQ(lines.front().contents, "void test_does_bar() {");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  run(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"abc def\\n\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer1: pqr\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_DOESNT_CONTAIN(\"absent\");");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  CHECK_TRACE_CONTENTS(");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "      \"layer2: xyz\"");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "  );");  lines.pop_front();
-  CHECK_EQ(lines.front().contents, "}");  lines.pop_front();
-  CHECK(lines.empty());
-}
-
 //// helpers
 
 void test_trim() {
diff --git a/transect/000organization.cc b/transect/000organization.cc
deleted file mode 100644
index 9a1938ff..00000000
--- a/transect/000organization.cc
+++ /dev/null
@@ -1,136 +0,0 @@
-//: You guessed right: the '000' prefix means you should start reading here.
-//:
-//: This project is set up to load all files with a numeric prefix. Just
-//: create a new file and start hacking.
-//:
-//: The first few files (00*) are independent of what this program does, an
-//: experimental skeleton that will hopefully make it both easier for others to
-//: understand and more malleable, easier to rewrite and remould into radically
-//: different shapes without breaking in subtle corner cases. The premise is
-//: that understandability and rewrite-friendliness are related in a virtuous
-//: cycle. Doing one well makes it easier to do the other.
-//:
-//: Lower down, this file contains a legal, bare-bones C++ program. It doesn't
-//: do anything yet; subsequent files will contain :(...) directives to insert
-//: lines into it. For example:
-//:   :(after "more events")
-//: This directive means: insert the following lines after a line in the
-//: program containing the words "more events".
-//:
-//: A simple tool is included to 'tangle' all the files together in sequence
-//: according to their directives into a single source file containing all the
-//: code for the project, and then feed the source file to the compiler.
-//: (It'll drop these comments starting with a '//:' prefix that only make
-//: sense before tangling.)
-//:
-//: Directives free up the programmer to order code for others to read rather
-//: than as forced by the computer or compiler. Each individual feature can be
-//: organized in a self-contained 'layer' that adds code to many different data
-//: structures and functions all over the program. The right decomposition into
-//: layers will let each layer make sense in isolation.
-//:
-//:   "If I look at any small part of it, I can see what is going on -- I don't
-//:   need to refer to other parts to understand what something is doing.
-//:
-//:   If I look at any large part in overview, I can see what is going on -- I
-//:   don't need to know all the details to get it.
-//:
-//:   Every level of detail is as locally coherent and as well thought-out as
-//:   any other level."
-//:
-//:       -- Richard Gabriel, "The Quality Without A Name"
-//:          (http://dreamsongs.com/Files/PatternsOfSoftware.pdf, page 42)
-//:
-//: Directives are powerful; they permit inserting or modifying any point in
-//: the program. Using them tastefully requires mapping out specific lines as
-//: waypoints for future layers to hook into. Often such waypoints will be in
-//: comments, capitalized to hint that other layers rely on their presence.
-//:
-//: A single waypoint might have many different code fragments hooking into
-//: it from all over the codebase. Use 'before' directives to insert
-//: code at a location in order, top to bottom, and 'after' directives to
-//: insert code in reverse order. By convention waypoints intended for insertion
-//: before begin with 'End'. Notice below how the layers line up above the "End
-//: Foo" waypoint.
-//:
-//:   File 001          File 002                File 003
-//:   ============      ===================     ===================
-//:   // Foo
-//:   ------------
-//:              <----  :(before "End Foo")
-//:                     ....
-//:                     ...
-//:   ------------
-//:              <----------------------------  :(before "End Foo")
-//:                                             ....
-//:                                             ...
-//:   // End Foo
-//:   ============
-//:
-//: Here's part of a layer in color: http://i.imgur.com/0eONnyX.png. Directives
-//: are shaded dark.
-//:
-//: Layers do more than just shuffle code around. In a well-organized codebase
-//: it should be possible to stop loading after any file/layer, build and run
-//: the program, and pass all tests for loaded features. (Relevant is
-//: http://youtube.com/watch?v=c8N72t7aScY, a scene from "2001: A Space
-//: Odyssey".) Get into the habit of running the included script called
-//: 'test_layers' before you commit any changes.
-//:
-//: This 'subsetting guarantee' ensures that this directory contains a
-//: cleaned-up narrative of the evolution of this codebase. Organizing
-//: autobiographically allows newcomers to rapidly orient themselves, reading
-//: the first few files to understand a simple gestalt of a program's core
-//: purpose and features, and later gradually working their way through other
-//: features as the need arises.
-//:
-//: Programmers shouldn't need to understand everything about a program to
-//: hack on it. But they shouldn't be prevented from a thorough understanding
-//: of each aspect either. The goal of layers is to reward curiosity.
-
-// Includes
-// End Includes
-
-// Types
-// End Types
-
-// Function prototypes are auto-generated in the 'build*' scripts; define your
-// functions in any order. Just be sure to declare each function header all on
-// one line, ending with the '{'. Our auto-generation scripts are too minimal
-// and simple-minded to handle anything else.
-#include "function_list"  // by convention, files ending with '_list' are auto-generated
-
-// Globals
-//
-// All statements in this section should always define a single variable on a
-// single line. The 'build*' scripts will simple-mindedly auto-generate extern
-// declarations for them. Remember to define (not just declare) constants with
-// extern linkage in this section, since C++ global constants have internal
-// linkage by default.
-//
-// End Globals
-
-int main(int argc, char* argv[]) {
-  atexit(reset);
-
-  // End One-time Setup
-
-  // Commandline Parsing
-  // End Commandline Parsing
-
-  return 0;  // End Main
-}
-
-// Unit Tests
-// End Unit Tests
-
-//: our first directive; insert the following header at the start of the program
-:(before "End Includes")
-#include <stdlib.h>
-
-//: Without directives or with the :(code) directive, lines get added at the
-//: end.
-:(code)
-void reset() {
-  // End Reset
-}
diff --git a/transect/001help.cc b/transect/001help.cc
deleted file mode 100644
index 3cab06d9..00000000
--- a/transect/001help.cc
+++ /dev/null
@@ -1,261 +0,0 @@
-//: Everything this project/binary supports.
-//: This should give you a sense for what to look forward to in later layers.
-
-:(before "End Commandline Parsing")
-if (argc <= 1 || is_equal(argv[1], "--help")) {
-  //: this is the functionality later layers will provide
-  // currently no automated tests for commandline arg parsing
-  if (argc <= 1) {
-    cerr << "Please provide a Mu program to run.\n"
-         << "\n";
-  }
-  cerr << "Usage:\n"
-       << "  mu [options] [test] [files]\n"
-       << "or:\n"
-       << "  mu [options] [test] [files] -- [ingredients for function/recipe 'main']\n"
-       << "Square brackets surround optional arguments.\n"
-       << "\n"
-       << "Examples:\n"
-       << "  To load files and run 'main':\n"
-       << "    mu file1.mu file2.mu ...\n"
-       << "  To run 'main' and dump a trace of all operations at the end:\n"
-       << "    mu --trace file1.mu file2.mu ...\n"
-       << "  To run all tests:\n"
-       << "    mu test\n"
-       << "  To load files and then run all tests:\n"
-       << "    mu test file1.mu file2.mu ...\n"
-       << "  To run a single Mu scenario:\n"
-       << "    mu test file1.mu file2.mu ... scenario\n"
-       << "  To run a single Mu scenario and dump a trace at the end:\n"
-       << "    mu --trace test file1.mu file2.mu ... scenario\n"
-       << "  To load files and run only the tests in explicitly loaded files (for apps):\n"
-       << "    mu --test-only-app test file1.mu file2.mu ...\n"
-       << "  To load all files with a numeric prefix in a directory:\n"
-       << "    mu directory1 directory2 ...\n"
-       << "  You can test directories just like files.\n"
-       << "    mu test directory1 directory2 ...\n"
-       << "  To pass ingredients to a mu program, provide them after '--':\n"
-       << "    mu file_or_dir1 file_or_dir2 ... -- ingredient1 ingredient2 ...\n"
-       << "  To see where a mu program is spending its time:\n"
-       << "    mu --profile file_or_dir1 file_or_dir2 ...\n"
-       << "  this slices and dices time spent in various profile.* output files\n"
-       << "\n"
-       << "  To browse a trace generated by a previous run:\n"
-       << "    mu browse-trace file\n"
-       ;
-  return 0;
-}
-
-//: Support for option parsing.
-//: Options always begin with '--' and are always the first arguments. An
-//: option will never follow a non-option.
-:(before "End Commandline Parsing")
-char** arg = &argv[1];
-while (argc > 1 && starts_with(*arg, "--")) {
-  if (false)
-    ;  // no-op branch just so any further additions can consistently always start with 'else'
-  // End Commandline Options(*arg)
-  else
-    cerr << "skipping unknown option " << *arg << '\n';
-  --argc;  ++argv;  ++arg;
-}
-
-//:: Helper function used by the above fragment of code (and later layers too,
-//:: who knows?).
-//: The :(code) directive appends function definitions to the end of the
-//: project. Regardless of where functions are defined, we can call them
-//: anywhere we like as long as we format the function header in a specific
-//: way: put it all on a single line without indent, end the line with ') {'
-//: and no trailing whitespace. As long as functions uniformly start this
-//: way, our 'build*' scripts contain a little command to automatically
-//: generate declarations for them.
-:(code)
-bool is_equal(char* s, const char* lit) {
-  return strncmp(s, lit, strlen(lit)) == 0;
-}
-
-bool starts_with(const string& s, const string& pat) {
-  string::const_iterator a=s.begin(), b=pat.begin();
-  for (/*nada*/;  a!=s.end() && b!=pat.end();  ++a, ++b)
-    if (*a != *b) return false;
-  return b == pat.end();
-}
-
-//: I'll throw some style conventions here for want of a better place for them.
-//: As a rule I hate style guides. Do what you want, that's my motto. But since
-//: we're dealing with C/C++, the one big thing we want to avoid is undefined
-//: behavior. If a compiler ever encounters undefined behavior it can make
-//: your program do anything it wants.
-//:
-//: For reference, my checklist of undefined behaviors to watch out for:
-//:   out-of-bounds access
-//:   uninitialized variables
-//:   use after free
-//:   dereferencing invalid pointers: null, a new of size 0, others
-//:
-//:   casting a large number to a type too small to hold it
-//:
-//:   integer overflow
-//:   division by zero and other undefined expressions
-//:   left-shift by negative count
-//:   shifting values by more than or equal to the number of bits they contain
-//:   bitwise operations on signed numbers
-//:
-//:   Converting pointers to types of different alignment requirements
-//:     T* -> void* -> T*: defined
-//:     T* -> U* -> T*: defined if non-function pointers and alignment requirements are same
-//:     function pointers may be cast to other function pointers
-//:
-//:       Casting a numeric value into a value that can't be represented by the target type (either directly or via static_cast)
-//:
-//: To guard against these, some conventions:
-//:
-//: 0. Initialize all primitive variables in functions and constructors.
-//:
-//: 1. Minimize use of pointers and pointer arithmetic. Avoid 'new' and
-//: 'delete' as far as possible. Rely on STL to perform memory management to
-//: avoid use-after-free issues (and memory leaks).
-//:
-//: 2. Avoid naked arrays to avoid out-of-bounds access. Never use operator[]
-//: except with map. Use at() with STL vectors and so on.
-//:
-//: 3. Valgrind all the things.
-//:
-//: 4. Avoid unsigned numbers. Not strictly an undefined-behavior issue, but
-//: the extra range doesn't matter, and it's one less confusing category of
-//: interaction gotchas to worry about.
-//:
-//: Corollary: don't use the size() method on containers, since it returns an
-//: unsigned and that'll cause warnings about mixing signed and unsigned,
-//: yadda-yadda. Instead use this macro below to perform an unsafe cast to
-//: signed. We'll just give up immediately if a container's ever too large.
-//: Basically, Mu is not concerned about this being a little slower than it
-//: could be. (https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759de5a7)
-//:
-//: Addendum to corollary: We're going to uniformly use int everywhere, to
-//: indicate that we're oblivious to number size, and since Clang on 32-bit
-//: platforms doesn't yet support multiplication over 64-bit integers, and
-//: since multiplying two integers seems like a more common situation to end
-//: up in than integer overflow.
-:(before "End Includes")
-#define SIZE(X) (assert((X).size() < (1LL<<(sizeof(int)*8-2))), static_cast<int>((X).size()))
-
-//: 5. Integer overflow is guarded against at runtime using the -ftrapv flag
-//: to the compiler, supported by Clang (GCC version only works sometimes:
-//: http://stackoverflow.com/questions/20851061/how-to-make-gcc-ftrapv-work).
-:(before "atexit(reset)")
-initialize_signal_handlers();  // not always necessary, but doesn't hurt
-//? cerr << INT_MAX+1 << '\n';  // test overflow
-//? assert(false);  // test SIGABRT
-:(code)
-// based on https://spin.atomicobject.com/2013/01/13/exceptions-stack-traces-c
-void initialize_signal_handlers() {
-  struct sigaction action;
-  bzero(&action, sizeof(action));
-  action.sa_sigaction = dump_and_exit;
-  sigemptyset(&action.sa_mask);
-  sigaction(SIGABRT, &action, NULL);  // assert() failure or integer overflow on linux (with -ftrapv)
-  sigaction(SIGILL,  &action, NULL);  // integer overflow on OS X (with -ftrapv)
-}
-void dump_and_exit(int sig, siginfo_t* /*unused*/, void* /*unused*/) {
-  switch (sig) {
-    case SIGABRT:
-      #ifndef __APPLE__
-        cerr << "SIGABRT: might be an integer overflow if it wasn't an assert() failure or exception\n";
-        _Exit(1);
-      #endif
-      break;
-    case SIGILL:
-      #ifdef __APPLE__
-        cerr << "SIGILL: most likely caused by integer overflow\n";
-        _Exit(1);
-      #endif
-      break;
-    default:
-      break;
-  }
-}
-:(before "End Includes")
-#include <signal.h>
-
-//: For good measure we'll also enable SIGFPE.
-:(before "atexit(reset)")
-feenableexcept(FE_OVERFLOW | FE_UNDERFLOW);
-//? assert(sizeof(int) == 4 && sizeof(float) == 4);
-//? //                          | exp   |  mantissa
-//? int smallest_subnormal = 0b00000000000000000000000000000001;
-//? float smallest_subnormal_f = *reinterpret_cast<float*>(&smallest_subnormal);
-//? cerr << "ε: " << smallest_subnormal_f << '\n';
-//? cerr << "ε/2: " << smallest_subnormal_f/2 << " (underflow)\n";  // test SIGFPE
-:(before "End Includes")
-#include <fenv.h>
-:(code)
-#ifdef __APPLE__
-// Public domain polyfill for feenableexcept on OS X
-// http://www-personal.umich.edu/~williams/archive/computation/fe-handling-example.c
-int feenableexcept(unsigned int excepts) {
-  static fenv_t fenv;
-  unsigned int new_excepts = excepts & FE_ALL_EXCEPT;
-  unsigned int old_excepts;
-  if (fegetenv(&fenv)) return -1;
-  old_excepts = fenv.__control & FE_ALL_EXCEPT;
-  fenv.__control &= ~new_excepts;
-  fenv.__mxcsr &= ~(new_excepts << 7);
-  return fesetenv(&fenv) ? -1 : old_excepts;
-}
-#endif
-
-//: 6. Map's operator[] being non-const is fucking evil.
-:(before "Globals")  // can't generate prototypes for these
-// from http://stackoverflow.com/questions/152643/idiomatic-c-for-reading-from-a-const-map
-template<typename T> typename T::mapped_type& get(T& map, typename T::key_type const& key) {
-  typename T::iterator iter(map.find(key));
-  assert(iter != map.end());
-  return iter->second;
-}
-template<typename T> typename T::mapped_type const& get(const T& map, typename T::key_type const& key) {
-  typename T::const_iterator iter(map.find(key));
-  assert(iter != map.end());
-  return iter->second;
-}
-template<typename T> typename T::mapped_type const& put(T& map, typename T::key_type const& key, typename T::mapped_type const& value) {
-  // map[key] requires mapped_type to have a zero-arg (default) constructor
-  map.insert(std::make_pair(key, value)).first->second = value;
-  return value;
-}
-template<typename T> bool contains_key(T& map, typename T::key_type const& key) {
-  return map.find(key) != map.end();
-}
-template<typename T> typename T::mapped_type& get_or_insert(T& map, typename T::key_type const& key) {
-  return map[key];
-}
-//: The contract: any container that relies on get_or_insert should never call
-//: contains_key.
-
-//: 7. istreams are a royal pain in the arse. You have to be careful about
-//: what subclass you try to putback into. You have to watch out for the pesky
-//: failbit and badbit. Just avoid eof() and use this helper instead.
-:(code)
-bool has_data(istream& in) {
-  return in && !in.eof();
-}
-
-:(before "End Includes")
-#include <assert.h>
-
-#include <iostream>
-using std::istream;
-using std::ostream;
-using std::iostream;
-using std::cin;
-using std::cout;
-using std::cerr;
-#include <iomanip>
-
-#include <string.h>
-#include <string>
-using std::string;
-
-#include <algorithm>
-using std::min;
-using std::max;
diff --git a/transect/002test.cc b/transect/002test.cc
deleted file mode 100644
index 817b0d47..00000000
--- a/transect/002test.cc
+++ /dev/null
@@ -1,104 +0,0 @@
-//: A simple test harness. To create new tests, define functions starting with
-//: 'test_'. To run all tests so defined, run:
-//:   $ ./mu test
-//:
-//: Every layer should include tests, and can reach into previous layers.
-//: However, it seems like a good idea never to reach into tests from previous
-//: layers. Every test should be a contract that always passes as originally
-//: written, regardless of any later layers. Avoid writing 'temporary' tests
-//: that are only meant to work until some layer.
-
-:(before "End Types")
-typedef void (*test_fn)(void);
-:(before "Globals")
-// move a global ahead into types that we can't generate an extern declaration for
-const test_fn Tests[] = {
-  #include "test_list"  // auto-generated; see 'build*' scripts
-};
-
-:(before "End Globals")
-bool Run_tests = false;
-bool Passed = true;  // set this to false inside any test to indicate failure
-
-:(before "End Includes")
-#define CHECK(X) \
-  if (Passed && !(X)) { \
-    cerr << "\nF - " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): " << #X << '\n'; \
-    Passed = false; \
-    return;  /* Currently we stop at the very first failure. */ \
-  }
-
-#define CHECK_EQ(X, Y) \
-  if (Passed && (X) != (Y)) { \
-    cerr << "\nF - " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): " << #X << " == " << #Y << '\n'; \
-    cerr << "  got " << (X) << '\n';  /* BEWARE: multiple eval */ \
-    Passed = false; \
-    return;  /* Currently we stop at the very first failure. */ \
-  }
-
-:(before "End Reset")
-Passed = true;
-
-:(before "End Commandline Parsing")
-if (argc > 1 && is_equal(argv[1], "test")) {
-  Run_tests = true;  --argc;  ++argv;  // shift 'test' out of commandline args
-}
-
-:(before "End Main")
-if (Run_tests) {
-  // Test Runs
-  // we run some tests and then exit; assume no state need be maintained afterward
-
-  long num_failures = 0;
-  // End Test Run Initialization
-  time_t t;  time(&t);
-  cerr << "C tests: " << ctime(&t);
-  for (size_t i=0;  i < sizeof(Tests)/sizeof(Tests[0]);  ++i) {
-//?     cerr << "running " << Test_names[i] << '\n';
-    run_test(i);
-    if (Passed) cerr << '.';
-    else ++num_failures;
-  }
-  cerr << '\n';
-  // End Tests
-  if (num_failures > 0) {
-    cerr << num_failures << " failure"
-         << (num_failures > 1 ? "s" : "")
-         << '\n';
-    return 1;
-  }
-  return 0;
-}
-
-:(code)
-void run_test(size_t i) {
-  if (i >= sizeof(Tests)/sizeof(Tests[0])) {
-    cerr << "no test " << i << '\n';
-    return;
-  }
-  reset();
-  // End Test Setup
-  (*Tests[i])();
-  // End Test Teardown
-}
-
-//: Convenience: run a single test
-:(before "Globals")
-// Names for each element of the 'Tests' global, respectively.
-const string Test_names[] = {
-  #include "test_name_list"  // auto-generated; see 'build*' scripts
-};
-:(after "Test Runs")
-string maybe_single_test_to_run = argv[argc-1];
-if (!starts_with(maybe_single_test_to_run, "test_"))
-  maybe_single_test_to_run.insert(0, "test_");
-for (size_t i=0;  i < sizeof(Tests)/sizeof(Tests[0]);  ++i) {
-  if (Test_names[i] == maybe_single_test_to_run) {
-    run_test(i);
-    if (Passed) cerr << ".\n";
-    return 0;
-  }
-}
-
-:(before "End Includes")
-#include <stdlib.h>
diff --git a/transect/003trace.cc b/transect/003trace.cc
deleted file mode 100644
index 3b4ae596..00000000
--- a/transect/003trace.cc
+++ /dev/null
@@ -1,408 +0,0 @@
-//: The goal of layers is to make programs more easy to understand and more
-//: malleable, easy to rewrite in radical ways without accidentally breaking
-//: some corner case. Tests further both goals. They help understandability by
-//: letting one make small changes and get feedback. What if I wrote this line
-//: like so? What if I removed this function call, is it really necessary?
-//: Just try it, see if the tests pass. Want to explore rewriting this bit in
-//: this way? Tests put many refactorings on a firmer footing.
-//:
-//: But the usual way we write tests seems incomplete. Refactorings tend to
-//: work in the small, but don't help with changes to function boundaries. If
-//: you want to extract a new function you have to manually test-drive it to
-//: create tests for it. If you want to inline a function its tests are no
-//: longer valid. In both cases you end up having to reorganize code as well as
-//: tests, an error-prone activity.
-//:
-//: In response, this layer introduces the notion of *domain-driven* testing.
-//: We focus on the domain of inputs the whole program needs to handle rather
-//: than the correctness of individual functions. All 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;
-//:
-//: Tests 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
-//:
-//: Since we never call anything but the run() function directly, we never have
-//: 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 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 tests, trace facts about your domain rather than details of
-//: how you computed them.
-//:
-//: More details: http://akkartik.name/blog/tracing-tests
-//:
-//: ---
-//:
-//: Between layers and domain-driven testing, programming starts to look like a
-//: fundamentally different activity. Instead of a) superficial, b) local rules
-//: on c) code [like say http://blog.bbv.ch/2013/06/05/clean-code-cheat-sheet],
-//: 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 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
-//: map of the environment the program must operate under. Whether a program 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
-//: colocated with the code that makes them work.
-//:
-//:   "Programming properly should be regarded as an activity by which
-//:   programmers form a mental model, rather than as production of a program."
-//:   -- Peter Naur (http://alistair.cockburn.us/ASD+book+extract%3A+%22Naur,+Ehn,+Musashi%22)
-
-:(before "End Types")
-struct trace_line {
-  int depth;  // optional field just to help browse traces later
-  string label;
-  string contents;
-  trace_line(string l, string c) :depth(0), label(l), contents(c) {}
-  trace_line(int d, string l, string c) :depth(d), label(l), contents(c) {}
-};
-
-//: Support for tracing an entire run.
-//: Traces can have a lot of overhead, so only turn them on when asked.
-:(before "End Commandline Options(*arg)")
-else if (is_equal(*arg, "--trace")) {
-  Save_trace = true;
-}
-:(before "End Commandline Parsing")
-if (Save_trace) {
-  cerr << "initializing trace\n";
-  Trace_stream = new trace_stream;
-}
-:(code)
-void cleanup_main() {
-  if (!Trace_stream) return;
-  if (Save_trace)
-    Trace_stream->save();
-  delete Trace_stream;
-  Trace_stream = NULL;
-}
-:(before "End One-time Setup")
-atexit(cleanup_main);
-
-:(before "End Types")
-// Pre-define some global constants that trace_stream needs to know about.
-// Since they're in the Types section, they'll be included in any cleaved
-// compilation units. So no extern linkage.
-const int Max_depth = 9999;
-const int Error_depth = 0;  // definitely always print errors
-
-struct trace_stream {
-  vector<trace_line> past_lines;
-  // accumulator for current line
-  ostringstream* curr_stream;
-  string curr_label;
-  int curr_depth;
-  int collect_depth;
-  ofstream null_stream;  // never opens a file, so writes silently fail
-  trace_stream() :curr_stream(NULL), curr_depth(Max_depth), collect_depth(Max_depth) {}
-  ~trace_stream() { if (curr_stream) delete curr_stream; }
-
-  ostream& stream(string label) {
-    return stream(Max_depth, label);
-  }
-
-  ostream& stream(int depth, string label) {
-    if (depth > collect_depth) return null_stream;
-    curr_stream = new ostringstream;
-    curr_label = label;
-    curr_depth = depth;
-    (*curr_stream) << std::hex;
-    return *curr_stream;
-  }
-
-  void save() {
-    cerr << "saving trace to 'last_run'\n";
-    ofstream fout("last_run");
-    fout << readable_contents("");
-    fout.close();
-  }
-
-  // be sure to call this before messing with curr_stream or curr_label
-  void newline();
-  // useful for debugging
-  string readable_contents(string label);  // empty label = show everything
-};
-
-:(code)
-void trace_stream::newline() {
-  if (!curr_stream) return;
-  string curr_contents = curr_stream->str();
-  if (!curr_contents.empty()) {
-    past_lines.push_back(trace_line(curr_depth, trim(curr_label), curr_contents));  // preserve indent in contents
-    if ((!Hide_errors && curr_depth == Error_depth)
-        || Dump_trace
-        || (!Dump_label.empty() && curr_label == Dump_label))
-      cerr << curr_label << ": " << curr_contents << '\n';
-  }
-  delete curr_stream;
-  curr_stream = NULL;
-  curr_label.clear();
-  curr_depth = Max_depth;
-}
-
-string trace_stream::readable_contents(string label) {
-  ostringstream output;
-  label = trim(label);
-  for (vector<trace_line>::iterator p = past_lines.begin();  p != past_lines.end();  ++p)
-    if (label.empty() || label == p->label)
-      output << std::setw(4) << p->depth << ' ' << p->label << ": " << p->contents << '\n';
-  return output.str();
-}
-
-:(before "End Globals")
-trace_stream* Trace_stream = NULL;
-int Trace_errors = 0;  // used only when Trace_stream is NULL
-
-:(before "End Globals")
-bool Hide_errors = false;  // if set, don't print even error trace lines to screen
-bool Dump_trace = false;  // if set, print trace lines to screen
-string Dump_label = "";  // if set, print trace lines matching a single label to screen
-:(before "End Reset")
-Hide_errors = false;
-Dump_trace = false;  // toggle this to print traces to screen as they are emitted
-Dump_label = "";
-
-:(before "End Includes")
-#define CLEAR_TRACE  delete Trace_stream, Trace_stream = new trace_stream;
-
-// Top-level helper. IMPORTANT: can't nest
-#define trace(...)  !Trace_stream ? cerr /*print nothing*/ : Trace_stream->stream(__VA_ARGS__)
-
-// Just for debugging; 'git log' should never show any calls to 'dbg'.
-#define dbg trace(0, "a")
-#define DUMP(label)  if (Trace_stream) cerr << Trace_stream->readable_contents(label);
-
-// Errors are a special layer.
-#define raise  (!Trace_stream ? (++Trace_errors,cerr) /*do print*/ : Trace_stream->stream(Error_depth, "error"))
-// If we aren't yet sure how to deal with some corner case, use assert_for_now
-// to indicate that it isn't an inviolable invariant.
-#define assert_for_now assert
-
-// Inside tests, fail any tests that displayed (unexpected) errors.
-// Expected errors in tests should always be hidden and silently checked for.
-:(before "End Test Teardown")
-if (Passed && !Hide_errors && trace_contains_errors()) {
-  Passed = false;
-}
-:(code)
-bool trace_contains_errors() {
-  return Trace_errors > 0 || trace_count("error") > 0;
-}
-
-:(before "End Types")
-struct end {};
-:(code)
-ostream& operator<<(ostream& os, end /*unused*/) {
-  if (Trace_stream) Trace_stream->newline();
-  return os;
-}
-
-:(before "End Globals")
-bool Save_trace = false;  // if set, write out trace to disk
-
-// Trace_stream is a resource, lease_tracer uses RAII to manage it.
-:(before "End Types")
-struct lease_tracer {
-  lease_tracer();
-  ~lease_tracer();
-};
-:(code)
-lease_tracer::lease_tracer() { Trace_stream = new trace_stream; }
-lease_tracer::~lease_tracer() {
-  if (Save_trace) Trace_stream->save();
-  delete Trace_stream, Trace_stream = NULL;
-}
-:(before "End Includes")
-#define START_TRACING_UNTIL_END_OF_SCOPE  lease_tracer leased_tracer;
-:(before "End Test Setup")
-START_TRACING_UNTIL_END_OF_SCOPE
-
-:(before "End Includes")
-#define CHECK_TRACE_CONTENTS(...)  check_trace_contents(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__)
-
-#define CHECK_TRACE_CONTAINS_ERRORS()  CHECK(trace_contains_errors())
-#define CHECK_TRACE_DOESNT_CONTAIN_ERRORS() \
-  if (Passed && trace_contains_errors()) { \
-    cerr << "\nF - " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): unexpected errors\n"; \
-    DUMP("error"); \
-    Passed = false; \
-    return; \
-  }
-
-#define CHECK_TRACE_COUNT(label, count) \
-  if (Passed && trace_count(label) != (count)) { \
-    cerr << "\nF - " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): trace_count of " << label << " should be " << count << '\n'; \
-    cerr << "  got " << trace_count(label) << '\n';  /* multiple eval */ \
-    DUMP(label); \
-    Passed = false; \
-    return;  /* Currently we stop at the very first failure. */ \
-  }
-
-#define CHECK_TRACE_DOESNT_CONTAIN(...)  CHECK(trace_doesnt_contain(__VA_ARGS__))
-
-:(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, "");
-  int curr_expected_line = 0;
-  while (curr_expected_line < SIZE(expected_lines) && expected_lines.at(curr_expected_line).empty())
-    ++curr_expected_line;
-  if (curr_expected_line == SIZE(expected_lines)) return true;
-  string label, contents;
-  split_label_contents(expected_lines.at(curr_expected_line), &label, &contents);
-  for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin();  p != Trace_stream->past_lines.end();  ++p) {
-    if (label != p->label) continue;
-    if (contents != trim(p->contents)) continue;
-    ++curr_expected_line;
-    while (curr_expected_line < SIZE(expected_lines) && expected_lines.at(curr_expected_line).empty())
-      ++curr_expected_line;
-    if (curr_expected_line == SIZE(expected_lines)) return true;
-    split_label_contents(expected_lines.at(curr_expected_line), &label, &contents);
-  }
-
-  if (line_exists_anywhere(label, contents)) {
-    cerr << "\nF - " << FUNCTION << "(" << FILE << ":" << LINE << "): line [" << label << ": " << contents << "] out of order in trace:\n";
-    DUMP("");
-  }
-  else {
-    cerr << "\nF - " << FUNCTION << "(" << FILE << ":" << LINE << "): missing [" << contents << "] in trace:\n";
-    DUMP(label);
-  }
-  Passed = false;
-  return false;
-}
-
-void split_label_contents(const string& s, string* label, string* contents) {
-  static const string delim(": ");
-  size_t pos = s.find(delim);
-  if (pos == string::npos) {
-    *label = "";
-    *contents = trim(s);
-  }
-  else {
-    *label = trim(s.substr(0, pos));
-    *contents = trim(s.substr(pos+SIZE(delim)));
-  }
-}
-
-bool line_exists_anywhere(const string& label, const string& contents) {
-  for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin();  p != Trace_stream->past_lines.end();  ++p) {
-    if (label != p->label) continue;
-    if (contents == trim(p->contents)) return true;
-  }
-  return false;
-}
-
-int trace_count(string label) {
-  return trace_count(label, "");
-}
-
-int trace_count(string label, string line) {
-  if (!Trace_stream) return 0;
-  long result = 0;
-  for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin();  p != Trace_stream->past_lines.end();  ++p) {
-    if (label == p->label) {
-      if (line == "" || trim(line) == trim(p->contents))
-        ++result;
-    }
-  }
-  return result;
-}
-
-int trace_count_prefix(string label, string prefix) {
-  if (!Trace_stream) return 0;
-  long result = 0;
-  for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin();  p != Trace_stream->past_lines.end();  ++p) {
-    if (label == p->label) {
-      if (starts_with(trim(p->contents), trim(prefix)))
-        ++result;
-    }
-  }
-  return result;
-}
-
-bool trace_doesnt_contain(string label, string line) {
-  return trace_count(label, line) == 0;
-}
-
-bool trace_doesnt_contain(string expected) {
-  vector<string> tmp = split_first(expected, ": ");
-  if (SIZE(tmp) == 1) {
-    raise << expected << ": missing label or contents in trace line\n" << end();
-    assert(false);
-  }
-  return trace_doesnt_contain(tmp.at(0), tmp.at(1));
-}
-
-vector<string> split(string s, string delim) {
-  vector<string> result;
-  size_t begin=0, end=s.find(delim);
-  while (true) {
-    if (end == string::npos) {
-      result.push_back(string(s, begin, string::npos));
-      break;
-    }
-    result.push_back(string(s, begin, end-begin));
-    begin = end+SIZE(delim);
-    end = s.find(delim, begin);
-  }
-  return result;
-}
-
-vector<string> split_first(string s, string delim) {
-  vector<string> result;
-  size_t end=s.find(delim);
-  result.push_back(string(s, 0, end));
-  if (end != string::npos)
-    result.push_back(string(s, end+SIZE(delim), string::npos));
-  return result;
-}
-
-string trim(const string& s) {
-  string::const_iterator first = s.begin();
-  while (first != s.end() && isspace(*first))
-    ++first;
-  if (first == s.end()) return "";
-
-  string::const_iterator last = --s.end();
-  while (last != s.begin() && isspace(*last))
-    --last;
-  ++last;
-  return string(first, last);
-}
-
-:(before "End Includes")
-#include <vector>
-using std::vector;
-#include <list>
-using std::list;
-#include <set>
-using std::set;
-
-#include <sstream>
-using std::istringstream;
-using std::ostringstream;
-
-#include <fstream>
-using std::ifstream;
-using std::ofstream;
diff --git a/transect/003trace.test.cc b/transect/003trace.test.cc
deleted file mode 100644
index 67b4c345..00000000
--- a/transect/003trace.test.cc
+++ /dev/null
@@ -1,124 +0,0 @@
-void test_trace_check_compares() {
-  trace("test layer") << "foo" << end();
-  CHECK_TRACE_CONTENTS("test layer: foo");
-}
-
-void test_trace_check_ignores_other_layers() {
-  trace("test layer 1") << "foo" << end();
-  trace("test layer 2") << "bar" << end();
-  CHECK_TRACE_CONTENTS("test layer 1: foo");
-  CHECK_TRACE_DOESNT_CONTAIN("test layer 2: foo");
-}
-
-void test_trace_check_ignores_leading_whitespace() {
-  trace("test layer 1") << " foo" << end();
-  CHECK_EQ(trace_count("test layer 1", /*too little whitespace*/"foo"), 1);
-  CHECK_EQ(trace_count("test layer 1", /*too much whitespace*/"  foo"), 1);
-}
-
-void test_trace_check_ignores_other_lines() {
-  trace("test layer 1") << "foo" << end();
-  trace("test layer 1") << "bar" << end();
-  CHECK_TRACE_CONTENTS("test layer 1: foo");
-}
-
-void test_trace_check_ignores_other_lines2() {
-  trace("test layer 1") << "foo" << end();
-  trace("test layer 1") << "bar" << end();
-  CHECK_TRACE_CONTENTS("test layer 1: bar");
-}
-
-void test_trace_ignores_trailing_whitespace() {
-  trace("test layer 1") << "foo\n" << end();
-  CHECK_TRACE_CONTENTS("test layer 1: foo");
-}
-
-void test_trace_ignores_trailing_whitespace2() {
-  trace("test layer 1") << "foo " << end();
-  CHECK_TRACE_CONTENTS("test layer 1: foo");
-}
-
-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");
-}
-
-void test_trace_supports_count() {
-  trace("test layer 1") << "foo" << end();
-  trace("test layer 1") << "foo" << end();
-  CHECK_EQ(trace_count("test layer 1", "foo"), 2);
-}
-
-void test_trace_supports_count2() {
-  trace("test layer 1") << "foo" << end();
-  trace("test layer 1") << "bar" << end();
-  CHECK_EQ(trace_count("test layer 1"), 2);
-}
-
-void test_trace_count_ignores_trailing_whitespace() {
-  trace("test layer 1") << "foo\n" << end();
-  CHECK_EQ(trace_count("test layer 1", "foo"), 1);
-}
-
-// pending: DUMP tests
-// pending: readable_contents() adds newline if necessary.
-// pending: raise also prints to stderr.
-// pending: raise doesn't print to stderr if Hide_errors is set.
-// pending: raise doesn't have to be saved if Hide_errors is set, just printed.
-// pending: raise prints to stderr if Trace_stream is NULL.
-// pending: raise prints to stderr if Trace_stream is NULL even if Hide_errors is set.
-
-// can't check trace because trace methods call 'split'
-
-void test_split_returns_at_least_one_elem() {
-  vector<string> result = split("", ",");
-  CHECK_EQ(result.size(), 1);
-  CHECK_EQ(result.at(0), "");
-}
-
-void test_split_returns_entire_input_when_no_delim() {
-  vector<string> result = split("abc", ",");
-  CHECK_EQ(result.size(), 1);
-  CHECK_EQ(result.at(0), "abc");
-}
-
-void test_split_works() {
-  vector<string> result = split("abc,def", ",");
-  CHECK_EQ(result.size(), 2);
-  CHECK_EQ(result.at(0), "abc");
-  CHECK_EQ(result.at(1), "def");
-}
-
-void test_split_works2() {
-  vector<string> result = split("abc,def,ghi", ",");
-  CHECK_EQ(result.size(), 3);
-  CHECK_EQ(result.at(0), "abc");
-  CHECK_EQ(result.at(1), "def");
-  CHECK_EQ(result.at(2), "ghi");
-}
-
-void test_split_handles_multichar_delim() {
-  vector<string> result = split("abc,,def,,ghi", ",,");
-  CHECK_EQ(result.size(), 3);
-  CHECK_EQ(result.at(0), "abc");
-  CHECK_EQ(result.at(1), "def");
-  CHECK_EQ(result.at(2), "ghi");
-}
-
-void test_trim() {
-  CHECK_EQ(trim(""), "");
-  CHECK_EQ(trim(" "), "");
-  CHECK_EQ(trim("  "), "");
-  CHECK_EQ(trim("a"), "a");
-  CHECK_EQ(trim(" a"), "a");
-  CHECK_EQ(trim("  a"), "a");
-  CHECK_EQ(trim("  ab"), "ab");
-  CHECK_EQ(trim("a "), "a");
-  CHECK_EQ(trim("a  "), "a");
-  CHECK_EQ(trim("ab  "), "ab");
-  CHECK_EQ(trim(" a "), "a");
-  CHECK_EQ(trim("  a  "), "a");
-  CHECK_EQ(trim("  ab  "), "ab");
-}
diff --git a/transect/010vm.cc b/transect/010vm.cc
deleted file mode 100644
index fad1ee77..00000000
--- a/transect/010vm.cc
+++ /dev/null
@@ -1,230 +0,0 @@
-//: type definitions start with either 'record' or 'choice'
-
-:(before "End Types")
-typedef int type_id;
-:(before "End Globals")
-map</*name*/string, type_id> Type_id;
-map<type_id, type_info> Type_info;
-type_id Next_type_id = 1;
-// primitive types
-type_id Literal_type_id = 0, Int_type_id = 0, Byte_type_id = 0, Address_type_id = 0, Array_type_id = 0, Ref_type_id = 0;
-:(before "End Types")
-struct type_info {
-  type_id id;
-  string name;
-  kind_of_type kind;
-  int size;  // in bytes
-  vector<type_declaration> elements;
-  type_info() :kind(PRIMITIVE), size(0) {}
-};
-:(before "struct type_info")
-enum kind_of_type {
-  PRIMITIVE,
-  RECORD,
-  CHOICE,
-};
-struct type_declaration {
-  string name;
-  vector<type_id> type;
-};
-
-//: global definitions start with 'var'
-
-:(before "End Types")
-typedef int global_id;
-:(before "End Globals")
-map</*name*/string, global_id> Global_id;
-map<global_id, global_info> Global_info;
-global_id Next_global_id = 1;
-:(before "End Types")
-struct global_info {
-  global_id id;
-  vector<type_id> type;
-  int address;
-  global_info() :address(0) {}
-};
-
-//: function definitions start with 'fn'
-
-:(before "End Types")
-typedef int function_id;
-:(before "End Globals")
-map</*name*/string, function_id> Function_id;
-map<function_id, function_info> Function_info;
-function_id Next_function_id = 1;
-:(before "End Types")
-struct function_info {
-  function_id id;
-  string name;
-  vector<operand> in;
-  vector<operand> in_out;
-  vector<instruction> instructions;
-  map</*local variable name*/string, int> stack_offset;
-  function_info() :id(0) {}
-};
-:(before "struct function_info")
-// operands have form name/property1/property2/... : (type1 type2 ...)
-struct operand {
-  string name;
-  vector<type_id> type;
-  vector<string> properties;
-  operand(string);
-  void set_type(istream&);
-};
-
-struct instruction {
-  function_id id;
-  string name;
-  vector<operand> in;
-  vector<operand> in_out;
-};
-:(code)
-operand::operand(string s) {
-  istringstream in(s);
-  name = slurp_until(in, '/');
-  while (has_data(in))
-    properties.push_back(slurp_until(in, '/'));
-}
-
-// extremely hacky; assumes a single-level list of words in parens, with no nesting
-void operand::set_type(istream& in) {
-  assert(has_data(in));
-  string curr;
-  in >> curr;
-//?   cerr << "2: " << curr << '\n';
-  if (curr.at(0) != '(') {
-    type.push_back(get(Type_id, curr));
-    return;
-  }
-  curr = curr.substr(/*skip '('*/1);
-  while (!ends_with(curr, ")")) {
-    if (curr.empty()) continue;
-    assert(curr.at(0) != '(');
-    type.push_back(get(Type_id, curr));
-    // update
-    assert(has_data(in));
-    in >> curr;
-  }
-  assert(ends_with(curr, ")"));
-  curr = curr.substr(0, SIZE(curr)-1);
-  if (!curr.empty()) {
-    /*'(' or ')' isn't a token by itself*/
-    type.push_back(get(Type_id, curr));
-  }
-}
-
-string to_string(const operand& o) {
-  ostringstream out;
-  out << o.name;
-  if (o.type.empty()) return out.str();
-  out << " : ";
-  if (SIZE(o.type) == 1) {
-    out << get(Type_info, o.type.at(0)).name;
-    return out.str();
-  }
-  out << "(";
-  for (int i = 0;  i < SIZE(o.type);  ++i) {
-    if (i > 0) out << ", ";
-    out << get(Type_info, o.type.at(i)).name;
-  }
-  out << ")";
-  return out.str();
-}
-
-string slurp_until(istream& in, char delim) {
-  ostringstream out;
-  char c;
-  while (in >> c) {
-    if (c == delim) {
-      // drop the delim
-      break;
-    }
-    out << c;
-  }
-  return out.str();
-}
-
-bool ends_with(const string& s, const string& pat) {
-  for (string::const_reverse_iterator p = s.rbegin(), q = pat.rbegin();  q != pat.rend();  ++p, ++q) {
-    if (p == s.rend()) return false;  // pat too long
-    if (*p != *q) return false;
-  }
-  return true;
-}
-
-:(before "End One-time Setup")
-init_primitive_types();
-:(code)
-void init_primitive_types() {
-  Literal_type_id = new_type("literal", PRIMITIVE, 0);
-  Int_type_id = new_type("int", PRIMITIVE, 4);
-  Byte_type_id = new_type("byte", PRIMITIVE, 1);
-  Address_type_id = new_type("address", PRIMITIVE, 4);
-  Array_type_id = new_type("array", PRIMITIVE, 0);  // size will depend on length
-  Ref_type_id = new_type("ref", PRIMITIVE, 8);  // address + alloc id
-}
-
-type_id new_type(string name, kind_of_type kind, int size) {
-  assert(!contains_key(Type_id, name));
-  int result = Next_type_id++;
-  put(Type_id, name, result);
-  assert(!contains_key(Type_info, result));
-  type_info& curr = Type_info[result];  // insert
-  curr.id = result;
-  curr.name = name;
-  curr.kind = kind;
-  curr.size = size;
-  return result;
-}
-
-//: Start each test by undoing the previous test's types, globals and functions
-
-:(before "End One-time Setup")
-save_snapshots();
-:(before "End Reset")
-restore_snapshots();
-
-:(before "End Globals")
-map<string, type_id> Type_id_snapshot;
-map<type_id, type_info> Type_info_snapshot;
-type_id Next_type_id_snapshot = 0;
-
-map<string, global_id> Global_id_snapshot;
-map<global_id, global_info> Global_info_snapshot;
-global_id Next_global_id_snapshot = 0;
-
-map<string, function_id> Function_id_snapshot;
-map<function_id, function_info> Function_info_snapshot;
-function_id Next_function_id_snapshot = 0;
-:(code)
-void save_snapshots() {
-  Type_id_snapshot = Type_id;
-  Type_info_snapshot = Type_info;
-  Next_type_id_snapshot = Next_type_id;
-
-  Global_id_snapshot = Global_id;
-  Global_info_snapshot = Global_info;
-  Next_global_id_snapshot = Next_global_id;
-
-  Function_id_snapshot = Function_id;
-  Function_info_snapshot = Function_info;
-  Next_function_id_snapshot = Next_function_id;
-}
-
-void restore_snapshots() {
-  Type_id = Type_id_snapshot;
-  Type_info = Type_info_snapshot;
-  Next_type_id = Next_type_id_snapshot;
-
-  Global_id = Global_id_snapshot;
-  Global_info = Global_info_snapshot;
-  Next_global_id = Next_global_id_snapshot;
-
-  Function_id = Function_id_snapshot;
-  Function_info = Function_info_snapshot;
-  Next_function_id = Next_function_id_snapshot;
-}
-
-:(before "End Includes")
-#include <map>
-using std::map;
diff --git a/transect/011load.cc b/transect/011load.cc
deleted file mode 100644
index f8cf96e8..00000000
--- a/transect/011load.cc
+++ /dev/null
@@ -1,228 +0,0 @@
-//: Phase 1 of translating Mu code: load it from a textual representation.
-//:
-//: The process of translating Mu code:
-//:   load -> check types -> convert
-
-:(scenarios load)  // use 'load' instead of 'run' in all scenarios in this layer
-:(scenario single_function)
-fn foo [
-  1 : int <- copy 23
-]
-+parse: function: foo
-+parse:   0 in operands
-+parse:   0 in_out operands
-+parse: instruction: copy
-+parse:   in => 23 : literal
-+parse:   in_out => 1 : int
-
-:(code)
-void load(string form) {
-  istringstream in(form);
-  load(in);
-}
-
-void load(istream& in) {
-  while (has_data(in)) {
-    string line_data;
-    getline(in, line_data);
-    if (line_data.empty()) continue;  // maybe eof
-    char c = first_non_whitespace(line_data);
-    if (c == '\0') continue;  // only whitespace
-    if (c == '#') continue;  // only comment
-    trace(99, "parse") << "line: " << line_data << end();
-    istringstream lin(line_data);
-    while (has_data(lin)) {
-      string word_data;
-      lin >> word_data;
-      if (word_data.empty()) continue;  // maybe eof
-      if (word_data[0] == '#') break;  // comment; ignore rest of line
-      if (word_data == "record")
-        load_record(lin, in);
-      else if (word_data == "choice")
-        load_choice(lin, in);
-      else if (word_data == "var")
-        load_global(lin, in);
-      else if (word_data == "fn")
-        load_function(lin, in);
-      else
-        raise << "unrecognized top-level keyword '" << word_data << "'; should be one of 'record', 'choice', 'var' or 'fn'\n" << end();
-      break;
-    }
-    // nothing here, because we'll be at the next top-level declaration
-  }
-}
-
-void load_record(istream& first_line, istream& in) {
-}
-
-void load_choice(istream& first_line, istream& in) {
-}
-
-void load_global(istream& first_line, istream& in) {
-}
-
-void load_function(istream& first_line, istream& in) {
-  string name;
-  assert(has_data(first_line));
-  first_line >> name;
-  trace(99, "parse") << "function: " << name << end();
-  function_info& curr = new_function(name);
-  string tmp;
-  // read in parameters
-  while (has_data(first_line)) {
-    // read operand name
-    first_line >> tmp;
-//?     cerr << "0: " << tmp << '\n';
-    if (tmp == "[") break;
-    if (tmp == "->") break;
-    assert(tmp != ":");
-    curr.in.push_back(operand(tmp));
-
-    // skip ':'
-    assert(has_data(first_line));
-    first_line >> tmp;
-//?     cerr << "1: " << tmp << '\n';
-    assert(tmp == ":");  // types are required in function headers
-
-    // read operand type
-    assert(has_data(first_line));
-    curr.in.back().set_type(first_line);
-  }
-  // read in-out parameters
-  while (tmp != "[" && has_data(first_line)) {
-    // read operand name
-    first_line >> tmp;
-//?     cerr << "inout 0: " << tmp << '\n';
-    if (tmp == "[") break;
-    assert(tmp != "->");
-    assert(tmp != ":");  // types are required in function headers
-    curr.in_out.push_back(operand(tmp));
-
-    // skip ':'
-    assert(has_data(first_line));
-    first_line >> tmp;
-//?     cerr << "inout 1: " << tmp << '\n';
-    assert(tmp == ":");
-
-    // read operand type
-    assert(has_data(first_line));
-    curr.in.back().set_type(first_line);
-  }
-  trace(99, "parse") << "  " << SIZE(curr.in) << " in operands" << end();
-  trace(99, "parse") << "  " << SIZE(curr.in_out) << " in_out operands" << end();
-  // not bothering checking for tokens past '[' in first_line
-  
-  // read instructions
-  while (has_data(in)) {
-    string line_data;
-    getline(in, line_data);
-    if (first_non_whitespace(line_data) == ']') break;
-//?     bool has_in_out = (line_data.find("<-") != string::npos);
-    istringstream line(line_data);
-    vector<string> words;
-    bool has_in_out = false;
-    while (has_data(line)) {
-      string w;
-      line >> w;
-      words.push_back(w);
-      if (w == "<-")
-        has_in_out = true;
-    }
-    instruction inst;
-    int i = 0;
-    assert(i < SIZE(words));
-    if (has_in_out) {
-      while (i < SIZE(words)) {
-//?         cerr << "in-out operand: " << i << ' ' << words.at(i) << '\n';
-        inst.in_out.push_back(operand(words.at(i)));
-        ++i;
-        assert(i < SIZE(words));
-        if (words.at(i) == ":") {
-          ++i;  // skip ':'
-          assert(i < SIZE(words));
-          assert(words.at(i) != "<-");
-          assert(words.at(i) != ":");
-          istringstream tmp(words.at(i));
-//?           cerr << "setting type to " << i << ' ' << words.at(i) << '\n';
-          inst.in_out.back().set_type(tmp);
-//?           cerr << "done\n";
-          ++i;
-          assert(i < SIZE(words));
-        }
-        if (words.at(i) == "<-") break;
-      }
-      assert(i < SIZE(words));
-      assert(words.at(i) == "<-");
-      ++i;
-    }
-    assert(i < SIZE(words));
-    assert(words.at(i) != "<-");
-    assert(words.at(i) != ":");
-    inst.name = words.at(i);
-    ++i;
-    while (i < SIZE(words)) {
-      inst.in.push_back(operand(words.at(i)));
-      ++i;
-      if (i < SIZE(words) && words.at(i) == ":") {
-        ++i;  // skip ':'
-        assert(i < SIZE(words));
-        assert(words.at(i) != "<-");
-        assert(words.at(i) != ":");
-        istringstream tmp(words.at(i));
-        inst.in.back().set_type(tmp);
-        ++i;
-      }
-      else if (is_integer(inst.in.back().name)) {
-        inst.in.back().type.push_back(Literal_type_id);
-      }
-    }
-    trace(99, "parse") << "instruction: " << inst.name << end();
-    for (int i = 0;  i < SIZE(inst.in);  ++i)
-      trace(99, "parse") << "  in => " << to_string(inst.in.at(i)) << end();
-    for (int i = 0;  i < SIZE(inst.in_out);  ++i)
-      trace(99, "parse") << "  in_out => " << to_string(inst.in_out.at(i)) << end();
-    curr.instructions.push_back(inst);
-  }
-}
-
-function_info& new_function(string name) {
-  assert(!contains_key(Function_id, name));
-  int id = Next_function_id++;
-  put(Function_id, name, id);
-  assert(!contains_key(Function_info, id));
-  function_info& result = Function_info[id];  // insert
-  result.id = id;
-  result.name = name;
-  return result;
-}
-
-char first_non_whitespace(string in) {
-  for (int i = 0;  i < SIZE(in);  ++i)
-    if (!isspace(in.at(i))) return in.at(i);
-  return '\0';
-}
-
-bool is_integer(const string& s) {
-  return s.find_first_not_of("0123456789-") == string::npos  // no other characters
-      && s.find_first_of("0123456789") != string::npos  // at least one digit
-      && s.find('-', 1) == string::npos;  // '-' only at first position
-}
-
-int to_integer(string n) {
-  char* end = NULL;
-  // safe because string.c_str() is guaranteed to be null-terminated
-  int result = strtoll(n.c_str(), &end, /*any base*/0);
-  if (*end != '\0') cerr << "tried to convert " << n << " to number\n";
-  assert(*end == '\0');
-  return result;
-}
-
-void test_is_integer() {
-  CHECK(is_integer("1234"));
-  CHECK(is_integer("-1"));
-  CHECK(!is_integer("234.0"));
-  CHECK(is_integer("-567"));
-  CHECK(!is_integer("89-0"));
-  CHECK(!is_integer("-"));
-  CHECK(!is_integer("1e3"));  // not supported
-}
diff --git a/vimrc.vim b/vimrc.vim
index adbb9403..5eba7448 100644
--- a/vimrc.vim
+++ b/vimrc.vim
@@ -19,29 +19,6 @@ function! HighlightTangledFile()
 
   highlight Special ctermfg=160
 
-  " Our C++ files can have Mu code in scenarios, so highlight Mu comments like
-  " regular comments.
-  syntax match muComment /#.*$/
-  highlight link muComment Comment
-  syntax match muSalientComment /##.*$/ | highlight link muSalientComment SalientComment
-  syntax match muCommentedCode /#? .*$/ | highlight link muCommentedCode CommentedCode
-  set comments+=n:#
-  " Some other bare-bones Mu highlighting.
-  syntax match muLiteral %[^ ]\+:literal/[^ ,]*\|[^ ]\+:literal\>%
-  syntax match muLiteral %[^ ]\+:label/[^ ,]*\|[^ ]\+:label\>%
-  syntax match muLiteral %[^ ]\+:type/[^ ,]*\|[^ ]\+:type\>%
-  syntax match muLiteral %[^ ]\+:offset/[^ ,]*\|[^ ]\+:offset\>%
-  syntax match muLiteral %[^ ]\+:variant/[^ ,]*\|[^ ]\+:variant\>%
-  syntax match muLiteral % true\(\/[^ ]*\)\?\| false\(\/[^ ]*\)\?%  " literals will never be the first word in an instruction
-  syntax match muLiteral % null\(\/[^ ]*\)\?%
-  highlight link muLiteral Constant
-  syntax match muAssign " <- \|\<raw\>" | highlight link muAssign SpecialChar
-  " common keywords
-  syntax match muRecipe "^recipe\>\|^recipe!\>\|^def\>\|^def!\>\|^before\>\|^after\>\| -> " | highlight muRecipe ctermfg=208
-  syntax match muScenario "^scenario\>" | highlight muScenario ctermfg=34
-  syntax match muPendingScenario "^pending-scenario\>" | highlight link muPendingScenario SpecialChar
-  syntax match muData "^type\>\|^container\>\|^exclusive-container\>" | highlight muData ctermfg=226
-
   syntax match subxString %"[^"]*"% | highlight link subxString Constant
   " match globals but not registers like 'EAX'
   syntax match subxGlobal %\<[A-Z][a-z0-9_-]*\>% | highlight link subxGlobal SpecialChar