about summary refs log tree commit diff stats
path: root/archive
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2019-07-27 16:01:55 -0700
committerKartik Agaram <vc@akkartik.com>2019-07-27 17:47:59 -0700
commit6e1eeeebfb453fa7c871869c19375ce60fbd7413 (patch)
tree539c4a3fdf1756ae79770d5c4aaf6366f1d1525e /archive
parent8846a7f85cc04b77b2fe8a67b6d317723437b00c (diff)
downloadmu-6e1eeeebfb453fa7c871869c19375ce60fbd7413.tar.gz
5485 - promote SubX to top-level
Diffstat (limited to 'archive')
-rw-r--r--archive/1.vm.arc/Readme.md21
-rw-r--r--archive/1.vm.arc/blocking.arc.t26
-rw-r--r--archive/1.vm.arc/buffered-stdin.mu28
-rw-r--r--archive/1.vm.arc/callcc.mu20
-rw-r--r--archive/1.vm.arc/channel.mu49
-rw-r--r--archive/1.vm.arc/charterm/charterm.rkt2798
-rw-r--r--archive/1.vm.arc/charterm/demo.rkt306
-rw-r--r--archive/1.vm.arc/charterm/doc.scrbl7
-rw-r--r--archive/1.vm.arc/charterm/info.rkt29
-rw-r--r--archive/1.vm.arc/charterm/main.rkt3
-rw-r--r--archive/1.vm.arc/charterm/planet-docs/doc/index.html117
-rw-r--r--archive/1.vm.arc/charterm/planet-docs/doc/racket.css234
-rw-r--r--archive/1.vm.arc/charterm/planet-docs/doc/scribble-common.js153
-rw-r--r--archive/1.vm.arc/charterm/planet-docs/doc/scribble-style.css0
-rw-r--r--archive/1.vm.arc/charterm/planet-docs/doc/scribble.css487
-rw-r--r--archive/1.vm.arc/charterm/test-charterm.rkt20
-rw-r--r--archive/1.vm.arc/chessboard.arc.t239
-rw-r--r--archive/1.vm.arc/chessboard.mu259
-rw-r--r--archive/1.vm.arc/color-repl.mu498
-rw-r--r--archive/1.vm.arc/counters.mu33
-rw-r--r--archive/1.vm.arc/edit.arc.t33
-rw-r--r--archive/1.vm.arc/edit.mu18
-rw-r--r--archive/1.vm.arc/exuberant-ctags-rc7
-rw-r--r--archive/1.vm.arc/factorial.mu22
-rw-r--r--archive/1.vm.arc/fork.mu18
-rw-r--r--archive/1.vm.arc/generic.mu30
-rw-r--r--archive/1.vm.arc/graphics.mu23
-rw-r--r--archive/1.vm.arc/highlights21
-rw-r--r--archive/1.vm.arc/load.arc28
-rwxr-xr-xarchive/1.vm.arc/mu27
-rw-r--r--archive/1.vm.arc/mu.arc3259
-rw-r--r--archive/1.vm.arc/mu.arc.t5208
-rw-r--r--archive/1.vm.arc/mu.arc.t.html4154
-rw-r--r--archive/1.vm.arc/render.vim93
-rw-r--r--archive/1.vm.arc/scratch.vim50
-rw-r--r--archive/1.vm.arc/stdin.mu27
-rw-r--r--archive/1.vm.arc/tangle.mu35
-rw-r--r--archive/1.vm.arc/trace.arc.t1659
-rw-r--r--archive/1.vm.arc/trace.mu1092
-rw-r--r--archive/1.vm.arc/vimrc.vim8
-rw-r--r--archive/1.vm.arc/x.mu6
-rw-r--r--archive/2.vm/000organization.cc136
-rw-r--r--archive/2.vm/001help.cc264
-rw-r--r--archive/2.vm/002test.cc104
-rw-r--r--archive/2.vm/003trace.cc501
-rw-r--r--archive/2.vm/003trace.test.cc126
-rw-r--r--archive/2.vm/010vm.cc900
-rw-r--r--archive/2.vm/011load.cc489
-rw-r--r--archive/2.vm/012transform.cc102
-rw-r--r--archive/2.vm/013update_operation.cc40
-rw-r--r--archive/2.vm/014literal_string.cc274
-rw-r--r--archive/2.vm/015literal_noninteger.cc51
-rw-r--r--archive/2.vm/016dilated_reagent.cc166
-rw-r--r--archive/2.vm/017parse_tree.cc124
-rw-r--r--archive/2.vm/018constant.cc79
-rw-r--r--archive/2.vm/019type_abbreviations.cc236
-rw-r--r--archive/2.vm/020run.cc571
-rw-r--r--archive/2.vm/021check_instruction.cc260
-rw-r--r--archive/2.vm/022arithmetic.cc1071
-rw-r--r--archive/2.vm/023boolean.cc224
-rw-r--r--archive/2.vm/024jump.cc237
-rw-r--r--archive/2.vm/025compare.cc624
-rw-r--r--archive/2.vm/026call.cc246
-rw-r--r--archive/2.vm/027call_ingredient.cc220
-rw-r--r--archive/2.vm/028call_return.cc197
-rw-r--r--archive/2.vm/029tools.cc326
-rw-r--r--archive/2.vm/030container.cc819
-rw-r--r--archive/2.vm/031merge.cc270
-rw-r--r--archive/2.vm/032array.cc635
-rw-r--r--archive/2.vm/033exclusive_container.cc554
-rw-r--r--archive/2.vm/034address.cc514
-rw-r--r--archive/2.vm/035lookup.cc664
-rw-r--r--archive/2.vm/036abandon.cc153
-rw-r--r--archive/2.vm/038new_text.cc288
-rw-r--r--archive/2.vm/040brace.cc566
-rw-r--r--archive/2.vm/041jump_target.cc220
-rw-r--r--archive/2.vm/042name.cc414
-rw-r--r--archive/2.vm/043space.cc331
-rw-r--r--archive/2.vm/044space_surround.cc79
-rw-r--r--archive/2.vm/045closure_name.cc204
-rw-r--r--archive/2.vm/046check_type_by_name.cc265
-rw-r--r--archive/2.vm/050scenario.cc1039
-rw-r--r--archive/2.vm/051scenario_test.mu70
-rw-r--r--archive/2.vm/052tangle.cc529
-rw-r--r--archive/2.vm/053recipe_header.cc793
-rw-r--r--archive/2.vm/054static_dispatch.cc683
-rw-r--r--archive/2.vm/055shape_shifting_container.cc773
-rw-r--r--archive/2.vm/056shape_shifting_recipe.cc1307
-rw-r--r--archive/2.vm/057immutable.cc715
-rw-r--r--archive/2.vm/058to_text.cc24
-rw-r--r--archive/2.vm/059to_text.mu48
-rw-r--r--archive/2.vm/060rewrite_literal_string.cc81
-rw-r--r--archive/2.vm/061text.mu1427
-rw-r--r--archive/2.vm/062convert_ingredients_to_text.cc212
-rw-r--r--archive/2.vm/063array.mu181
-rw-r--r--archive/2.vm/064list.mu366
-rw-r--r--archive/2.vm/065duplex_list.mu781
-rw-r--r--archive/2.vm/066stream.mu80
-rw-r--r--archive/2.vm/067random.cc34
-rw-r--r--archive/2.vm/068random.mu75
-rw-r--r--archive/2.vm/069hash.cc422
-rw-r--r--archive/2.vm/070table.mu109
-rw-r--r--archive/2.vm/072recipe.cc711
-rw-r--r--archive/2.vm/073scheduler.cc709
-rw-r--r--archive/2.vm/074wait.cc664
-rw-r--r--archive/2.vm/075channel.mu510
-rw-r--r--archive/2.vm/076continuation.cc406
-rw-r--r--archive/2.vm/080display.cc462
-rw-r--r--archive/2.vm/081print.mu914
-rw-r--r--archive/2.vm/082scenario_screen.cc458
-rw-r--r--archive/2.vm/083scenario_screen_test.mu47
-rw-r--r--archive/2.vm/084console.mu104
-rw-r--r--archive/2.vm/085scenario_console.cc317
-rw-r--r--archive/2.vm/086scenario_console_test.mu25
-rw-r--r--archive/2.vm/087file.cc225
-rw-r--r--archive/2.vm/088file.mu213
-rw-r--r--archive/2.vm/089scenario_filesystem.cc245
-rw-r--r--archive/2.vm/090scenario_filesystem_test.mu99
-rw-r--r--archive/2.vm/091socket.cc348
-rw-r--r--archive/2.vm/092socket.mu177
-rw-r--r--archive/2.vm/099hardware_checks.cc67
-rw-r--r--archive/2.vm/101run_sandboxed.cc711
-rw-r--r--archive/2.vm/998check_type_pointers.cc36
-rw-r--r--archive/2.vm/999spaces.cc86
-rw-r--r--archive/2.vm/Readme.md449
-rw-r--r--archive/2.vm/args.mu8
-rwxr-xr-xarchive/2.vm/build046
-rwxr-xr-xarchive/2.vm/build169
-rwxr-xr-xarchive/2.vm/build2175
-rwxr-xr-xarchive/2.vm/build3201
-rwxr-xr-xarchive/2.vm/build4297
-rwxr-xr-xarchive/2.vm/build_and_test_until18
-rw-r--r--archive/2.vm/cannot_write_tests_for17
-rw-r--r--archive/2.vm/channel.mu45
-rw-r--r--archive/2.vm/chessboard.mu572
-rwxr-xr-xarchive/2.vm/clean9
-rw-r--r--archive/2.vm/console.mu16
-rw-r--r--archive/2.vm/continuation1.mu25
-rw-r--r--archive/2.vm/continuation2.mu37
-rw-r--r--archive/2.vm/continuation3.mu34
-rw-r--r--archive/2.vm/continuation4.mu47
-rw-r--r--archive/2.vm/continuation5.mu49
-rwxr-xr-xarchive/2.vm/copy_mu11
-rw-r--r--archive/2.vm/counters.mu29
-rw-r--r--archive/2.vm/display.mu25
-rw-r--r--archive/2.vm/edit/001-editor.mu464
-rw-r--r--archive/2.vm/edit/002-typing.mu1144
-rw-r--r--archive/2.vm/edit/003-shortcuts.mu4462
-rw-r--r--archive/2.vm/edit/004-programming-environment.mu549
-rw-r--r--archive/2.vm/edit/005-sandbox.mu1193
-rw-r--r--archive/2.vm/edit/006-sandbox-copy.mu395
-rw-r--r--archive/2.vm/edit/007-sandbox-delete.mu342
-rw-r--r--archive/2.vm/edit/008-sandbox-edit.mu325
-rw-r--r--archive/2.vm/edit/009-sandbox-test.mu231
-rw-r--r--archive/2.vm/edit/010-sandbox-trace.mu253
-rw-r--r--archive/2.vm/edit/011-errors.mu886
-rw-r--r--archive/2.vm/edit/012-editor-undo.mu2111
-rw-r--r--archive/2.vm/edit/Readme.md49
-rw-r--r--archive/2.vm/example1.mu7
-rw-r--r--archive/2.vm/exception1.mu61
-rw-r--r--archive/2.vm/exception2.mu62
-rw-r--r--archive/2.vm/exuberant_ctags_rc11
-rw-r--r--archive/2.vm/factorial.mu33
-rw-r--r--archive/2.vm/filesystem.mu20
-rw-r--r--archive/2.vm/fork.mu16
-rwxr-xr-xarchive/2.vm/git_log_filtered9
-rw-r--r--archive/2.vm/http-client.mu29
-rw-r--r--archive/2.vm/http-server.mu28
-rw-r--r--archive/2.vm/immutable-error.mu13
-rw-r--r--archive/2.vm/lambda-to-mu.mu590
-rwxr-xr-xarchive/2.vm/mu11
-rw-r--r--archive/2.vm/mu.vim98
-rw-r--r--archive/2.vm/mutable.mu13
-rwxr-xr-xarchive/2.vm/new_lesson15
-rw-r--r--archive/2.vm/nqueens.mu101
-rw-r--r--archive/2.vm/real-files.mu18
-rwxr-xr-xarchive/2.vm/relayout65
-rw-r--r--archive/2.vm/same-fringe.mu89
-rw-r--r--archive/2.vm/sandbox/001-editor.mu464
-rw-r--r--archive/2.vm/sandbox/002-typing.mu1144
-rw-r--r--archive/2.vm/sandbox/003-shortcuts.mu2800
-rw-r--r--archive/2.vm/sandbox/004-programming-environment.mu268
-rw-r--r--archive/2.vm/sandbox/005-sandbox.mu1081
-rw-r--r--archive/2.vm/sandbox/006-sandbox-copy.mu286
-rw-r--r--archive/2.vm/sandbox/007-sandbox-delete.mu345
-rw-r--r--archive/2.vm/sandbox/008-sandbox-edit.mu319
-rw-r--r--archive/2.vm/sandbox/009-sandbox-test.mu233
-rw-r--r--archive/2.vm/sandbox/010-sandbox-trace.mu243
-rw-r--r--archive/2.vm/sandbox/011-errors.mu687
-rw-r--r--archive/2.vm/sandbox/012-editor-undo.mu1907
-rw-r--r--archive/2.vm/sandbox/Readme.md33
-rwxr-xr-xarchive/2.vm/sandbox/mu_run16
-rw-r--r--archive/2.vm/sandbox/tmux.conf3
-rw-r--r--archive/2.vm/screen.mu29
-rwxr-xr-xarchive/2.vm/snapshot_lesson12
-rw-r--r--archive/2.vm/static-dispatch.mu29
-rw-r--r--archive/2.vm/tangle.mu36
-rw-r--r--archive/2.vm/termbox/COPYING19
-rw-r--r--archive/2.vm/termbox/Readme2
-rw-r--r--archive/2.vm/termbox/bytebuffer.inl79
-rw-r--r--archive/2.vm/termbox/input.inl185
-rw-r--r--archive/2.vm/termbox/output.inl320
-rw-r--r--archive/2.vm/termbox/termbox.c397
-rw-r--r--archive/2.vm/termbox/termbox.h190
-rw-r--r--archive/2.vm/termbox/utf8.c79
-rwxr-xr-xarchive/2.vm/test_layers88
-rw-r--r--archive/2.vm/vimrc.vim31
-rw-r--r--archive/2.vm/x.mu8
-rw-r--r--archive/3.transect/Readme6
-rwxr-xr-xarchive/3.transect/build109
-rwxr-xr-xarchive/3.transect/build_and_test_until18
-rwxr-xr-xarchive/3.transect/clean8
-rw-r--r--archive/3.transect/compiler10304
-rw-r--r--archive/3.transect/compiler227
-rw-r--r--archive/3.transect/compiler373
-rw-r--r--archive/3.transect/compiler484
-rw-r--r--archive/3.transect/compiler532
-rw-r--r--archive/3.transect/compiler636
-rw-r--r--archive/3.transect/compiler746
-rw-r--r--archive/3.transect/compiler853
-rw-r--r--archive/3.transect/compiler9254
-rw-r--r--archive/3.transect/ex3.k222
-rw-r--r--archive/3.transect/ex4.k234
-rw-r--r--archive/3.transect/ex5.k230
-rw-r--r--archive/3.transect/ex6.k223
-rw-r--r--archive/3.transect/ex7.k264
-rw-r--r--archive/3.transect/ex8.k236
-rw-r--r--archive/3.transect/factorial.k216
-rw-r--r--archive/3.transect/vimrc.vim36
229 files changed, 80488 insertions, 0 deletions
diff --git a/archive/1.vm.arc/Readme.md b/archive/1.vm.arc/Readme.md
new file mode 100644
index 00000000..b8292abb
--- /dev/null
+++ b/archive/1.vm.arc/Readme.md
@@ -0,0 +1,21 @@
+Original prototype, last modified 2015-03-14
+
+First install [Racket](http://racket-lang.org) (just for this prototype;
+last tested with v6.3). Then:
+
+  ```shell
+  $ cd mu/archives/1.vm
+  $ git clone http://github.com/arclanguage/anarki
+  $ cd anarki
+  $   git checkout d7290130a7  # last compatible snapshot
+  $ cd ..
+  $ ./mu test mu.arc.t  # run tests
+  ```
+
+Example programs:
+
+  ```shell
+  $ ./mu factorial.mu  # computes factorial of 5
+  $ ./mu fork.mu  # two threads print '33' and '34' forever
+  $ ./mu channel.mu  # two threads in a producer/consumer relationship
+  ```
diff --git a/archive/1.vm.arc/blocking.arc.t b/archive/1.vm.arc/blocking.arc.t
new file mode 100644
index 00000000..80f7f229
--- /dev/null
+++ b/archive/1.vm.arc/blocking.arc.t
@@ -0,0 +1,26 @@
+(selective-load "mu.arc" section-level)
+(set allow-raw-addresses*)
+
+(reset)
+(new-trace "blocking-example")
+(add-code
+  '((function reader [
+      (default-space:space-address <- new space:literal 30:literal/capacity)
+      (x:tagged-value 1:channel-address/space:global <- read 1:channel-address/space:global)
+     ])
+    (function main [
+      (default-space:space-address <- new space:literal 30:literal/capacity)
+      (1:channel-address <- init-channel 3:literal)
+      (2:integer/routine <- fork-helper reader:fn default-space:space-address/globals 50:literal/limit)
+      ; write nothing to the channel
+;?       (sleep until-routine-done:literal 2:integer/routine)
+     ])))
+;? (= dump-trace* (obj whitelist '("schedule" "run")))
+(run 'main)
+;? (prn "completed:")
+;? (each r completed-routines*
+;?   (prn " " r))
+(when (ran-to-completion 'reader)
+  (prn "F - reader waits for input"))
+
+(reset)
diff --git a/archive/1.vm.arc/buffered-stdin.mu b/archive/1.vm.arc/buffered-stdin.mu
new file mode 100644
index 00000000..9a7bc7ae
--- /dev/null
+++ b/archive/1.vm.arc/buffered-stdin.mu
@@ -0,0 +1,28 @@
+; reads lines, prints them back when you hit 'enter'
+; dies if you wait a while, because so far we never free memory
+(function main [
+  (default-space:space-address <- new space:literal 30:literal)
+  (cursor-mode) ;? 1
+  ; hook up stdin
+  (stdin:channel-address <- init-channel 1:literal)
+  (fork-helper send-keys-to-stdin:fn nil:literal/globals nil:literal/limit nil:literal/keyboard stdin:channel-address)
+  ; buffer stdin
+  (buffered-stdin:channel-address <- init-channel 1:literal)
+  (fork-helper buffer-lines:fn nil:literal/globals nil:literal/limit stdin:channel-address buffered-stdin:channel-address)
+  { begin
+    ; now read characters from the buffer until 'enter' is typed
+    (s:string-address <- new "? ")
+    (print-string nil:literal/terminal s:string-address)
+    { begin
+      (x:tagged-value buffered-stdin:channel-address/deref <- read buffered-stdin:channel-address)
+      (c:character <- maybe-coerce x:tagged-value character:literal)
+;?       ($print (("AAA " literal))) ;? 1
+;?       ($print c:character) ;? 1
+;?       ($print (("\n" literal))) ;? 1
+      (print-character nil:literal/terminal c:character)
+      (line-done?:boolean <- equal c:character ((#\newline literal)))
+      (loop-unless line-done?:boolean)
+    }
+    (loop)
+  }
+])
diff --git a/archive/1.vm.arc/callcc.mu b/archive/1.vm.arc/callcc.mu
new file mode 100644
index 00000000..20dffeff
--- /dev/null
+++ b/archive/1.vm.arc/callcc.mu
@@ -0,0 +1,20 @@
+; in mu, call-cc (http://en.wikipedia.org/wiki/Call-with-current-continuation)
+; is constructed out of a combination of two primitives:
+;   'current-continuation', which returns a continuation, and
+;   'continue-from', which takes a continuation to
+
+(function g [
+  (c:continuation <- current-continuation)  ; <-- loop back to here
+  (print-character nil:literal/terminal ((#\a literal)))
+  (reply c:continuation)
+])
+
+(function f [
+  (c:continuation <- g)
+  (reply c:continuation)
+])
+
+(function main [
+  (c:continuation <- f)
+  (continue-from c:continuation)            ; <-- ..when you hit this
+])
diff --git a/archive/1.vm.arc/channel.mu b/archive/1.vm.arc/channel.mu
new file mode 100644
index 00000000..61151833
--- /dev/null
+++ b/archive/1.vm.arc/channel.mu
@@ -0,0 +1,49 @@
+(function producer [
+  ; produce numbers 1 to 5 on a channel
+  (default-space:space-address <- new space:literal 30:literal)
+  (chan:channel-address <- next-input)
+  ; n = 0
+  (n:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- less-than n:integer 5:literal)
+    (break-unless done?:boolean)
+    ; other threads might get between these prints
+    ($print (("produce: " literal)))
+    (print-integer nil:literal/terminal n:integer)
+    ($print (("\n" literal)))
+    ; 'box' n into a dynamically typed 'tagged value' because that's what
+    ; channels take
+    (n2:integer <- copy n:integer)
+    (n3:tagged-value-address <- init-tagged-value integer:literal n2:integer)
+    (chan:channel-address/deref <- write chan:channel-address n3:tagged-value-address/deref)
+    (n:integer <- add n:integer 1:literal)
+    (loop)
+  }
+])
+
+(function consumer [
+  ; consume and print integers from a channel
+  (default-space:space-address <- new space:literal 30:literal)
+  (chan:channel-address <- next-input)
+  { begin
+    ; read a tagged value from the channel
+    (x:tagged-value chan:channel-address/deref <- read chan:channel-address)
+    ; unbox the tagged value into an integer
+    (n2:integer <- maybe-coerce x:tagged-value integer:literal)
+    ; other threads might get between these prints
+    ($print (("consume: " literal)))
+    (print-integer nil:literal/terminal n2:integer)
+    ($print (("\n" literal)))
+    (loop)
+  }
+])
+
+(function main [
+  (default-space:space-address <- new space:literal 30:literal)
+  (chan:channel-address <- init-channel 3:literal)
+  ; create two background 'routines' that communicate by a channel
+  (routine1:integer <- fork consumer:fn nil:literal/globals nil:literal/limit chan:channel-address)
+  (routine2:integer <- fork producer:fn nil:literal/globals nil:literal/limit chan:channel-address)
+  (sleep until-routine-done:literal routine1:integer)
+  (sleep until-routine-done:literal routine2:integer)
+])
diff --git a/archive/1.vm.arc/charterm/charterm.rkt b/archive/1.vm.arc/charterm/charterm.rkt
new file mode 100644
index 00000000..cae12098
--- /dev/null
+++ b/archive/1.vm.arc/charterm/charterm.rkt
@@ -0,0 +1,2798 @@
+#lang racket/base
+;; Copyright (c) Neil Van Dyke.  See file "info.rkt".
+
+(require (for-syntax racket/base
+                     racket/syntax)
+         racket/system
+         (planet neil/mcfly))
+
+(doc (section "Introduction")
+
+     (para "The "
+           "CharTerm"
+           " package provides a Racket interface for character-cell video
+display terminals on Unix-like systems -- such as for "
+           (as-index "GNU Screen")
+           " and "
+           (as-index (code "tmux"))
+           " sessions on "
+           (index '("cloud server" "server") "cloud servers")
+           ", "
+           (as-index "XTerm")
+           " windows on a workstation desktop, and some older hardware
+terminals (even the venerable "
+           (as-index "DEC VT100")
+           ").  Currently, it implements a subset of features available on most
+terminals.")
+
+     (para "This package could be used to implement a status/management console
+for a Racket-based server process (perhaps run in GNU Screen or "
+           (code "tmux")
+           " on a server machine, to be detached and reattached from SSH
+sessions), a lightweight user interface for a systems tool, a command-line
+REPL, a text editor, creative retro uses of old equipment, and, perhaps most
+importantly, a "
+           ;; (hyperlink "http://en.wikipedia.org/wiki/Rogue_%28computer_game%29"
+           "Rogue-like"
+           ;;)
+           " application.")
+
+     (para "The "
+           "CharTerm"
+           " package does not include any native code (such as from "
+           (as-index (code "terminfo"))
+           ", "
+           (as-index (code "termcap"))
+           ", "
+           (as-index (code "curses"))
+           ", or "
+           (as-index (code "ncurses"))
+           ") in the Racket process,
+such as through the Racket FFI or C extensions, so there is less potential for
+a problem involving native code to threaten the reliability or security of a
+program.  "
+           "CharTerm"
+           " is implemented in pure Racket code except for executing "
+           (code "/bin/stty")
+           " for some purposes.  Specifically, "
+           (code "/bin/stty")
+           " at startup time and shutdown time, to set modes, and (for terminal
+types that don't seem to support a screen size report control sequence) when
+getting screen size.  Besides security and stability, lower dependence on
+native code might also simplify porting to host platforms that don't have those
+native code facilities."))
+
+(doc (subsection "Demo")
+
+     (para "For a demonstration, the following command, run from a terminal, should install the "
+           "CharTerm"
+           " package (if not already installed), and run the demo:")
+
+     (commandline "racket -pm neil/charterm/demo")
+
+     (para "This demo reports what keys you pressed, while letting you edit a
+text field, and while displaying a clock.  The clock is updated roughly once
+per second, and is not updated during heavy keyboard input, such as when typing
+fast.  The demo responds to changing terminal sizes, such as when an XTerm is
+window is resized.  It also displays the determined terminal size, and some
+small tests of the "
+           (racket #:width)
+           " argument to "
+           (racket charterm-display)
+           ".  Exit the demo by pressing the "
+           (bold "Esc")
+           " key.")
+
+     (para "Note: Although this demo includes an editable text field, as proof
+of concept, the current version of "
+           "CharTerm"
+           " does not provide editable text fields as reusable functionality."))
+
+(doc (subsection "Simple Example")
+
+     (para "Here's your first "
+           "CharTerm"
+           " program:")
+
+     (RACKETBLOCK
+      (UNSYNTAX (code "#lang racket/base"))
+
+      (require (planet neil/charterm))
+
+      (with-charterm
+       (charterm-clear-screen)
+       (charterm-cursor 10 5)
+       (charterm-display "Hello, ")
+       (charterm-bold)
+       (charterm-display "you")
+       (charterm-normal)
+       (charterm-display ".")
+       (charterm-cursor 1 1)
+       (charterm-display "Press a key...")
+       (let ((key (charterm-read-key)))
+         (charterm-cursor 1 1)
+         (charterm-clear-line)
+         (printf "You pressed: ~S\r\n" key))))
+
+     (para "Now you're living the dream of the '70s."))
+
+(doc (section "Terminal Diversity")
+
+     (para "Like people, few terminals are exactly the same.")
+
+     (para "Some key (ha) terms (ha) used by "
+           "CharTerm"
+           " are:")
+
+     (itemlist (item (tech "termvar")
+                     " --- a string value like from the Unix-like "
+                     (code "TERM")
+                     " environment variable, used to determine a default "
+                     (tech "protocol")
+                     " and "
+                     (tech "keydec")
+                     ".")
+
+               (item (tech "protocol")
+                     " --- how to control the display, query for information, etc.")
+
+               (item (tech "keydec")
+                     " --- how to decode key encodings of a particular
+terminal.  A keydec is constructed from one or more keysets, can produce "
+                     (tech "keycode")
+                     "s or "
+                     (tech "keyinfo")
+                     "s.")
+
+               (item (tech "keyset")
+                     " --- a specification of encoding some of the keys in a
+particular terminal, including "
+                     (tech "keylabel")
+                     "s and "
+                     (tech "keycode")
+                     "s.")
+
+               (item (tech "keylabel")
+                     " --- a string for how a key is likely labeled on a
+keyboard, such as the DEC VT100 "
+                     (bold "PF1")
+                     " key would have a keylabel "
+                     (racket "PF1")
+                     " for a "
+                     (tech "keycode")
+                     " "
+                     (racket 'f1)
+                     ".")
+
+               (item (tech "keycode")
+                     " --- a value produced by a decoded key,
+such as a character for normal printable keys, like "
+                     (racket #\a)
+                     " and "
+                     (racket #\space)
+                     ", a symbol for some recognized unprintable keys, like "
+                     (racket 'escape)
+                     " and "
+                     (racket 'f1)
+                     ", or possibly a number for unrecognized keys.")
+
+               (item (tech "keyinfo")
+                     " --- an object that is used like a "
+                     (tech "keycode")
+                     ", except
+bundles together a keycode and a "
+                     (tech "keylabel")
+                     ", as well as alternatate keycodes and
+information about how the key was decoded (e.g., from which "
+                     (tech "keyset")
+                     ")."))
+
+     (para "These terms are discussed in the following subsections.")
+
+     (para "CharTerm"
+           " is developed with help of original documentation such as that
+curated by Paul Williams at "
+           (hyperlink "http://vt100.net/" "vt100.net")
+           ", various commentary found on the Web, observed behavior with
+modern software terminals like XTerm, various emulators for hardware terminals,
+and sometimes original hardware terminals.  Thanks to Mark Pearrow for
+contributing a TeleVideo 950, and Paul McCabe for a Wyse S50 WinTerm.")
+
+     (para "At time of this writing, the author is looking to acquire a DEC
+VT525, circa 1994, for ongoing testing.")
+
+     (para "The author welcomes feedback on useful improvements to "
+           "CharTerm"
+           "'s support for terminal diversity (no pun).  If you have a terminal
+that is sending an escape sequence not recognized by the demo, you can run the
+demo with the "
+           (Flag "n")
+           " (aka "
+           (DFlag "no-escape")
+           ") argument to see the exact byte sequence:")
+
+     (commandline "racket -pm- neil/charterm/demo -n")
+
+     (para "When "
+           (Flag "n")
+           " is used, this will be indicated by the bottom-most scrolling line,
+rather than saying ``"
+           (tt "To quit, press " (bold "Esc") ".")
+           "'' instead will say ``"
+           (tt "There is no escape from this demo.")
+           "'' You will have to kill the process through some other means."))
+
+(doc (subsection "Protocol")
+
+     (para "The first concept "
+           "CharTerm"
+           " has for distinguishing how to communicate with a terminal is what
+is what is called here "
+           (deftech "protocol")
+           ", which concerns everything except how keyboard keys are decoded.
+The following protocols are currently implemented:")
+
+     (itemlist
+
+      (item (deftech (code "ansi") " protocol")
+            " --- Terminals approximating ["
+            (tech "ANSI X3.64")
+            "], which is most terminals in use today, including software ones
+like XTerm.  This protocol is the emphasis of this package; the other protocols
+are for unusual situations.")
+
+      ;; (item (code "dec-vt100")
+      ;;       " --- The DEC VT100 and compatibles that could be considered "
+      ;;       (code "ansi")
+      ;;       " except don't have insert-line and delete-line.")
+
+      (item (deftech (code "wyse-wy50") " protocol")
+            " --- Terminals compatible with the Wyse WY-50.  This support is
+based on ["
+            (tech "WY-50-QRG")
+            "], ["
+            (tech "WY-60-UG")
+            "], ["
+            (tech "wy60")
+            "], and ["
+            (tech "PowerTerm")
+            "].  Note that video attributes are not supported, due to the WY-50's
+model of having video attribute changes occupy character cells; you may wish
+to run the Wyse terminal in an ANSI or VT100 mode.")
+
+      (item (deftech (code "televideo-925") " protocol")
+            " --- Terminals compatible with the TeleVideo 925.  This support is based on ["
+            (tech "TVI-925-IUG")
+            "] and behavior of ["
+            (tech "PowerTerm")
+            "].  Note that video attributes are not supported, due to the 925's
+model of having video attribute changes occupy character cells; you may wish to
+run your TeleVideo terminal in ANSI or VT100 mode, if it has one.")
+
+      (item (deftech (code "ascii") " protocol")
+            " --- Terminals that support ASCII but not much else that we know about.")))
+
+(define-syntax (%charterm:protocol-case stx)
+  (syntax-case stx (else)
+    ((_ ERROR-NAME ACTUAL-PROTO (PART0 PART1 PARTn ...) ...)
+     (let loop-clauses ((clause-stxes             (syntax->list #'((PART0 PART1 PARTn ...) ...)))
+                        (reverse-out-clause-stxes '())
+                        (else-stx                 #f)
+                        (need-protos-hash         (make-immutable-hasheq (map (lambda (proto)
+                                                                                (cons proto #t))
+                                                                              '(ansi
+                                                                                televideo-925
+                                                                                wyse-wy50)))))
+       (if (null? clause-stxes)
+           (let ((missing-protos (hash-keys need-protos-hash)))
+             (if (or else-stx (null? missing-protos))
+                 (quasisyntax/loc stx
+                   (let ((actual-proto ACTUAL-PROTO))
+                     (case actual-proto
+                       #,@(reverse reverse-out-clause-stxes)
+                       #,(or else-stx
+                             (syntax/loc stx
+                               (else (error ERROR-NAME
+                                            "unimplemented for protocol: ~S"
+                                            actual-proto)))))))
+                 (raise-syntax-error '%charterm:protocol-case
+                                     (format "missing protocols ~S" missing-protos)
+                                     stx)))
+           (let* ((clause-stx   (car clause-stxes))
+                  (clause-parts (syntax->list clause-stx))
+                  (part0-stx    (car clause-parts))
+                  (part0-e      (syntax-e part0-stx)))
+             (if (eq? 'else part0-e)
+                 (if else-stx
+                     (raise-syntax-error '%charterm:protocol-case
+                                         "else clause multiply defined"
+                                         clause-stx
+                                         #f
+                                         (list else-stx))
+                     (loop-clauses (cdr clause-stxes)
+                                   reverse-out-clause-stxes
+                                   clause-stx
+                                   need-protos-hash))
+                 (let loop-protos ((proto-stxes      (syntax->list (car (syntax->list clause-stx))))
+                                   (need-protos-hash need-protos-hash))
+                   (if (null? proto-stxes)
+                       (loop-clauses (cdr clause-stxes)
+                                     (cons clause-stx reverse-out-clause-stxes)
+                                     else-stx
+                                     need-protos-hash)
+                       (let* ((proto-stx (car proto-stxes))
+                              (proto-e   (syntax-e proto-stx)))
+                         (if (symbol? proto-e)
+                             (if (hash-has-key? need-protos-hash proto-e)
+                                 (loop-protos (cdr proto-stxes)
+                                              (hash-remove need-protos-hash proto-e))
+                                 (raise-syntax-error '%charterm:protocol-case
+                                                     "protocol unrecognized or multiply defined"
+                                                     proto-stx))
+                             (raise-syntax-error '%charterm:protocol-case
+                                                 "invalid protocol symbol"
+                                                 proto-stx))))))))))))
+
+(define-syntax (%charterm:unimplemented stx)
+  (syntax-case stx ()
+    ((_ CT ERROR-NAME)
+     (syntax/loc stx
+       (error ERROR-NAME
+              "unimplemented feature for protocol ~S"
+              (charterm-protocol CT))))))
+
+(doc (subsection "Key Encoding")
+
+     (para "While most video display control, they seem to vary more by key
+encoding.")
+
+     (para "The "
+           "CharTerm"
+           " author was motivated to increase the sophistication of its
+keyboard handling after a series of revelations on the Sunday of the long
+weekend in which "
+           "CharTerm"
+           " was initially written.  The first was discovering that four of the
+function keys that had been working fine in "
+           (code "rxvt")
+           " did not work in XTerm.  Dave Gilbert somewhat demystified this by
+pointing out that the original VT100 had only four function keys, which set
+into motion an unfortunate series of bad decisions by various developers of
+terminal software to be needlessly incompatible with each other.  After
+Googling, a horrifying 2005 Web post by Phil Gregory ["
+           (tech "Gregory")
+           "], which showed that key encoding among XTerm variants was even
+worse than one could ever fear.  Even if one already knew how much subtleties
+of old terminals varied (e.g., auto-newline behavior, whether an attribute
+change consumed a space, etc.), this incompatibility in newer software was
+surprising. Then, on a hunch, I tried the Linux Console on a Debian Squeeze
+machine, which surely is ANSI, and found, however, that it generated "
+           (italic "yet different")
+           " byte sequences, for the first "
+           (italic "five")
+           " (not four) function keys.  Then I compared all to the ["
+           (tech "ECMA-48")
+           "] standard, which turns out to be nigh-inscrutable, so which might
+help explain why everyone became so anti-social.")
+
+     (para "CharTerm"
+           " now provides the abstractions of "
+           (tech "keysets")
+           " and "
+           (tech "keydecs")
+           " to deal with this diversity in a maintainable way."))
+
+(doc (subsubsection "Keylabel")
+
+     (para "A "
+           (deftech "keylabel")
+           " is a Racket string for how a key is likely labeled on a particular terminal's keyboard.  Different keyboards may have different keylabels for the same "
+           (tech "keycode")
+           ".  For example, a VT100 has a "
+           (bold "PF1")
+           " key (keylabel "
+           (racket "PF1")
+           ", keycode "
+           (racket 'f1)
+           "), while many other keyboards would label the key "
+           (bold "F1")
+           " (keylabel "
+           (racket "F1")
+           ", keycode "
+           (racket 'f1)
+           ").  The keylabel currently is most useful for documenting and debugging, although it could later be used when giving instructions to the user, such as knowing whether to tell the user the "
+           (bold "Return")
+           " key or the "
+           (bold "Enter")
+           " key; the "
+           (bold "Backspace")
+           " or the "
+           (bold "Rubout")
+           " key; etc."))
+
+(doc (subsubsection "Keycode")
+
+     (para "A "
+           (deftech "keycode")
+           " is a value representing a key read from a terminal, which can be a Racket character, symbol, or number.  Keys corresponding to printable characters have keycodes as Racket characters.  Some keys corresponding to special non-printable characters can have keycodes of Racket symbols, such as "
+           (racket 'return)
+           ", "
+           (racket 'f1)
+           ", "
+           (racket 'up)
+           ", etc."))
+
+;; TODO: Document here all the symbol keycodes we define.
+
+(doc (defproc (charterm-keycode? (x any/c))
+         boolean?
+       "Predicate for whether or not "
+       (racket x)
+       " is a valid keycode."))
+(provide charterm-keycode?)
+(define (charterm-keycode? x)
+  (if (or (symbol? x)
+          (char? x)
+          (exact-nonnegative-integer? x))
+      #t
+      #f))
+
+(doc (subsubsection "Keyinfo")
+
+     (para "A "
+           (deftech "keyinfo")
+           " represents a "
+           (tech "keycode")
+           " for a key, a "
+           (tech "keylabel")
+           ", and how it is encoded as bytes.  It is represented in Racket as
+a "
+           (racket charterm-keyinfo)
+           " object."))
+
+(define-struct charterm-keyinfo
+  (keyset-id
+   bytelang
+   bytelist
+   keylabel
+   keycode
+   all-keycodes)
+  #:transparent)
+
+(doc (defproc (charterm-keyinfo? (x any/c))
+         boolean?)
+     "Predicate for whether or not "
+     (racket x)
+     " is a "
+     (racket charterm-keyinfo)
+     " object.")
+(provide charterm-keyinfo?)
+
+(doc (defproc*
+         (((charterm-keyinfo-keyset-id    (ki charterm-keyinfo?)) symbol?)
+          ((charterm-keyinfo-bytelang     (ki charterm-keyinfo?)) string?)
+          ((charterm-keyinfo-bytelist     (ki charterm-keyinfo?)) (listof byte?))
+          ((charterm-keyinfo-keylabel     (ki charterm-keyinfo?)) string?)
+          ((charterm-keyinfo-keycode      (ki charterm-keyinfo?)) charterm-keycode?)
+          ((charterm-keyinfo-all-keycodes (ki charterm-keyinfo?)) (listof charterm-keycode?)))
+       (para "Get information from a "
+             (racket charterm-keyinfo)
+             " object.")))
+(provide charterm-keyinfo-keyset-id
+         charterm-keyinfo-bytelang
+         charterm-keyinfo-bytelist
+         charterm-keyinfo-keylabel
+         charterm-keyinfo-keycode
+         charterm-keyinfo-all-keycodes)
+
+(define %charterm:bytestr-to-byte-hash
+  (make-hash
+   `(("nul"      . 0)
+     ("null"     . 0)
+     ("lf"       . 10)
+     ("linefeed" . 10)
+     ("cr"       . 13)
+     ("return"   . 13)
+     ("ret"      . 13)
+     ("esc"      . 27)
+     ("^["       . 27)
+     ("sp"       . 32)
+     ("space"    . 32)
+     ,@(for/list ((n (in-range 1 26)))
+         (cons (string #\^ (integer->char (+ 96 n)))
+               n))
+     ,@(for/list ((n (in-range 1 26)))
+         (cons (string-append "ctrl-"
+                              (string (integer->char (+ 96 n))))
+               n))
+     ,@(for/list ((n (in-range 32 127)))
+         (cons (string (integer->char n))
+               n))
+     ,@(for/list ((n (in-range 0 255)))
+         (cons (string-append "("
+                              (number->string n)
+                              ")")
+               n)))))
+
+(define (%charterm:bytestr->byte bytestr)
+  (hash-ref %charterm:bytestr-to-byte-hash bytestr))
+
+(define (%charterm:bytelang->bytelist bytelang secondary?)
+  (let ((bytelist (map %charterm:bytestr->byte
+                       (regexp-split #rx" +" bytelang))))
+    (if (and secondary? (not (= 1 (length bytelist))))
+        (error '%charterm:bytelang->bytelist
+               "bytelist for secondary keyset: ~S"
+               bytelist)
+        bytelist)))
+
+(define (%charterm:keycode->keylabel keycode)
+  (cond ((not keycode) #f)
+        ((symbol? keycode) (string-titlecase (symbol->string keycode)))
+        ((char?   keycode) (string keycode))
+        ((number? keycode) (number->string keycode))
+        (else (error '%charterm:keycode->keylabel
+                     "invalid keycode: ~S"
+                     keycode))))
+
+(define (%charterm:keylang->keyinfo keyset-id keylang secondary?)
+  (apply (lambda (bytelang . args)
+           (let-values (((bytelist)
+                         (%charterm:bytelang->bytelist bytelang secondary?))
+                        ((keylabel keycode all-keycodes)
+                         (let ((keylabel (car args)))
+                           (if (or (string? keylabel)
+                                   (not keylabel))
+                               (values keylabel
+                                       (cadr args)
+                                       (cdr args))
+                               (let ((keycode (car args)))
+                                 (values (%charterm:keycode->keylabel keycode)
+                                         keycode
+                                         args))))))
+             (make-charterm-keyinfo keyset-id
+                                    bytelang
+                                    bytelist
+                                    keylabel
+                                    keycode
+                                    all-keycodes)))
+         keylang))
+
+(doc (subsubsection "Keyset")
+
+     (para "A "
+           (deftech "keyset")
+           " is a specification of keys on a particular keyboard, including their "
+           (tech "keylabel")
+           ", encoding as bytes, and primary and alternate "
+           (tech #:key "keycode" "keycodes")
+           ".")
+
+     ;; TODO: Expose ability to construct keysets, once it's finalized.
+     (para "The means of constructing a keyset is currently internal to this package."))
+
+(define-struct charterm-keyset
+  (id primary-keyinfos secondary-keyinfos)
+  #:transparent)
+
+(doc (defproc (charterm-keyset? (x any/c))
+         boolean?
+       (para "Predicate for whether or not "
+             (racket x)
+             " is a keyset.")))
+(provide charterm-keyset?)
+
+(doc (defproc (charterm-keyset-id (ks charterm-keyset?))
+         symbol?)
+     (para "Get a symbol identifying the keyset."))
+(provide charterm-keyset-id)
+
+;; (define (%charterm:keyinfos? x)
+;;   (for/and ((x (in-list x)))
+;;     (charterm-keyinfo? x)))
+;;
+;; (define (%charterm:assert-keyinfos keyinfos)
+;;   (or (%charterm:keyinfos? keyinfos)
+;;       (error '%charterm:assert-keyinfos
+;;              "assertion failed: ~S"
+;;              keyinfos)))
+
+(define (make-charterm-keyset-from-keylangs keyset-id
+                                            keylangs
+                                            (secondary-keylangs '()))
+  (let ((primary-keyinfos   (map (lambda (keylang)
+                                   (%charterm:keylang->keyinfo keyset-id keylang #f))
+                                 keylangs))
+        (secondary-keyinfos (map (lambda (keylang)
+                                   (%charterm:keylang->keyinfo keyset-id keylang #t))
+                                 secondary-keylangs)))
+    ;; (%charterm:assert-keyinfos primary-keyinfos)
+    ;; (%charterm:assert-keyinfos secondary-keyinfos)
+    (charterm-keyset keyset-id
+                     primary-keyinfos
+                     secondary-keyinfos)))
+
+(doc (defthing charterm-ascii-keyset charterm-keyset?
+       (para "From the old ["
+             (tech "ASCII")
+             "] standard.  When defining a "
+             (tech "keydec")
+             ", this is good to have as a final keyset, after the others.")))
+(define charterm-ascii-keyset
+  (let ((keylangs
+         `(("(0)"   "NUL"       nul             null)
+           ("(1)"   "Ctrl-A"    ctrl-a          start-of-heading soh)
+           ("(2)"   "Ctrl-B"    ctrl-b          start-of-text stx)
+           ("(3)"   "Ctrl-C"    ctrl-c          end-of-text etx)
+           ("(4)"   "Ctrl-D"    ctrl-d          end-of-transmission eot)
+           ("(5)"   "Ctrl-E"    ctrl-e          enquiry enq)
+           ("(6)"   "Ctrl-F"    ctrl-f          acknowledge ack)
+           ("(7)"   "Ctrl-G"    ctrl-g          bell bel)
+           ("(8)"   "Backspace" backspace       ctrl-h bs)
+           ("(9)"   "Tab"       tab             ctrl-i horizontal-tab ht)
+           ("(10)"  "Linefeed"  linefeed        ctrl-j line-feed lf)
+           ("(11)"  "Ctrl-K"    ctrl-k          vertical-tab vt)
+           ("(12)"  "Ctrl-L"    ctrl-l          formfeed form-feed ff)
+           ("(13)"  "Return"    return          ctrl-m carriage-return cr)
+           ("(14)"  "Ctrl-N"    ctrl-n          shift-out so)
+           ("(15)"  "Ctrl-O"    ctrl-o          shift-in si)
+           ("(16)"  "Ctrl-P"    ctrl-p          data-link-escape dle)
+           ("(17)"  "Ctrl-Q"    ctrl-q          device-control-1 dc1)
+           ("(18)"  "Ctrl-R"    ctrl-r          device-control-2 dc2)
+           ("(19)"  "Ctrl-S"    ctrl-s          device-control-3 dc3)
+           ("(20)"  "Ctrl-T"    ctrl-t          device-control-4 dc4)
+           ("(21)"  "Ctrl-U"    ctrl-u          negative-acknowledgement nak)
+           ("(22)"  "Ctrl-V"    ctrl-v          synchronous-idle syn)
+           ("(23)"  "Ctrl-W"    ctrl-w          end-of-transmission-block etb)
+           ("(24)"  "Ctrl-X"    ctrl-x          cancel can)
+           ("(25)"  "Ctrl-Y"    ctrl-y          end-of-medium em)
+           ("(26)"  "Ctrl-Z"    ctrl-z          substitute sub)
+           ("(27)"  "Esc"       escape          esc)
+           ("(28)"  "FS"        file-separator  fs)
+           ("(29)"  "GS"        group-separator gs)
+           ("(30)"  "RS"        record-separtor rs)
+           ("(31)"  "US"        unit-separator  us)
+           ("(32)"  "Space"     #\space         space sp)
+           ("(127)" "Delete"    delete          del)
+           ,@(for/list ((n (in-range 32 127)))
+               (let ((c (integer->char n)))
+                 (list (string-append "(" (number->string n) ")")
+                       (string c)
+                       c))))))
+    (make-charterm-keyset-from-keylangs
+     'ascii
+     keylangs
+     keylangs)))
+
+(doc (defthing charterm-dec-vt100-keyset charterm-keyset?
+       (para "From the DEC VT100.  This currently defines the four function
+keys (labeled on the keyboard, "
+             (bold "PF1")
+             " through "
+             (bold "PF4")
+             ") as "
+             (racket 'f1)
+             " through "
+             (racket 'f4)
+             ", and the arrow keys.  ["
+             (tech "VT100-UG")
+             "] and ["
+             (tech "PowerTerm")
+             "] were used as references.")))
+(provide charterm-dec-vt100-keyset)
+(define  charterm-dec-vt100-keyset
+  (make-charterm-keyset-from-keylangs
+   'dec-vt100
+   '(("esc O P" "PF1" f1)
+     ("esc O Q" "PF2" f2)
+     ("esc O R" "PF3" f3)
+     ("esc O S" "PF4" f4)
+
+     ("esc [ A" up)
+     ("esc [ B" down)
+     ("esc [ C" right)
+     ("esc [ D" left)
+     
+     ;; Note: PowerTerm does not map PC key F1 like VT100, etc.  It maps all
+     ;; the PC F keys to other sequences that are like the VT220.
+     )))
+
+(doc (defthing charterm-dec-vt220-keyset charterm-keyset?
+       (para "From the DEC VT220.  This currently defines function keys "
+             (bold "F1")
+             " through "
+             (bold "F20")
+             ".")))
+(provide charterm-dec-vt220-keyset)
+(define  charterm-dec-vt220-keyset
+  (make-charterm-keyset-from-keylangs
+   'dec-vt220
+   '(
+     ("esc [ 1 1 ~" f1)
+     ("esc [ 1 2 ~" f2)
+     ("esc [ 1 3 ~" f3)
+     ("esc [ 1 4 ~" f4)
+     ("esc [ 1 5 ~" f5)
+     ("esc [ 1 7 ~" f6)
+     ("esc [ 1 8 ~" f7)
+     ("esc [ 1 9 ~" f8)
+     ("esc [ 2 0 ~" f9)
+     ("esc [ 2 1 ~" f10)
+     ("esc [ 2 3 ~" f11)
+     ("esc [ 2 4 ~" f12)
+     ("esc [ 2 5 ~" f13)
+     ("esc [ 2 6 ~" f14)
+     ("esc [ 2 8 ~" f15)
+     ("esc [ 2 9 ~" f16)
+     ("esc [ 3 1 ~" f17)
+     ("esc [ 3 2 ~" f18)
+     ("esc [ 3 3 ~" f19)
+     ("esc [ 3 4 ~" f20)
+
+     ;; TODO: Make the keylang expand to both "esc [" and "(155)" CSI or
+     ;; whatever.
+     
+     ("(155) 1 1 ~" f1)
+     ("(155) 1 2 ~" f2)
+     ("(155) 1 3 ~" f3)
+     ("(155) 1 4 ~" f4)
+     ("(155) 1 5 ~" f5)
+     ("(155) 1 7 ~" f6)
+     ("(155) 1 8 ~" f7)
+     ("(155) 1 9 ~" f8)
+     ("(155) 2 0 ~" f9)
+     ("(155) 2 1 ~" f10)
+     ("(155) 2 3 ~" f11)
+     ("(155) 2 4 ~" f12)
+     ("(155) 2 5 ~" f13)
+     ("(155) 2 6 ~" f14)
+     ("(155) 2 8 ~" f15)
+     ("(155) 2 9 ~" f16)
+     ("(155) 3 1 ~" f17)
+     ("(155) 3 2 ~" f18)
+     ("(155) 3 3 ~" f19)
+     ("(155) 3 4 ~" f20)
+
+     )))
+
+(doc (defthing charterm-screen-keyset charterm-keyset?
+       (para "From the "
+             (hyperlink "http://en.wikipedia.org/wiki/GNU_Screen"
+                        "GNU Screen")
+             " terminal multiplexer, according to ["
+             (tech "Gregory")
+             "].  Also used by "
+             (hyperlink "http://en.wikipedia.org/wiki/Tmux"
+                        (code "tmux"))
+             ".")))
+(provide charterm-screen-keyset)
+(define  charterm-screen-keyset
+  (make-charterm-keyset-from-keylangs
+   'screen
+   '(("esc O P"     f1)
+     ("esc O Q"     f2)
+     ("esc O R"     f3)
+     ("esc O S"     f4)
+     ("esc [ 1 5 ~" f5)
+     ("esc [ 1 7 ~" f6)
+     ("esc [ 1 8 ~" f7)
+     ("esc [ 1 9 ~" f8)
+     ("esc [ 2 0 ~" f9)
+     ("esc [ 2 1 ~" f10)
+     ("esc [ 2 3 ~" f11)
+     ("esc [ 2 4 ~" f12)
+
+     ("esc [ 3 ~" "Delete" delete del)
+     ("esc [ 7 ~" "Home" home)
+     ("esc [ 8 ~" "End"  end)
+     
+     ("(127)" "Backspace" backspace)
+     )))
+
+(doc (defthing charterm-linux-keyset charterm-keyset?
+       (para "From the Linux console.  Currently defines function keys "
+             (bold "F1")
+             " through "
+             (bold "F5")
+             " only, since the rest will be inherited from other keysets.")))
+(provide charterm-linux-keyset)
+(define charterm-linux-keyset
+  (make-charterm-keyset-from-keylangs
+   'linux
+   '(("esc [ [ A" f1)
+     ("esc [ [ B" f2)
+     ("esc [ [ C" f3)
+     ("esc [ [ D" f4)
+     ("esc [ [ E" f5))))
+
+(doc (defthing charterm-xterm-x11r6-keyset charterm-keyset?
+       (para "From the XTerm in X11R6, according to ["
+             (tech "Gregory")
+             "].")))
+(provide charterm-xterm-x11r6-keyset)
+(define  charterm-xterm-x11r6-keyset
+  (make-charterm-keyset-from-keylangs
+   'xterm-x11r6
+   '(("esc [ 1 1 ~"     f1)
+     ("esc [ 1 2 ~"     f2)
+     ("esc [ 1 3 ~"     f3)
+     ("esc [ 1 4 ~"     f4)
+     ("esc [ 1 5 ~"     f5)
+     ("esc [ 1 7 ~"     f6)
+     ("esc [ 1 8 ~"     f7)
+     ("esc [ 1 9 ~"     f8)
+     ("esc [ 2 0 ~"     f9)
+     ("esc [ 2 1 ~"     f10)
+     ("esc [ 2 3 ~"     f11)
+     ("esc [ 2 4 ~"     f12)
+     ("esc [ 1 1 ; 2 ~" f13)
+     ("esc [ 1 2 ; 2 ~" f14)
+     ("esc [ 1 3 ; 2 ~" f15)
+     ("esc [ 1 4 ; 2 ~" f16)
+     ("esc [ 1 5 ; 2 ~" f17)
+     ("esc [ 1 7 ; 2 ~" f18)
+     ("esc [ 1 8 ; 2 ~" f19)
+     ("esc [ 1 9 ; 2 ~" f20)
+     ("esc [ 2 0 ; 2 ~" f21)
+     ("esc [ 2 1 ; 2 ~" f22)
+     ("esc [ 2 3 ; 2 ~" f23)
+     ("esc [ 2 4 ; 2 ~" f24)
+     ("esc [ 1 1 ; 5 ~" f25)
+     ("esc [ 1 2 ; 5 ~" f26)
+     ("esc [ 1 3 ; 5 ~" f27)
+     ("esc [ 1 4 ; 5 ~" f28)
+     ("esc [ 1 5 ; 5 ~" f29)
+     ("esc [ 1 7 ; 5 ~" f30)
+     ("esc [ 1 8 ; 5 ~" f31)
+     ("esc [ 1 9 ; 5 ~" f32)
+     ("esc [ 2 0 ; 5 ~" f33)
+     ("esc [ 2 1 ; 5 ~" f34)
+     ("esc [ 2 3 ; 5 ~" f35)
+     ("esc [ 2 4 ; 5 ~" f36)
+     ("esc [ 1 1 ; 6 ~" f37)
+     ("esc [ 1 2 ; 6 ~" f38)
+     ("esc [ 1 3 ; 6 ~" f39)
+     ("esc [ 1 4 ; 6 ~" f40)
+     ("esc [ 1 5 ; 6 ~" f41)
+     ("esc [ 1 7 ; 6 ~" f42)
+     ("esc [ 1 8 ; 6 ~" f43)
+     ("esc [ 1 9 ; 6 ~" f44)
+     ("esc [ 2 0 ; 6 ~" f45)
+     ("esc [ 2 1 ; 6 ~" f46)
+     ("esc [ 2 3 ; 6 ~" f47)
+     ("esc [ 2 4 ; 6 ~" f48))))
+
+(doc (defthing charterm-xterm-xfree86-keyset charterm-keyset?
+       (para "From the XFree86 XTerm, according to ["
+             (tech "Gregory")
+             "].")))
+(provide charterm-xterm-xfree86-keyset)
+(define  charterm-xterm-xfree86-keyset
+  (make-charterm-keyset-from-keylangs
+   'xterm-xfree86
+   '(("esc O P"         f1)
+     ("esc O Q"         f2)
+     ("esc O R"         f3)
+     ("esc O S"         f4)
+     ("esc [ 1 5 ~"     f5)
+     ("esc [ 1 7 ~"     f6)
+     ("esc [ 1 8 ~"     f7)
+     ("esc [ 1 9 ~"     f8)
+     ("esc [ 2 0 ~"     f9)
+     ("esc [ 2 1 ~"     f10)
+     ("esc [ 2 3 ~"     f11)
+     ("esc [ 2 4 ~"     f12)
+     ("esc O 2 P"       f13)
+     ("esc O 2 Q"       f14)
+     ("esc O 2 R"       f15)
+     ("esc O 2 S"       f16)
+     ("esc [ 1 5 ; 2 ~" f17)
+     ("esc [ 1 7 ; 2 ~" f18)
+     ("esc [ 1 8 ; 2 ~" f19)
+     ("esc [ 1 9 ; 2 ~" f20)
+     ("esc [ 2 0 ; 2 ~" f21)
+     ("esc [ 2 1 ; 2 ~" f22)
+     ("esc [ 2 3 ; 2 ~" f23)
+     ("esc [ 2 4 ; 2 ~" f24)
+     ("esc O 5 P"       f25)
+     ("esc O 5 Q"       f26)
+     ("esc O 5 R"       f27)
+     ("esc O 5 S"       f28)
+     ("esc [ 1 5 ; 5 ~" f29)
+     ("esc [ 1 7 ; 5 ~" f30)
+     ("esc [ 1 8 ; 5 ~" f31)
+     ("esc [ 1 9 ; 5 ~" f32)
+     ("esc [ 2 0 ; 5 ~" f33)
+     ("esc [ 2 1 ; 5 ~" f34)
+     ("esc [ 2 3 ; 5 ~" f35)
+     ("esc [ 2 4 ; 5 ~" f36)
+     ("esc O 6 P"       f37)
+     ("esc O 6 Q"       f38)
+     ("esc O 6 R"       f39)
+     ("esc O 6 S"       f40)
+     ("esc [ 1 5 ; 6 ~" f41)
+     ("esc [ 1 7 ; 6 ~" f42)
+     ("esc [ 1 8 ; 6 ~" f43)
+     ("esc [ 1 9 ; 6 ~" f44)
+     ("esc [ 2 0 ; 6 ~" f45)
+     ("esc [ 2 1 ; 6 ~" f46)
+     ("esc [ 2 3 ; 6 ~" f47)
+     ("esc [ 2 4 ; 6 ~" f48))))
+
+(doc (defthing charterm-xterm-new-keyset charterm-keyset?
+       (para "From the current "
+             (code "xterm-new")
+             ", often called simply "
+             (code "xterm")
+             ", as developed by Thomas E. Dickey, and documented in ["
+             (tech "XTerm-ctlseqs")
+             "].  Several also came from decompiling a "
+             (code "terminfo")
+             " entry.  Thanks to Dickey for his emailed help.")))
+(provide charterm-xterm-new-keyset)
+(define  charterm-xterm-new-keyset
+  (make-charterm-keyset-from-keylangs
+   'xterm-new
+   '(
+
+     ;; CSI = "esc ["
+     ;; SS3 = "esc O"
+
+     ("esc [ A" up)
+     ("esc [ B" down)
+     ("esc [ C" right)
+     ("esc [ D" left)
+     ("esc [ H" home)
+     ("esc [ F" end)
+
+     ;; The following came from decompiling an xterm terminfo
+     ("esc O A" up)
+     ("esc O B" down)
+     ("esc O C" right)
+     ("esc O D" left)
+     ("esc O H" home)
+     ("esc O F" end)
+
+     ("esc O P" f1)
+     ("esc O Q" f2)
+     ("esc O R" f3)
+     ("esc O S" f4)
+     ("esc [ 1 5 ~" f5)
+     ("esc [ 1 7 ~" f6)
+     ("esc [ 1 8 ~" f7)
+     ("esc [ 1 9 ~" f8)
+     ("esc [ 2 0 ~" f9)
+     ("esc [ 2 1 ~" f10)
+     ("esc [ 2 3 ~" f11)
+     ("esc [ 2 4 ~" f12)
+
+     ("esc O I" tab kp-tab)
+     ("esc O M" "Enter" return enter kp-return kp-enter)
+     ("esc O P" "PF1" f1 kp-f1)
+     ("esc O Q" "PF2" f2 kp-f2)
+     ("esc O R" "PF3" f3 kp-f3)
+     ("esc O S" "PF4" f4 kp-f4)
+     ("esc [ 3 ~" "Delete" delete del kp-delete)
+     ("esc [ 2 ~" "Insert" insert ins kp-insert)
+     ("esc O F"   "End" end kp-end)
+     ("esc [ B" "Down" down kp-down)
+     ("esc [ 6 ~" "PgDn" pgdn kp-pgdn)
+     ("esc [ D" "Left" left kp-left)
+     ("esc [ E" "Begin" begin kp-begin)
+     ("esc [ C" "Right" right kp-right)
+     ("esc O H" "Home" home kp-home)
+     ("esc [ A" "Up" up kp-up)
+     ("esc [ 5 ~" "PgUp" pgup kp-pgup)
+
+     ("esc [ 1 1 ~" "F1" f1)
+     ("esc [ 1 2 ~" "F2" f2)
+     ("esc [ 1 3 ~" "F3" f3)
+     ("esc [ 1 4 ~" "F4" f4)
+
+     ;; TODO: continue working on this from dickey's xterm control sequences doc
+
+     )))
+
+(doc (defthing charterm-rxvt-keyset charterm-keyset?
+       (para "From the "
+             (hyperlink "http://en.wikipedia.org/wiki/Rxvt"
+                        (code "rxvt"))
+             " terminal emulator.  These come from ["
+             (tech "Gregory")
+             "], and
+currently define function keys "
+             (racket 'f1)
+             " through "
+             (racket 'f44)
+             ".")))
+(define charterm-rxvt-keyset
+  (make-charterm-keyset-from-keylangs
+   'rxvt
+   '(("esc [ 1 1 ~" f1)
+     ("esc [ 1 2 ~" f2)
+     ("esc [ 1 3 ~" f3)
+     ("esc [ 1 4 ~" f4)
+     ("esc [ 1 5 ~" f5)
+     ("esc [ 1 7 ~" f6)
+     ("esc [ 1 8 ~" f7)
+     ("esc [ 1 9 ~" f8)
+     ("esc [ 2 0 ~" f9)
+     ("esc [ 2 1 ~" f10)
+     ("esc [ 2 3 ~" shift-f1  f11) ;; TODO: These shift- and ctrl- are actually from termvar xterm in an rxvt
+     ("esc [ 2 4 ~" shift-f2  f12)
+     ("esc [ 2 5 ~" shift-f3  f13)
+     ("esc [ 2 6 ~" shift-f4  f14)
+     ("esc [ 2 8 ~" shift-f5  f15)
+     ("esc [ 2 9 ~" shift-f6  f16)
+     ("esc [ 3 1 ~" shift-f7  f17)
+     ("esc [ 3 2 ~" shift-f8  f18)
+     ("esc [ 3 3 ~" shift-f9  f19)
+     ("esc [ 3 4 ~" shift-f10 f20)
+     ("esc [ 2 3 $" shift-f11 f21)
+     ("esc [ 2 4 $" shift-f12 f22)
+     ("esc [ 1 1 ^" ctrl-f1   f23)
+     ("esc [ 1 2 ^" ctrl-f2   f24)
+     ("esc [ 1 3 ^" ctrl-f3   f25)
+     ("esc [ 1 4 ^" ctrl-f4   f26)
+     ("esc [ 1 5 ^" ctrl-f5   f27)
+     ("esc [ 1 7 ^" ctrl-f6   f28)
+     ("esc [ 1 8 ^" ctrl-f7   f29)
+     ("esc [ 1 9 ^" ctrl-f8   f30)
+     ("esc [ 2 0 ^" ctrl-f9   f31)
+     ("esc [ 2 1 ^" ctrl-f10  f32)
+     ("esc [ 2 3 ^" ctrl-f11  f33)
+     ("esc [ 2 4 ^" ctrl-f12  f34)
+     ("esc [ 2 5 ^" f35)
+     ("esc [ 2 6 ^" f36)
+     ("esc [ 2 8 ^" f37)
+     ("esc [ 2 9 ^" f38)
+     ("esc [ 3 1 ^" f39)
+     ("esc [ 3 2 ^" f40)
+     ("esc [ 3 3 ^" f41)
+     ("esc [ 3 4 ^" f42)
+     ("esc [ 2 3 @" f43)
+     ("esc [ 2 4 @" f44)
+     ("(127)" "Backspace" backspace) ; Override one from "ascii" keyset.
+     ;; TODO: actually, these arrow keys were observed in rxvt with termvar xterm.  which keyset should they be in?
+     ("esc [ A"     "Up" up)
+     ("esc [ B"     "Down" down)
+     ("esc [ C"     "Right" right)
+     ("esc [ D"     "Left" left)
+     ("esc [ 5 ~"   "PgUp" pgup page-up)
+     ("esc [ 6 ~"   "PgDn" pgdn page-down)
+     ("esc [ 7 ~"   "Home" home)
+     ("esc [ 8 ~"   "End"  end)
+     ("esc [ 3 ~"   "Delete" delete del)
+     ("esc [ 2 ~"   "Insert" insert ins)
+     )))
+
+(doc (defthing charterm-wyse-wy50-keyset charterm-keyset?
+       (para "From the Wyse WY-50, based on ["
+             (tech "WY-50-QRG")
+             "] and looking at photos of WY-50 keyboard, and tested in ["
+             (tech "wy60")
+             "] and ["
+             (tech "PowerTerm")
+             "].  The shifted function keys are provided as both "
+             (racket 'shift-f1)
+             " through "
+             (racket 'shift-16)
+             ", and "
+             (racket 'f17)
+             " through "
+             (racket 'f31)
+             ".")))
+(provide charterm-wyse-wy50-keyset)
+(define  charterm-wyse-wy50-keyset
+  (make-charterm-keyset-from-keylangs
+   'wyse-wy50
+   '(("^a @ cr"  f1)
+     ("^a A cr"  f2)
+     ("^a B cr"  f3)
+     ("^a C cr"  f4)
+     ("^a D cr"  f5)
+     ("^a E cr"  f6)
+     ("^a F cr"  f7)
+     ("^a G cr"  f8)
+     ("^a H cr"  f9)
+     ("^a I cr"  f10)
+     ("^a J cr"  f11)
+     ("^a K cr"  f12)
+     ("^a L cr"  f13)
+     ("^a M cr"  f14)
+     ("^a N cr"  f15)
+     ("^a O cr"  f16)
+     ("^a ` cr"  "Shift-F1" shift-f1 f17)
+     ("^a a cr"  "Shift-F2" shift-f2 f18)
+     ("^a b cr"  "Shift-F3" shift-f3 f19)
+     ("^a c cr"  "Shift-F4" shift-f4 f20)
+     ("^a d cr"  "Shift-F5" shift-f5 f21)
+     ("^a e cr"  "Shift-F6" shift-f6 f22)
+     ("^a f cr"  "Shift-F7" shift-f7 f23)
+     ("^a g cr"  "Shift-F8" shift-f8 f24)
+     ("^a h cr"  "Shift-F9" shift-f9 f25)
+     ("^a i cr"  "Shift-F10" shift-f10 f26)
+     ("^a j cr"  "Shift-F11" shift-f11 f27)
+     ("^a k cr"  "Shift-F12" shift-f12 f28)
+     ("^a l cr"  "Shift-F13" shift-f13 f29)
+     ("^a m cr"  "Shift-F14" shift-f14 f30)
+     ("^a n cr"  "Shift-F15" shift-f15 f31)
+     ("^a o cr"  "Shift-F16" shift-f16 f32)
+     ("ctrl-h"   "Left" left)
+     ("linefeed" "Down" down)
+     ("(11)"     "Up" up)
+     ("(12)"     "Right" right)
+     ("esc W"    "DEL Char" delete)
+     ("esc Q"    "INS Char" insert-char)
+     ("esc q"    "Ins" insert ins)
+     ("esc T"    "CLR Line" clear-line)
+     ("esc r"    "Repl" repl)
+     ("esc R"    "DEL Line" delete-line)
+     ("esc J"    "PAGE Prev" pgup page-up)
+     ("esc K"    "PAGE Next" pgdn page-down)
+     ("esc P"    "Print" print)
+     ("esc Y"    "CLR Screen" clear-screen)
+     ("(30)"     "Home" home record-separator rs)
+     ("(13)"     "Return" return)
+     ("(127)"    "Shift-Backspace" backspace shift-backspace)
+     )))
+
+(doc (defthing charterm-televideo-925-keyset charterm-keyset?
+       (para "From the TeleVideo 925, based on ["
+             (tech "TVI-925-IUG")
+             "], ["
+             (tech "PowerTerm")
+             "], and from looking at a TeleVideo 950 keyboard.")))
+(provide charterm-televideo-925-keyset charterm-keyset?)
+(define  charterm-televideo-925-keyset
+  (make-charterm-keyset-from-keylangs
+   'televideo-925
+   '(("ctrl-a @ cr" f1)
+     ("ctrl-a A cr" f2)
+     ("ctrl-a B cr" f3)
+     ("ctrl-a C cr" f4)
+     ("ctrl-a D cr" f5)
+     ("ctrl-a E cr" f6)
+     ("ctrl-a F cr" f7)
+     ("ctrl-a G cr" f8)
+     ("ctrl-a H cr" f9)
+     ("ctrl-a I cr" f10)
+     ("ctrl-a J cr" f11)
+
+     ("ctrl-a \\ cr" "SHIFT-F1" shift-f1)
+     ("ctrl-a a cr" "SHIFT-F2" shift-f2)
+     ("ctrl-a b cr" "SHIFT-F3" shift-f3)
+     ("ctrl-a c cr" "SHIFT-F4" shift-f4)
+     ("ctrl-a d cr" "SHIFT-F5" shift-f5)
+     ("ctrl-a e cr" "SHIFT-F6" shift-f6)
+     ("ctrl-a f cr" "SHIFT-F7" shift-f7)
+     ("ctrl-a g cr" "SHIFT-F8" shift-f8)
+     ("ctrl-a h cr" "SHIFT-F9" shift-f9)
+     ("ctrl-a i cr" "SHIFT-F10" shift-f10)
+     ("ctrl-a j cr" "SHIFT-F11" shift-f11)
+
+     ("ctrl-k" "Up" up ctrl-k)
+     ("ctrl-v" "Down" down ctrl-v)
+     ("ctrl-h" "Left" left ctrl-h)
+     ("ctrl-l" "Right" right ctrl-l)
+
+     ("esc W" "CHAR DELETE" delete del char-delete)
+
+     ("esc Q" "CHAR INSERT" insert ins char-insert)
+
+     ("esc j" "Reverse Linefeed" reverse-linefeed reverse-lf reverse-line-feed)
+
+     ("esc i" "BACK TAB" backtab back-tab)
+     ("ctrl-m" "RETURN" return ctrl-m)
+     ("ctrl-j" "LINEFEED" linefeed lf ctrl-j)
+     ("(127)" "DEL" delete del)
+     ;; ("esc Q" "CHAR INSERT" char-insert char-ins)
+
+     )))
+
+(doc (subsubsection "Keydec")
+
+     (para "A "
+           (deftech "keydec")
+           " object is a key decoder for a specific variety of terminal, such
+as for a specific "
+           (tech "termvar")
+           ".  A keydec is used to turn received key encodings from a terminal into "
+           (tech "keycode")
+           " or "
+           (tech "keyinfo")
+           " values.  A keydec is constructed from a prioritized list of "
+           (tech "keyset")
+           " objects, with earlier-listed keysets taking priority of
+later-listed keysets when there is conflict between them as to how to decode a
+particular byte sequence."))
+
+(define (%charterm:make-keytree (alist '()))
+  (make-immutable-hasheqv alist))
+
+(define (%charterm:keytree-add-keyinfo-if-can keytree keyinfo)
+  (let ((bytelist (charterm-keyinfo-bytelist keyinfo)))
+    (let loop-bytelist ((this-byte  (car bytelist))
+                        (rest-bytes (cdr bytelist))
+                        (node       keytree))
+      (cond ((hash? node)
+             (cond ((hash-ref node this-byte #f)
+                    => (lambda (existing-sub-node)
+                         ;; Node has a match for this byte, so do we have another
+                         ;; byte and can follow it?
+                         (if (null? rest-bytes)
+                             ;; Node has a match for this byte, but we have no
+                             ;; more bytes, so can't add.
+                             node
+                             ;; Node has a match for this byte, and we have more
+                             ;; bytes, so follow it.
+                             (hash-set node
+                                       this-byte
+                                       (loop-bytelist (car rest-bytes)
+                                                      (cdr rest-bytes)
+                                                      existing-sub-node)))))
+                   (else
+                    ;; Node has no match for this byte, so add new path.
+                    (hash-set node
+                              this-byte
+                              (let loop ((rest-bytes rest-bytes))
+                                (if (null? rest-bytes)
+                                    keyinfo
+                                    (%charterm:make-keytree
+                                     (cons (cons (car rest-bytes)
+                                                 (loop (cdr rest-bytes)))
+                                           '()))))))))
+
+            ((charterm-keyinfo? node)
+             ;; Node is already a keyinfo, so can't add.
+             node)
+            (else (error
+                   '%charterm:keytree-add-keyinfo-if-can
+                   "invalid node ~S with this-byte ~S, rest-bytes ~S, keyinfo ~S"
+                   node
+                   this-byte
+                   rest-bytes
+                   keyinfo))))))
+
+(define (%charterm:keytree-add-any-keyinfos-can keytree keyinfos)
+  (let loop ((keyinfos keyinfos)
+             (keytree  keytree))
+    (if (null? keyinfos)
+        keytree
+        (loop (cdr keyinfos)
+              (%charterm:keytree-add-keyinfo-if-can keytree
+                                                    (car keyinfos))))))
+
+(define (%charterm:make-keytree-from-keyinfoses keyinfoses)
+  (let loop ((keyinfoses keyinfoses)
+             (keytree (%charterm:make-keytree)))
+    (if (null? keyinfoses)
+        keytree
+        (let ((keyinfos (car keyinfoses)))
+          ;; (and (not (null? keyinfos))
+          ;;      (not (charterm-keyinfo? (car keyinfos)))
+          ;;      (error '%charterm:make-keytree-from-keyinfoses
+          ;;             "bad keyinfos: ~S"
+          ;;             keyinfos))
+          (loop (cdr keyinfoses)
+                (%charterm:keytree-add-any-keyinfos-can keytree
+                                                        keyinfos))))))
+
+(doc (defproc (charterm-keydec-id (kd charterm-keydec?))
+         symbol?
+       (para "Gets the ID symbol of the "
+             (tech "keydec")
+             " being used.")))
+(provide charterm-keydec-id)
+
+(struct charterm-keydec
+  (id
+   primary-keytree
+   secondary-keytree)
+  #:transparent)
+
+(define (charterm-make-keydec keydec-id . keysets)
+  (charterm-keydec keydec-id
+                   (%charterm:make-keytree-from-keyinfoses
+                    (map charterm-keyset-primary-keyinfos keysets))
+                   (%charterm:make-keytree-from-keyinfoses
+                    (map charterm-keyset-secondary-keyinfos keysets))))
+
+(doc (subsubsub*section "ANSI Keydecs"))
+
+(doc (defthing charterm-vt100-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "vt100")
+             ".")))
+(provide charterm-vt100-keydec)
+(define  charterm-vt100-keydec
+  (charterm-make-keydec 'vt100
+                        charterm-dec-vt100-keyset
+                        charterm-dec-vt220-keyset
+                        charterm-xterm-new-keyset
+                        charterm-linux-keyset
+                        charterm-rxvt-keyset
+                        charterm-xterm-xfree86-keyset
+                        charterm-xterm-x11r6-keyset
+                        charterm-ascii-keyset))
+
+(doc (defthing charterm-vt220-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "vt220")
+             ".")))
+(provide charterm-vt220-keydec)
+(define  charterm-vt220-keydec
+  (charterm-make-keydec 'vt220
+                        charterm-dec-vt220-keyset
+                        charterm-dec-vt100-keyset
+                        charterm-ascii-keyset))
+
+(doc (defthing charterm-screen-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "screen")
+             ".")))
+(provide charterm-screen-keydec)
+(define  charterm-screen-keydec
+  (charterm-make-keydec 'screen
+                        charterm-screen-keyset
+                        charterm-linux-keyset
+                        charterm-dec-vt220-keyset
+                        charterm-dec-vt100-keyset
+                        charterm-xterm-new-keyset
+                        charterm-xterm-xfree86-keyset
+                        charterm-xterm-x11r6-keyset
+                        charterm-ascii-keyset))
+
+(doc (defthing charterm-linux-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "linux")
+             ".")))
+(provide charterm-linux-keydec)
+(define  charterm-linux-keydec
+  (charterm-make-keydec 'linux
+                        charterm-linux-keyset
+                        charterm-dec-vt220-keyset
+                        charterm-dec-vt100-keyset
+                        charterm-xterm-new-keyset
+                        charterm-xterm-xfree86-keyset
+                        charterm-xterm-x11r6-keyset
+                        charterm-screen-keyset
+                        charterm-ascii-keyset))
+
+(doc (defthing charterm-xterm-new-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "xterm-new")
+             ".")))
+(provide charterm-xterm-new-keydec)
+(define  charterm-xterm-new-keydec
+  (charterm-make-keydec 'xterm-new
+                        charterm-xterm-new-keyset
+                        charterm-xterm-xfree86-keyset
+                        charterm-xterm-x11r6-keyset
+                        charterm-rxvt-keyset
+                        charterm-dec-vt220-keyset
+                        charterm-dec-vt100-keyset
+                        charterm-linux-keyset
+                        charterm-ascii-keyset))
+
+(doc (defthing charterm-xterm-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "xterm")
+             ".  Currently same as the keydec for "
+             (code "xterm")
+             ", except for a different ID.")))
+(provide charterm-xterm-keydec)
+(define  charterm-xterm-keydec
+  (charterm-make-keydec 'xterm
+                        charterm-xterm-new-keyset
+                        charterm-xterm-xfree86-keyset
+                        charterm-xterm-x11r6-keyset
+                        charterm-rxvt-keyset
+                        charterm-dec-vt220-keyset
+                        charterm-dec-vt100-keyset
+                        charterm-linux-keyset
+                        charterm-ascii-keyset))
+
+(doc (defthing charterm-rxvt-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "rxvt")
+             ".")))
+(provide charterm-rxvt-keydec)
+(define  charterm-rxvt-keydec
+  (charterm-make-keydec 'rxvt
+                        charterm-rxvt-keyset
+                        charterm-xterm-new-keyset
+                        charterm-xterm-xfree86-keyset
+                        charterm-xterm-x11r6-keyset
+                        charterm-dec-vt220-keyset
+                        charterm-dec-vt100-keyset
+                        charterm-linux-keyset
+                        charterm-ascii-keyset))
+
+(doc (subsubsub*section "Wyse Keydecs"))
+
+(doc (defthing charterm-wy50-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "wy50")
+             ".")))
+(provide charterm-wy50-keydec)
+(define  charterm-wy50-keydec
+  (charterm-make-keydec 'wy50
+                        charterm-wyse-wy50-keyset
+                        charterm-ascii-keyset))
+
+(doc (subsubsub*section "TeleVideo Keydecs"))
+
+(doc (defthing charterm-tvi925-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "tvi925")
+             ".")))
+(provide charterm-tvi925-keydec)
+(define  charterm-tvi925-keydec
+  (charterm-make-keydec 'tvi925
+                        charterm-televideo-925-keyset
+                        charterm-ascii-keyset))
+
+(doc (subsubsub*section "ASCII Keydecs"))
+
+(doc (defthing charterm-ascii-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for "
+             (tech "termvar")
+             " "
+             (racket "ascii")
+             ".")))
+(provide charterm-ascii-keydec)
+(define  charterm-ascii-keydec
+  (charterm-make-keydec 'ascii
+                        charterm-ascii-keyset))
+
+(doc (subsubsub*section "Default Keydecs"))
+
+(doc (defthing charterm-ansi-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for any presumed ANSI-ish terminal, combining many ANSI-ish "
+             (tech "keysets")
+             ".")))
+(define charterm-ansi-keydec
+  (charterm-make-keydec 'ansi
+                        charterm-dec-vt220-keyset
+                        charterm-dec-vt100-keyset
+                        charterm-xterm-new-keyset
+                        charterm-linux-keyset
+                        charterm-rxvt-keyset
+                        charterm-xterm-xfree86-keyset
+                        charterm-xterm-x11r6-keyset
+                        charterm-ascii-keyset))
+
+(doc (defthing charterm-insane-keydec charterm-keydec?
+       (para (tech "Keydec")
+             " for the uniquely desperate situation of wanting to possibly have
+extensive key decoding for a terminal that might not even be ansi, but be
+Wyse, TeleVideo, or some other ASCII.")))
+(provide charterm-insane-keydec)
+(define  charterm-insane-keydec
+  (charterm-make-keydec 'insane
+                        charterm-xterm-new-keyset
+                        charterm-linux-keyset
+                        charterm-dec-vt220-keyset
+                        charterm-dec-vt100-keyset
+                        charterm-linux-keyset
+                        charterm-xterm-xfree86-keyset
+                        charterm-xterm-x11r6-keyset
+                        charterm-wyse-wy50-keyset
+                        charterm-televideo-925-keyset
+                        charterm-ascii-keyset))
+
+(doc (subsection "Termvar")
+
+     (para "A "
+           (deftech "termvar")
+           " is what the "
+           (code "charterm")
+           " package calls the value of the Unix-like "
+           (code "TERM")
+           " environment variable.  Each "
+           (tech "termvar")
+           " has a default "
+           (tech "protocol")
+           " and "
+           (tech "keydec")
+           ".  Note, however, that "
+           (code "TERM")
+           " is not always a precise indicator of the best protocol and keydec,
+but by default we work with what we have."))
+
+;; TODO: Document the termvars here?  Move this subsection?
+
+(doc (section (code "charterm") " Object")
+
+     (para "The "
+           (racket charterm)
+           " object captures the state of a session with a particular terminal.")
+
+     (para "A "
+           (racket charterm)
+           " object is also a synchronizable event, so it can be used with
+procedures such as "
+           (racket sync)
+           ".  As an event, it becomes ready when there is at least one byte
+available for reading from the terminal, and its synchronization result is
+itself."))
+
+(doc (defproc (charterm? (x any/c))
+         boolean?
+       (para "Predicate for whether or not "
+             (var x)
+             " is a "
+             (racket charterm)
+             ".")))
+(provide charterm?)
+
+(doc (defproc (charterm-termvar (ct charterm?))
+         (or/c #f string?))
+     (para "Gets the "
+           (tech "termvar")
+           "."))
+(provide charterm-termvar)
+
+(doc (defproc (charterm-protocol (ct charterm?))
+         symbol?)
+     (para "Gets the "
+           (tech "protocol")
+           "."))
+(provide charterm-protocol)
+
+(doc (defproc (charterm-keydec (ct charterm?))
+         symbol?)
+     (para "Gets the "
+           (tech "keydec")
+           "."))
+(provide (rename-out (charterm-keydec* charterm-keydec)))
+
+(define-struct charterm
+  (tty
+   in
+   out
+   evt
+   buf-size
+   buf
+   (buf-start #:mutable)
+   (buf-end #:mutable)
+   termvar
+   protocol
+   keydec*
+   (screensize #:mutable))
+  #:property prop:evt (struct-field-index evt))
+
+(define (%charterm:protocol-unimplemented error-name ct)
+  (error error-name
+         "protocol unimplemented: ~S"
+         (charterm-protocol ct)))
+
+(define (%charterm:protocol-unreachable error-name ct)
+  (error error-name
+         "internal error: protocol unreachable: ~S"
+         (charterm-protocol ct)))
+
+(define %charterm:stty-minus-f-arg-string
+  (case (system-type 'os)
+    ((macosx) "-f")
+    (else     "-F")))
+  
+(doc (defparam current-charterm ct (or/c #f charterm?)
+       (para "This parameter provides the default "
+             (racket charterm)
+             " for most of the other procedures.  It is usually set automatically by "
+             (racket call-with-charterm)
+             ", "
+             (racket with-charterm)
+             ", "
+             (racket open-charterm)
+             ", and "
+             (racket close-charterm)
+             ".")))
+(provide current-charterm)
+(define current-charterm (make-parameter #f))
+
+(doc (defproc (open-charterm
+               (#:tty      tty      (or/c #f path-string?) #f)
+               (#:current? current? boolean?               #t))
+         charterm?
+       (para "Returns an open "
+             (racket charterm)
+             " object, by opening I/O ports on the terminal device at "
+             (racket tty)
+             " (or, if "
+             (racket #f)
+             ", file "
+             (filepath "/dev/tty")
+             "), and setting raw mode and disabling echo (via "
+             (filepath "/bin/stty")
+             ").  If "
+             (racket current?)
+             " is true, the "
+             (racket current-charterm)
+             " parameter is also set to this object.")))
+(provide open-charterm)
+(define (open-charterm #:tty      (tty      #f)
+                       #:current? (current? #t))
+  (let* ((tty (cleanse-path (or tty "/dev/tty")))
+         (tty-str  (path->string tty)))
+    (or (system* "/bin/stty"
+                 %charterm:stty-minus-f-arg-string
+                 tty-str
+                 "raw"
+                 "-echo")
+        (error 'open-charterm
+               "stty ~S failed"
+               tty-str))
+    (with-handlers ((exn:fail? (lambda (e)
+                                 (with-handlers ((exn:fail? void))
+                                   (system* "/bin/stty"
+                                            %charterm:stty-minus-f-arg-string
+                                            tty-str
+                                            "sane"))
+                                 (raise e))))
+      (let*-values (((in out)   (open-input-output-file tty
+                                                        #:exists 'update))
+                    ((buf-size) 2048))
+        ;; TODO: Do we actually need to turn off buffering?
+        (file-stream-buffer-mode in  'none)
+        (file-stream-buffer-mode out 'none)
+        (let*-values
+            (((termvar) (getenv "TERM"))
+             ((termvar) (cond ((not termvar) #f)
+                              ((equal? "" termvar) #f)
+                              (else (string-downcase termvar))))
+             ((protocol keydec)
+              ;; TODO: Once the patterns have been fleshed out, make the exact
+              ;; matches a hash, and optimize the regexps.
+              (cond ((not termvar) (values #f #f))
+                    ;; Exact Matches:
+                    ((equal? "ascii"     termvar) (values 'ascii         charterm-ascii-keydec))
+                    ((equal? "dumb"      termvar) (values 'ascii         charterm-ascii-keydec))
+                    ((equal? "linux"     termvar) (values 'ansi      charterm-linux-keydec))
+                    ((equal? "rxvt"      termvar) (values 'ansi      charterm-rxvt-keydec))
+                    ((equal? "screen"    termvar) (values 'ansi      charterm-screen-keydec))
+                    ((equal? "tvi925"    termvar) (values 'televideo-925 charterm-tvi925-keydec))
+                    ((equal? "tvi950"    termvar) (values 'televideo-925 charterm-tvi925-keydec))
+                    ((equal? "vt100"     termvar) (values 'ansi      charterm-vt100-keydec))
+                    ((equal? "vt102"     termvar) (values 'ansi      charterm-vt100-keydec))
+                    ((equal? "vt220"     termvar) (values 'ansi      charterm-vt220-keydec))
+                    ((equal? "wy50"      termvar) (values 'wyse-wy50     charterm-wy50-keydec))
+                    ((equal? "wy60"      termvar) (values 'wyse-wy50     charterm-wy50-keydec))
+                    ((equal? "wy75"      termvar) (values 'wyse-wy50     charterm-wy50-keydec))
+                    ((equal? "wyse50"    termvar) (values 'wyse-wy50     charterm-wy50-keydec))
+                    ((equal? "wyse60"    termvar) (values 'wyse-wy50     charterm-wy50-keydec))
+                    ((equal? "wyse75"    termvar) (values 'wyse-wy50     charterm-wy50-keydec))
+                    ((equal? "xterm"     termvar) (values 'ansi      charterm-xterm-new-keydec))
+                    ((equal? "xterm-new" termvar) (values 'ansi      charterm-xterm-new-keydec))
+                    ;; ANSI-ish Guesses:
+                    ((regexp-match #rx"ansi$"  termvar) (values 'ansi charterm-ansi-keydec))
+                    ((regexp-match #rx"^ansi"  termvar) (values 'ansi charterm-ansi-keydec))
+                    ((regexp-match #rx"^xterm" termvar) (values 'ansi charterm-xterm-new-keydec))
+                    ((regexp-match #rx"^rxvt"  termvar) (values 'ansi charterm-rxvt-keydec))
+                    ((regexp-match #rx"^vt"    termvar) (values 'ansi charterm-rxvt-keydec))
+                    ;; Non-ANSI Guesses:
+                    ((regexp-match #rx"^wy"  termvar) (values 'wyse-wy50     charterm-wy50-keydec))
+                    ((regexp-match #rx"^tvi" termvar) (values 'televideo-925 charterm-tvi925-keydec))
+                    ;; Default:
+                    (else (values #f #f))))
+             ((protocol keydec)
+              (values (or protocol 'ansi)
+                      (or keydec charterm-ansi-keydec))))
+          (letrec ((wrapping-evt (wrap-evt in
+                                           (lambda (evt) ct)))
+                   (ct (make-charterm tty-str               ; tty
+                                      in                    ; in
+                                      out                   ; out
+                                      wrapping-evt          ; evt
+                                      buf-size              ; buf-size
+                                      (make-bytes buf-size) ; buf
+                                      0                     ; buf-start
+                                      0                     ; buf-end
+                                      termvar               ; termvar
+                                      protocol              ; protocol
+                                      keydec                ; keydec
+                                        ; screensize
+                                      (if (and (eq? protocol 'ansi)
+                                               (not (member termvar '("screen"))))
+                                          'control/stty/none
+                                          'stty/none))))
+            (and current?
+                 (current-charterm ct))
+            ct))))))
+
+(doc (defproc (close-charterm (#:charterm ct charterm? (current-charterm)))
+         void?
+       (para "Closes "
+             (racket ct)
+             " by closing the I/O ports, and undoing "
+             (racket open-charterm)
+             "'s changes via "
+             (filepath "/bin/stty")
+             ".  If "
+             (racket current-charterm)
+             " is set to "
+             (racket ct)
+             ", then that parameter will be changed to "
+             (racket #f)
+             " for good measure.  You might wish to use "
+             (racket with-charterm)
+             " instead of worrying about calling "
+             (racket close-charterm)
+             " directly.")
+       (para "Note: If you exit your Racket process without properly closing the "
+             (racket charterm)
+             ", your terminal may be left in a crazy state.  You can fix it with
+the command:")
+       (commandline "stty sane")))
+(provide close-charterm)
+(define (close-charterm #:charterm (ct (current-charterm)))
+  (with-handlers ((exn:fail? void)) (close-input-port  (charterm-in ct)))
+  (with-handlers ((exn:fail? void)) (close-output-port (charterm-out ct)))
+  ;; TODO: Set the port fields of the struct to #f?
+  (if (with-handlers ((exn:fail? (lambda (e) #f)))
+        (system* "/bin/stty"
+                 %charterm:stty-minus-f-arg-string
+                 (charterm-tty ct)
+                 "cooked"
+                 "echo"))
+      (if (eq? ct (current-charterm))
+          (current-charterm #f)
+          (void))
+      (error 'close-charterm
+             "stty failed")))
+
+;; (define (call-with-charterm proc #:tty (tty #f))
+;;   (let* ((tty (cleanse-path tty))
+;;          (ct  (open-charterm #:tty tty #:current? #f)))
+;;     (dynamic-wind
+;;       void
+;;       (lambda ()
+;;         (proc ct))
+;;       (lambda ()
+;;         (close-charterm #:charterm ct)))))
+
+(doc (defform (with-charterm expr? ...))
+     (para "Opens a "
+           (racket charterm)
+           " and evaluates the body expressions in sequence with "
+           (racket current-charterm)
+           " set appropriately.  When control jumps out of the body, in a
+manner of speaking, the "
+           (racket charterm)
+           " is closed."))
+(provide with-charterm)
+(define-syntax (with-charterm stx)
+  (syntax-case stx ()
+    ((_ BODY0 BODYn ...)
+     #'(let ((ct #f))
+         (dynamic-wind
+           (lambda ()
+             (set! ct (open-charterm #:current? #t)))
+           (lambda ()
+             BODY0 BODYn ...)
+           (lambda ()
+             (close-charterm #:charterm ct)
+             (set! ct #f)))))))
+
+(doc (section "Terminal Information"))
+
+(doc (defproc (charterm-screen-size (#:charterm ct charterm? (current-charterm)))
+         (values (or/c #f exact-nonnegative-integer?)
+                 (or/c #f exact-nonnegative-integer?))
+       (para "Attempts to get the screen size, in character columns and rows.
+It may do this through a control sequence or through "
+             (code "/bin/stty")
+             ".  If unable to get a value, then default of (80,24) is used.")
+       (para "The current behavior in this version of "
+             (code "charterm")
+             " is to adaptively try different methods of getting screen size,
+and to remember what worked for the next time this procedure is called for "
+             (racket ct)
+             ".  For terminals that are identified as "
+             (code "screen")
+             " by the "
+             (code "TERM")
+             " environment variable (e.g., terminal emulators like GNU Screen
+and "
+             (code "tmux")
+             "), the current behavior is to not try the control sequence (which
+causes a 1-second delay waiting for a terminal response that never arrives),
+and to just use "
+             (code "stty")
+             ".  For all other terminals, the control sequence is tried first, before trying "
+             (code "stty")
+             ".  If neither the control sequence nor "
+             (code "stty")
+             " work, then neither method is tried again for "
+             (racket ct)
+             ", and instead the procedure always returns ("
+             (racket #f)
+             ", "
+             (racket #f)
+             ").  This behavior very well might change in future versions of "
+             (code "charterm")
+             ", and the author welcomes feedback on which methods work with
+which terminals.")))
+(provide charterm-screen-size)
+(define (charterm-screen-size #:charterm (ct (current-charterm)))
+  ;; TODO: Make it store screen side in slots of charterm object too.  Then
+  ;; create a "with-resizeable-charterm" form that has a resize handler (or
+  ;; maybe make the resize handler an argument to "with-charterm".
+  (let loop ()
+    (case (charterm-screensize ct)
+      ((control) (%charterm:screen-size-via-control ct))
+      ((stty)    (%charterm:screen-size-via-stty    ct))
+      ;; TODO: Instead of (80,24), maybe be sensitive to termvar.
+      ((none)    (values 80 24))
+      ((control/stty/none)
+       (let-values (((cols rows) (%charterm:screen-size-via-control ct)))
+         (if (and cols rows)
+             (values cols rows)
+             (begin (set-charterm-screensize! ct 'stty/none)
+                    (loop)))))
+      ((stty/none)
+       (let-values (((cols rows) (%charterm:screen-size-via-stty ct)))
+         (if (and cols rows)
+             (values cols rows)
+             (begin (set-charterm-screensize! ct 'none)
+                    (loop)))))
+      (else (error 'charterm-screen-size
+                   "invalid screensize ~S"
+                   (charterm-screensize ct))))))
+
+(define (%charterm:screen-size-via-control ct)
+  (%charterm:protocol-case
+   '%charterm:screen-size-via-control
+   (charterm-protocol ct)
+   ((ansi)
+    (%charterm:write-bytes ct #"\e[18t")
+    (cond ((%charterm:read-regexp-response ct #rx#"\e\\[8;([0-9]+);([0-9]+)t")
+           => (lambda (m)
+                (values (%charterm:bytes-ascii->nonnegative-integer (list-ref m 1))
+                        (%charterm:bytes-ascii->nonnegative-integer (list-ref m 0)))))
+          ;; TODO: We could do "ioctl" "TIOCGWINSZ", but that means FFI.
+          ;;
+          ;; TODO: We could execute "stty -a" (or perhaps "stty -g") to get
+          ;; around doing an FFI call.
+          (else (values #f #f))))
+   ((wyse-wy50 televideo-925)
+    (%charterm:protocol-unreachable '%charterm:screen-size-via-control ct))))
+
+(define (%charterm:screen-size-via-stty ct)
+  (let* ((stdout (open-output-bytes))
+         (stderr (open-output-bytes))
+         (proc   (list-ref (process*/ports stdout
+                                           (open-input-bytes #"")
+                                           stderr
+                                           "/bin/stty"
+                                           %charterm:stty-minus-f-arg-string
+                                           (charterm-tty ct)
+                                           "-a")
+                           4))
+         (bstr   (begin (proc 'wait)
+                        (get-output-bytes stdout))))
+    (if (eq? 'done-ok (proc 'status))
+        (let-values (((width height)
+                      (cond ((regexp-match-positions
+                              #rx#"rows +([0-9]+);.*columns +([0-9]+)"
+                              bstr)
+                             => (lambda (m)
+                                  (values (%charterm:bytes-ascii->nonnegative-integer
+                                           (subbytes bstr (caaddr m) (cdaddr m)))
+                                          (%charterm:bytes-ascii->nonnegative-integer
+                                           (subbytes bstr (caadr  m) (cdadr m))))))
+                            ((regexp-match-positions
+                              #rx#"columns +([0-9]+);.*rows +([0-9]+)"
+                              bstr)
+                             => (lambda (m)
+                                  (values (%charterm:bytes-ascii->nonnegative-integer
+                                           (subbytes bstr (caadr  m) (cdadr m)))
+                                          (%charterm:bytes-ascii->nonnegative-integer
+                                           (subbytes bstr (caaddr m) (cdaddr m))))))
+                            (else #f #f))))
+          ;; Note: These checks for 0 are for if "stty" returns 0, such as
+          ;; seems to happen in the emulator on the Wyse S50 when in SSH rather than Telnet.
+          (values (if (zero? width)  #f width)
+                  (if (zero? height) #f height)))
+        (values #f #f))))
+
+(doc (section "Display Control"))
+
+(define (%charterm:shift-buf ct)
+  (let ((buf-start (charterm-buf-start ct))
+        (buf-end   (charterm-buf-end   ct)))
+    (if (= buf-start buf-end)
+        ;; Buffer is empty, so are buf-start and buf-end at 0?
+        (if (zero? buf-end)
+            (void)
+            (begin (set-charterm-buf-start! ct 0)
+                   (set-charterm-buf-end!   ct 0)))
+        ;; Buffer is not empty, so is buf-start at 0?
+        ;;
+        ;; TODO: Maybe make this shift only if we need to to free N additional
+        ;; bytes at the end?
+        (if (zero? buf-start)
+            (void)
+            (let ((buf (charterm-buf ct)))
+              (bytes-copy! buf 0 buf buf-start buf-end)
+              (set-charterm-buf-start! ct 0)
+              (set-charterm-buf-end!   ct (- buf-end buf-start)))))))
+
+(define (%charterm:read-into-buf/timeout ct timeout)
+  (let ((in (charterm-in ct)))
+    (let loop ()
+      (let ((sync-result (sync/timeout/enable-break timeout in)))
+        (cond ((not sync-result) #f)
+              ((eq? sync-result in)
+               ;; TODO: if buf is empty, then read into start 0!
+               (let ((read-result (read-bytes-avail! (charterm-buf      ct)
+                                                     in
+                                                     (charterm-buf-end  ct)
+                                                     (charterm-buf-size ct))))
+                 (if (zero? read-result)
+                     ;; TODO: If there's a timeout, subtract from it?
+                     (loop)
+                     (begin (set-charterm-buf-end! ct (+ (charterm-buf-end ct) read-result))
+                            read-result))))
+              (else (error '%charterm:read-into-buf/timeout
+                           "*DEBUG* sync returned ~S"
+                           sync-result)))))))
+
+(define (%charterm:read-regexp-response ct rx #:timeout-seconds (timeout-seconds 1.0))
+  (let ((in (charterm-in ct)))
+    (%charterm:shift-buf ct)
+    ;; TODO: Implement timeout better, by checking clock and doing
+    ;; sync/timeout, or by setting timer.
+    (let loop ((timeout-seconds timeout-seconds))
+      (if (= (charterm-buf-end ct) (charterm-buf-size ct))
+          (begin
+            ;; TODO: Make this an exception instead of #f?
+            #f)
+          (begin (or (let ((buf       (charterm-buf       ct))
+                           (buf-start (charterm-buf-start ct))
+                           (buf-end   (charterm-buf-end   ct)))
+                       (cond ((regexp-match-positions rx
+                                                      buf
+                                                      buf-start
+                                                      buf-end)
+                              => (lambda (m)
+                                   ;; TODO: Audit and test some of this buffer
+                                   ;; code here and elsewhere.
+                                   (let ((match-start (caar m))
+                                         (match-end   (cdar m)))
+                                     (if (= match-start buf-start)
+                                         (set-charterm-buf-start! ct match-end)
+                                         (if (= match-end buf-end)
+                                             (set-charterm-buf-end! ct match-start)
+                                             (begin (bytes-copy! buf
+                                                                 match-start
+                                                                 buf
+                                                                 match-end
+                                                                 buf-end)
+                                                    (set-charterm-buf-end! ct
+                                                                           (+ match-start
+                                                                              (- buf-end
+                                                                                 match-end)))))))
+
+                                   (map (lambda (pos)
+                                          (subbytes buf (car pos) (cdr pos)))
+                                        (cdr m))))
+                             (else #f)))
+                     (if (%charterm:read-into-buf/timeout ct timeout-seconds)
+                         (loop timeout-seconds)
+                         #f
+                         )))))))
+
+(define (%charterm:bytes-ascii->nonnegative-integer bstr)
+  (let ((bstr-len (bytes-length bstr)))
+    (let loop ((i      0)
+               (result 0))
+      (if (= i bstr-len)
+          result
+          (let* ((b     (bytes-ref bstr i))
+                 (b-num (- b 48)))
+            (if (<= 0 b-num 9)
+                (loop (+ 1 i)
+                      (+ (* 10 result) b-num))
+                (error '%charterm:bytes-ascii->nonnegative-integer
+                       "invalid byte ~S"
+                       b)))))))
+
+(doc (subsection "Cursor"))
+
+(doc (defproc (charterm-cursor (x exact-positive-integer?)
+                               (y exact-positive-integer?)
+                               (#:charterm ct charterm? (current-charterm)))
+         void?
+       (para "Positions the cursor at column "
+             (racket x)
+             ", row "
+             (racket y)
+             ", with the upper-left character cell being (1, 1).")))
+(provide charterm-cursor)
+(define (charterm-cursor x y #:charterm (ct (current-charterm)))
+  (%charterm:position ct x y))
+
+(doc (defproc (charterm-newline (#:charterm ct charterm? (current-charterm)))
+         void?
+       (para "Sends a newline to the terminal.  This is typically a CR-LF
+sequence.")))
+(provide charterm-newline)
+(define (charterm-newline #:charterm (ct (current-charterm)))
+  (%charterm:write-bytes ct #"\r\n"))
+
+(doc (subsection "Displaying"))
+
+(define %charterm:err-byte 63)
+
+(doc (defproc (charterm-display
+               (#:charterm ct       charterm?                         (current-charterm))
+               (#:width    width    (or/c #f exact-positive-integer?) #f)
+               (#:pad      pad      (or/c 'width boolean?)            'width)
+               (#:truncate truncate (or/c 'width boolean?)            'width)
+               (           arg      any/c) ...)
+         void?
+       (para "Displays each "
+             (racket arg)
+             " on the terminal, as if formatted by "
+             (racket display)
+             ", with the exception that unprintable or non-ASCII characters
+might not be displayed.  (The exact behavior of what is permitted is expected
+to change in a later version of "
+             "CharTerm"
+             ", so avoid trying to send your own control sequences or using
+newlines, making assumptions about non-ASCII characters, etc.)")
+       (para "If "
+             (racket width)
+             " is a number, then "
+             (racket pad)
+             " and "
+             (racket truncate)
+             " specify whether or not to pad with spaces or truncate the output, respectively, to "
+             (racket width)
+             " characters.  When "
+             (racket pad)
+             " or "
+             (racket width)
+             " is "
+             (racket 'width)
+             ", that is a convenience meaning ``true if, and only if, "
+             (racket width)
+             " is not "
+             (racket #f)
+             ".''")))
+(provide charterm-display)
+(define (charterm-display #:charterm (ct       (current-charterm))
+                          #:width    (width    #f)
+                          #:pad      (pad      'width)
+                          #:truncate (truncate 'width)
+                          . args)
+  ;; TODO: make it replace unprintable and non-ascii characters with "?".  Even newlines, tabs, etc?
+  ;;
+  ;; TODO: Do we want buffering?
+  (let ((out      (charterm-out ct))
+        (pad      (if (eq? 'width pad)      (if width #t #f) pad))
+        (truncate (if (eq? 'width truncate) (if width #t #f) truncate)))
+    (and pad      (not width) (error 'charterm-display "#:pad cannot be true if #:width is not"))
+    (and truncate (not width) (error 'charterm-display "#:truncate cannot be true if #:width is not"))
+    (let loop ((args            args)
+               (remaining-width (or width 0)))
+      (if (null? args)
+          (if (and pad (> remaining-width 0))
+              ;; TODO: Get rid of this allocation.
+              (begin (%charterm:write-bytes ct (make-bytes remaining-width 32))
+                     (void))
+              (void))
+          (let* ((arg (car args))
+                 (bytes (cond ((bytes? arg)
+                               arg)
+                              ((string? arg)
+                               (string->bytes/latin-1 arg
+                                                      %charterm:err-byte
+                                                      0
+                                                      (if truncate
+                                                          (min (string-length arg)
+                                                               remaining-width)
+                                                          (string-length arg))))
+                              ((number? arg)
+                               (string->bytes/latin-1 (number->string arg)
+                                                      %charterm:err-byte))
+                              (else (let ((arg (format "~A" arg)))
+                                      (string->bytes/latin-1 arg
+                                                             %charterm:err-byte
+                                                             0
+                                                             (if truncate
+                                                                 (min (string-length arg)
+                                                                      remaining-width)
+                                                                 (string-length arg)))))))
+                 (remaining-width (- remaining-width (bytes-length bytes))))
+            (cond ((or (not truncate) (> remaining-width 0))
+                   (%charterm:write-bytes ct bytes)
+                   (loop (cdr args)
+                         remaining-width))
+                  ((zero? remaining-width)
+                   (%charterm:write-bytes ct bytes)
+                   (void))
+                  (else (%charterm:write-subbytes ct bytes 0 (+ (bytes-length bytes)
+                                                                remaining-width))
+                        (void))))))))
+
+(define (%charterm:send-code ct . args)
+  ;; TODO: Do we want buffering?
+  (let ((out (charterm-out ct)))
+    (let loop ((args args))
+      (if (null? args)
+          (void)
+          (let ((arg (car args)))
+            (cond ((bytes? arg)
+                   (write-bytes arg out))
+                  ((string? arg)
+                   (write-string arg out))
+                  ((integer? arg)
+                   (display (inexact->exact arg) out))
+                  ((pair? arg)
+                   (loop (car arg))
+                   (loop (cdr arg)))
+                  (else (error '%charterm:send-code
+                               "don't know how to send ~S"
+                               arg)))
+            (loop (cdr args)))))))
+
+;; (define %charterm:2-digit-bytes-vector
+;;   (vector #"00" #"01" #"02" #"03" #"04" #"05" #"06" #"07"
+;;           #"08" #"09" #"10" #"11" #"12" #"13" #"14" #"15"
+;;           #"16" #"17" #"18" #"19" #"20" #"21" #"22" #"23"
+;;           #"24" #"25" #"26" #"27" #"28" #"29" #"30" #"31"
+;;           #"32" #"33" #"34" #"35" #"36" #"37" #"38" #"39"
+;;           #"40" #"41" #"42" #"43" #"44" #"45" #"46" #"47"
+;;           #"48" #"49" #"50" #"51" #"52" #"53" #"54" #"55"
+;;           #"56" #"57" #"58" #"59" #"60" #"61" #"62" #"63"
+;;           #"64" #"65" #"66" #"67" #"68" #"68" #"69" #"70"
+;;           #"72" #"73" #"74" #"75" #"76" #"77" #"78" #"79"
+;;           #"80" #"81" #"82" #"83" #"84" #"85" #"86" #"87"))
+
+(define %charterm:televideo-925-cursor-position-to-byte-vector
+  (list->vector (cons #f
+                      (for/list ((n (in-range 1 96)))
+                        (+ 31 n)))))
+
+;; (provide/contract with error-checks on args
+(define (%charterm:position ct x y)
+  (%charterm:protocol-case
+   '%charterm:position
+   (charterm-protocol ct)
+   ((ansi)
+    (if (and (= 1 x) (= 1 y))
+        (%charterm:write-bytes ct #"\e[;H")
+        (%charterm:send-code ct #"\e[" y #";" x #"H")))
+   ((wyse-wy50)
+    ;; Note: We are using the WY-50 long codes because we don't know
+    ;; confidently that we are an 80-column screen.
+    (if (and (= 1 x) (= 1 y))
+        (%charterm:write-bytes ct #"\ea1R1C")
+        (%charterm:send-code ct #"\ea" y #"R" x #"C")))
+   ((televideo-925)
+    (if (and (= 1 x) (= 1 y))
+        (%charterm:write-bytes ct #"\e=  ")
+        (begin (%charterm:write-bytes ct #"\e=")
+               (%charterm:write-byte ct (vector-ref %charterm:televideo-925-cursor-position-to-byte-vector y))
+               (%charterm:write-byte ct (vector-ref %charterm:televideo-925-cursor-position-to-byte-vector x)))))))
+
+(doc (subsection "Video Attributes"))
+
+;; TODO: !!! document link to protocol section
+
+;; TODO: !!! define "charterm-has-video-attributes?"
+
+(doc (defproc*
+         (((charterm-normal    (#:charterm ct charterm? (current-charterm))) void?)
+          ((charterm-inverse   (#:charterm ct charterm? (current-charterm))) void?)
+          ((charterm-underline (#:charterm ct charterm? (current-charterm))) void?)
+          ((charterm-blink     (#:charterm ct charterm? (current-charterm))) void?)
+          ((charterm-bold      (#:charterm ct charterm? (current-charterm))) void?))
+       (para "Sets the "
+             (deftech "video attributes")
+             " for subsequent writes to the terminal.  In this version of "
+             (code "charterm")
+             ", each is mutually-exclusive, so, for example, setting "
+             (italic "bold")
+             " clears "
+             (italic "inverse")
+             ". Note that that video attributes are currently supported only for protocol "
+             (racket 'ansi)
+             ", due to limitations of the TeleVideo and Wyse models for
+video attributes.")))
+
+(provide charterm-normal)
+(define (charterm-normal #:charterm (ct (current-charterm)))
+  (%charterm:protocol-case
+   'charterm-normal
+   (charterm-protocol ct)
+   ((ansi)      (%charterm:write-bytes ct #"\e[m"))
+   ((wyse-wy50)     (void)) ; (%charterm:write-bytes ct #"\eA00"))
+   ((televideo-925) (void))))
+
+(provide charterm-inverse)
+(define (charterm-inverse #:charterm (ct (current-charterm)))
+  (%charterm:protocol-case
+   'charterm-inverse
+   (charterm-protocol ct)
+   ((ansi)      (%charterm:write-bytes ct #"\e[;7m"))
+   ((wyse-wy50)     (void)) ; (%charterm:write-bytes ct #"\eA04"))
+   ((televideo-925) (void))))
+
+(provide charterm-underline)
+(define (charterm-underline #:charterm (ct (current-charterm)))
+  (%charterm:protocol-case
+   'charterm-underline
+   (charterm-protocol ct)
+   ((ansi)      (%charterm:write-bytes ct #"\e[4m"))
+   ((wyse-wy50)     (void)) ; (%charterm:write-bytes ct #"\eA08"))
+   ((televideo-925) (void))))
+
+(provide charterm-blink)
+(define (charterm-blink #:charterm (ct (current-charterm)))
+  (%charterm:protocol-case
+   'charterm-blink
+   (charterm-protocol ct)
+   ((ansi)      (%charterm:write-bytes ct #"\e[5m"))
+   ((wyse-wy50)     (void)) ; (%charterm:write-bytes ct #"\eA02"))
+   ((televideo-925) (void))))
+
+(provide charterm-bold)
+(define (charterm-bold #:charterm (ct (current-charterm)))
+  (%charterm:protocol-case
+   'charterm-bold
+   (charterm-protocol ct)
+   ((ansi)      (%charterm:write-bytes ct #"\e[1m"))
+   ((wyse-wy50)     (void)) ; (%charterm:write-bytes ct #"\eA0<"))
+   ((televideo-925) (void))))
+
+(doc (subsection "Clearing"))
+
+(doc (defproc (charterm-clear-screen (#:charterm ct charterm? (current-charterm)))
+         void?
+       (para "Clears the screen, including first setting the video attributes to
+normal, and positioning the cursor at (1, 1).")))
+(provide charterm-clear-screen)
+(define (charterm-clear-screen #:charterm (ct (current-charterm)))
+  ;; TODO: Have a #:style argument?  Or #:background argument?
+  (%charterm:protocol-case
+   'charterm-clear-screen
+   (charterm-protocol ct)
+   ((ansi)      (%charterm:write-bytes ct #"\e[m\e[2J\e[;H"))
+   ((wyse-wy50)     (%charterm:write-bytes ct #"\e+\e*\ea1R1C"))
+   ((televideo-925) (%charterm:write-bytes ct #"\e+\e=  "))))
+
+(doc (defproc*
+         (((charterm-clear-line       (#:charterm ct charterm? (current-charterm))) void?)
+          ((charterm-clear-line-left  (#:charterm ct charterm? (current-charterm))) void?)
+          ((charterm-clear-line-right (#:charterm ct charterm? (current-charterm))) void?))
+       (para "Clears text from the line with the cursor, or part of the line with the cursor.")))
+
+(provide charterm-clear-line)
+(define (charterm-clear-line #:charterm (ct (current-charterm)))
+  (%charterm:protocol-case
+   'charterm:clear-line
+   (charterm-protocol ct)
+   ((ansi)      (%charterm:write-bytes ct #"\e[2K"))
+   ((televideo-925) (%charterm:write-bytes ct #"\r\eT"))
+   ;; TODO: wyse-wy50 is clearing to nulls, not spaces.
+   ((wyse-wy50)     (%charterm:write-bytes ct #"\r\et"))))
+
+(provide charterm-clear-line-left)
+(define (charterm-clear-line-left #:charterm (ct (current-charterm)))
+  (%charterm:protocol-case
+   'charterm-clear-line-left
+   (charterm-protocol ct)
+   ((ansi) (%charterm:write-bytes ct #"\e[1K"))
+   ((televideo-925 wyse-wy50)
+    ;; TODO: Do this by getting cursor position, then reposition and write spaces?
+    (%charterm:unimplemented ct 'clearterm-clear-line-left))))
+
+(provide charterm-clear-line-right)
+(define (charterm-clear-line-right #:charterm (ct (current-charterm)))
+  (%charterm:protocol-case
+   'charterm-clear-line-right
+   (charterm-protocol ct)
+   ((ansi)      (%charterm:write-bytes ct #"\e[K"))
+   ((televideo-925) (%charterm:write-bytes ct #"\eT"))
+   ;; TODO: wyse-wy50 is clearing to nulls, not spaces.
+   ((wyse-wy50)     (%charterm:write-bytes ct #"\et"))))
+
+(doc (subsection "Line Insert and Delete"))
+
+(doc (defproc (charterm-insert-line (count exact-positive-integer? 1)
+                                    (#:charterm ct charterm? (current-charterm)))
+         void?
+       (para "Inserts "
+             (racket count)
+             " blank lines at cursor.  Note that not all terminals support
+this.")))
+(provide charterm-insert-line)
+(define (charterm-insert-line (count 1) #:charterm (ct (current-charterm)))
+  (if (integer? count)
+      (cond ((= count 0) (void))
+            ((> count 0)
+             (%charterm:protocol-case
+              'charterm-insert-line
+              (charterm-protocol ct)
+              ((ansi)                (%charterm:send-code ct #"\e[" count "L"))
+              ((wyse-wy50 televideo-925) (%charterm:write-bytes ct #"\eE"))))
+            (else (error 'charterm-insert-line
+                         "invalid count: ~S"
+                         count)))
+      (error 'charterm-insert-line
+             "invalid count: ~S"
+             count)))
+
+(doc (defproc (charterm-delete-line (count exact-positive-integer? 1)
+                                    (#:charterm ct charterm? (current-charterm)))
+         void?
+       (para "Deletes "
+             (racket count)
+             " blank lines at cursor.  Note that not all terminals support
+this.")))
+(provide charterm-delete-line)
+(define (charterm-delete-line (count 1) #:charterm (ct (current-charterm)))
+  (if (integer? count)
+      (cond ((= count 0) (void))
+            ((> count 0)
+             (%charterm:protocol-case
+              'charterm-delete-line
+              (charterm-protocol ct)
+              ((ansi)
+               (%charterm:send-code ct #"\e[" count "M"))
+              ((wyse-wy50 televideo-925)
+               (if (= 1 count)
+                   (%charterm:write-bytes ct #"\eR")
+                   (let ((bstr (make-bytes (* 2 count) 82)))
+                     (let loop ((n (* 2 (- count 1))))
+                       (bytes-set! bstr n 27)
+                       (if (zero? n)
+                           (%charterm:write-bytes ct bstr)
+                           (loop (- n 2)))))))))
+            (else (error 'charterm-delete-line
+                         "invalid count: ~S"
+                         count)))
+      (error 'charterm-delete-line
+             "invalid count: ~S"
+             count)))
+
+(doc (subsubsection "Misc. Output"))
+
+(doc (defproc (charterm-bell (#:charterm ct charterm? (current-charterm)))
+         void?
+       (para "Rings the terminal bell.  This bell ringing might manifest as a
+beep, a flash of the screen, or nothing.")))
+(provide charterm-bell)
+(define (charterm-bell #:charterm (ct (current-charterm)))
+  (%charterm:write-bytes ct #"\007"))
+
+(doc (section "Keyboard Input")
+
+     ;; TODO: !!! document link to terminal diversity section
+
+     (para "Normally you will get keyboard input using the "
+           (racket charterm-read-key)
+           " procedure."))
+
+(doc (defproc (charterm-byte-ready? (#:charterm ct charterm? (current-charterm)))
+         boolean?
+       (para "Returns true/false for whether at least one byte is ready for
+reading (either in a buffer or on the port) from "
+             (racket ct)
+             ".  Note that, since some keys are encoded as multiple bytes, just
+because this procedure returns true doesn't mean that "
+             (racket charterm-read-key)
+             " won't block temporarily because it sees part of a potential
+multiple-byte key encoding.")))
+(provide charterm-byte-ready?)
+(define (charterm-byte-ready? #:charterm (ct (current-charterm)))
+  (or (> (charterm-buf-end ct) (charterm-buf-start ct))
+      (byte-ready? (charterm-in ct))))
+
+(doc (defproc (charterm-read-key
+               (#:charterm ct      charterm?           (current-charterm))
+               (#:timeout  timeout (or/c #f positive?) #f))
+         (or #f char? symbol?)
+       (para "Reads a key from "
+             (racket ct)
+             ", blocking indefinitely or until sometime after "
+             (racket timeout)
+             " seconds has been reached, if "
+             (racket timeout)
+             " is non-"
+             (racket #f)
+             ".  If timeout is reached, "
+             (racket #f)
+             " is returned.")
+       (para "Many keys are returned as characters, especially ones that
+correspond to printable characters.  For example, the unshifted "
+             (bold "Q")
+             " key is returned as character "
+             (racket #\q)
+             ".  Some other keys are returned as symbols, such as "
+             (racket 'return)
+             ", "
+             (racket 'escape)
+             ", "
+             (racket 'f1)
+             ", "
+             (racket 'shift-f12)
+             ", "
+             (racket 'right)
+             ", and many others.")
+       (para "Since some keys are sent as ambiguous sequences, "
+             (racket charterm-read-key)
+             " employs separate timeouts internally, such as to disambuate
+the "
+             (bold "Esc")
+             " key (byte sequence 27) from what on some terminals would be
+the "
+             (bold "F10")
+             " key (bytes sequence 27, 91, 50, 49, 126).")))
+(provide charterm-read-key)
+(define (charterm-read-key #:charterm (ct      (current-charterm))
+                           #:timeout  (timeout #f))
+  (%charterm:read-keyinfo-or-key 'charterm-read-key ct timeout #f))
+
+(doc (defproc (charterm-read-keyinfo
+               (#:charterm ct      charterm?           (current-charterm))
+               (#:timeout  timeout (or/c #f positive?) #f))
+         charterm-keyinfo?
+       (para "Like "
+             (racket charterm-read-keyinfo)
+             " except instead of returning a "
+             (tech "keycode")
+             ", it returns a "
+             (tech "keyinfo")
+             ".")))
+(provide charterm-read-keyinfo)
+(define (charterm-read-keyinfo #:charterm (ct      (current-charterm))
+                               #:timeout  (timeout #f))
+  (%charterm:read-keyinfo-or-key 'charterm-read-keyinfo ct timeout #t))
+
+(define (%charterm:read-keyinfo-or-key error-name ct timeout keyinfo?)
+  ;; TODO: Maybe make this shift decision smarter -- compile the key tree ahead
+  ;; of time so we know the max depth, and then we know exactly the max space
+  ;; we will need for this call.
+  (and (< (- (charterm-buf-size ct)
+             (charterm-buf-start ct))
+          10)
+       (%charterm:shift-buf ct))
+  (let ((buf       (charterm-buf        ct))
+        (buf-start (charterm-buf-start  ct))
+        (buf-end   (charterm-buf-end    ct))
+        (buf-size  (charterm-buf-size   ct))
+        (keydec    (charterm-keydec*    ct))
+        (b1        (%charterm:read-byte/timeout ct timeout)))
+    (if b1
+        (or (let loop ((tree        (charterm-keydec-primary-keytree keydec))
+                       (probe-start (+ 1 buf-start))
+                       (b           b1))
+              (cond ((hash-ref tree b #f)
+                     => (lambda (code-or-subtree)
+                          (cond ((hash? code-or-subtree)
+                                 ;; We have more subtree to search.
+                                 (if (or (< probe-start buf-end)
+                                         (and (< buf-end buf-size)
+                                              (%charterm:read-into-buf/timeout ct 0.5)))
+                                     ;; We have at least one more byte, so recurse.
+                                     (loop code-or-subtree
+                                           (+ 1 probe-start)
+                                           (bytes-ref buf probe-start))
+                                     ;; We have hit timeout or end of buffer, so
+                                     ;; just accept the original byte.
+                                     #f))
+                                ((charterm-keyinfo? code-or-subtree)
+                                 ;; We found our keyinfo, so consume the input and return the value.
+                                 (begin (set-charterm-buf-start! ct probe-start)
+                                        (if keyinfo?
+                                            code-or-subtree
+                                            (charterm-keyinfo-keycode code-or-subtree))
+                                        ))
+                                (else (error error-name
+                                             "invalid object in keytree keyinfo position: ~S"
+                                             code-or-subtree)))))
+                    (else #f)))
+            ;; We didn't find a key code, so try secondary keytree with initial byte.
+            (cond ((hash-ref (charterm-keydec-secondary-keytree keydec) b1 #f)
+                   => (lambda (keyinfo)
+                        (if keyinfo?
+                            keyinfo
+                            (charterm-keyinfo-keycode keyinfo))))
+                  (else (if keyinfo?
+                            ;; TODO: Cache these keyinfos for unrecognized keys
+                            ;; in the charterm object, or make a fallback
+                            ;; keyset for them (although the fallback keyset,
+                            ;; while it works for 8-bit characters, becomes
+                            ;; less practical if we implement multibyte).
+                            (make-charterm-keyinfo #f
+                                                   #f
+                                                   (list b1)
+                                                   "???"
+                                                   b1
+                                                   (list b1))
+                            (integer->char b1)))))
+        ;; Got a timeout, so return #f.
+        #f)))
+
+(define (%charterm:write-byte ct byt)
+  (write-byte byt (charterm-out ct)))
+
+(define (%charterm:write-bytes ct bstr . rest-bstrs)
+  (write-bytes bstr (charterm-out ct))
+  (or (null? rest-bstrs)
+      (for-each (lambda (bstr)
+                  (write-bytes bstr (charterm-out ct)))
+                rest-bstrs)))
+
+(define (%charterm:write-subbytes ct bstr start end)
+  (write-bytes bstr (charterm-out ct) start end))
+
+(define (%charterm:read-byte/timeout ct timeout)
+  (let ((buf-start (charterm-buf-start ct)))
+    (if (or (< buf-start (charterm-buf-end ct))
+            (%charterm:read-into-buf/timeout ct timeout))
+        (begin0 (bytes-ref (charterm-buf ct) buf-start)
+          (set-charterm-buf-start! ct (+ 1 buf-start)))
+        #f)))
+
+(define (%charterm:read-byte ct)
+  (%charterm:read-byte/timeout ct #f))
+
+(doc (section "References")
+
+     (para "[" (deftech "ANSI X3.64") "] "
+           (url "http://en.wikipedia.org/wiki/ANSI_escape_code"))
+
+     (para "[" (deftech "ASCII") "] "
+           (url "http://en.wikipedia.org/wiki/Ascii"))
+     
+     (para "[" (deftech "ECMA-43") "] "
+           (hyperlink "http://www.ecma-international.org/publications/standards/Ecma-043.htm"
+                      (italic "Standard ECMA-43: 8-bit Coded Character Set Structure and Rules"))
+           ", 3rd Ed., 1991-12")
+
+     (para "[" (deftech "ECMA-48") "] "
+           (hyperlink "http://www.ecma-international.org/publications/standards/Ecma-048.htm"
+                      (italic "Standard ECMA-48: Control Functions for Coded Character Sets"))
+           ", 5th Ed., 1991-06")
+
+     (para "[" (deftech "Gregory") "] "
+           "Phil Gregory, ``"
+           (hyperlink "http://aperiodic.net/phil/archives/Geekery/term-function-keys.html"
+                      "Terminal Function Key Escape Codes")
+           ",'' 2005-12-13 Web post, as viewed on 2012-06")
+
+     (para "[" (deftech "PowerTerm") "] "
+           "Ericom PowerTerm InterConnect 8.2.0.1000 terminal emulator, as run on Wyse S50 WinTerm")
+
+     (para "[" (deftech "TVI-925-IUG") "] "
+           (hyperlink "http://vt100.net/televideo/tvi925_ig.pdf"
+                      (italic "TeleVideo Model 925 CRT Terminal Installation and User's Guide")))
+
+     (para "[" (deftech "TVI-950-OM") "] "
+           (hyperlink "http://www.mirrorservice.org/sites/www.bitsavers.org/pdf/televideo/Operators_Manual_Model_950_1981.pdf"
+                      (italic "TeleVideo Operator's Manual Model 950"))
+           ", 1981")
+     
+     (para "[" (deftech "VT100-TM") "] "
+           "Digital Equipment Corp., "
+           (hyperlink "http://vt100.net/docs/vt100-tm/"
+                      (italic "VT100 Series Technical Manual"))
+           ", 2nd Ed., 1980-09")
+     
+     (para "[" (deftech "VT100-UG") "] "
+           "Digital Equipment Corp., "
+           (hyperlink "http://vt100.net/docs/vt100-ug/"
+                      (italic "VT100 User Guide"))
+           ", 3rd Ed., 1981-06")
+                      
+     (para "[" (deftech "VT100-WP") "] "
+           "Wikipedia, "
+           (hyperlink "http://en.wikipedia.org/wiki/VT100"
+                      "VT100"))
+
+     (para "[" (deftech "WY-50-QRG") "] "
+           (hyperlink "http://vt100.net/wyse/wy-50-qrg/wy-50-qrg.pdf"
+                      (italic "Wyse WY-50 Display Terminal Quick-Reference Guide")))
+
+     (para "[" (deftech "WY-60-UG") "] "
+           (hyperlink "http://vt100.net/wyse/wy-60-ug/wy-60-ug.pdf"
+                      (italic "Wyse WY-60 User's Guide")))
+
+     (para "[" (deftech "wy60") "] "
+           (hyperlink "http://code.google.com/p/wy60/"
+                      (code "wy60")
+                      " terminal emulator"))
+
+     (para "[" (deftech "XTerm-ctlseqs") "] "
+           "Edward Moy, Stephen Gildea, Thomas Dickey, ``"
+           (hyperlink "http://invisible-island.net/xterm/ctlseqs/ctlseqs.html"
+                      "Xterm Control Sequences")
+           ",'' 2012")
+
+     (para "[" (deftech "XTerm-Dickey") "] "
+           (url "http://invisible-island.net/xterm/"))
+
+     (para "[" (deftech "XTerm-FAQ") "] "
+           "Thomas E. Dickey, ``"
+           (hyperlink "http://invisible-island.net/xterm/xterm.faq.html"
+                      "XTerm FAQ")
+           ",'' dated 2012")
+
+     (para "[" (deftech "XTerm-WP") "] "
+           "Wikipedia, "
+           (hyperlink "http://en.wikipedia.org/wiki/Xterm"
+                      "xterm"))
+           
+     )
+
+(doc (section "Known Issues")
+
+     (itemlist
+
+      (item "Need to support ANSI alternate CSI for 8-bit terminals, even
+before supporting 8-bit characters and multibyte.")
+
+      (item "Only supports ASCII characters.  Adding UTF-8 support, for terminal emulators
+that support it, would be nice.")
+      
+      (item "Expose the character-decoding mini-language as a configurable
+option.  Perhaps wait until we implement timeout-based disambiguation at
+arbitrary points in the the DFA rather than just at the top.  Also, might be
+better to resolve multi-byte characters first, in case that affects the
+mini-language.")
+
+      (item "More controls for terminal features can be added.")
+      
+      (item "Currently only implemented to work on Unix-like systems like
+GNU/Linux.")
+
+      (item "Implement text input controls, either as part of this library or
+another, using "
+            (racket charterm-demo)
+            " as a starting point.")))
+
+;; Note: Different ways to test demo:
+;;
+;;           racket -t demo.rkt -m
+;; screen    racket -t demo.rkt -m
+;; tmux -c  "racket -t demo.rkt -m"
+;; xterm -e  racket -t demo.rkt -m
+;; rxvt -e   racket -t demo.rkt -m
+;; wy60 -c   racket -t demo.rkt -m
+;;
+;; racket -t demo.rkt -m- -n
+
+;; TODO: Source for TeleVideo manuals:
+;; http://www.mirrorservice.org/sites/www.bitsavers.org/pdf/televideo/
+
+;; TODO: Add shifted function keys from T60 keyboard (not USB one).
+
+(doc history
+
+     (#:planet 3:1 #:date "2013-05-13"
+               (itemlist
+                (item "Now uses lowercase "
+                      (code "-f")
+                      " argument on MacOS X.  (Thanks to Jens Axel S\u00F8gaard for reporting.)")
+                (item "Documentation tweaks.")))
+
+     (#:planet 3:0 #:date "2012-07-13"
+               (itemlist
+                (item "Changed ``"
+                      (code "ansi-ish")
+                      "'' in identifiers to ``"
+                      (code "ansi")
+                      "'', hence the PLaneT major version number change.")
+                (item "Documentation tweaks.")
+                (item "Renamed package from ``"
+                      (code "charterm")
+                      "'' to ``CharTerm''.")))
+
+     (#:planet 2:5 #:date "2012-06-28"
+               (itemlist
+                (item "A "
+                      (racket charterm)
+                      " object is now a synchronizable event.")
+                (item "Documentation tweaks.")))
+     
+     (#:planet 2:4 #:date "2012-06-25"
+               (itemlist
+                (item "Documentation fix for return type of "
+                      (racket charterm-read-keyinfo)
+                      ".")))
+     
+     (#:planet 2:3 #:date "2012-06-25"
+               (itemlist
+                (item "Fixed problem determining screen size on some
+XTerms.  (Thanks to Eli Barzilay for reporting.)")))
+
+     (#:planet 2:2 #:date "2012-06-25"
+               (itemlist
+                (item "Added another variation of encoding for XTerm arrow,
+Home, and End keys.  (Thanks to Eli Barzilay.)")))
+     
+     (#:planet 2:1 #:date "2012-06-24"
+               (itemlist
+                (item "Corrected PLaneT version number in "
+                      (racket require)
+                      " in an example.")))
+     
+     (#:planet 2:0 #:date "2012-06-24"
+               (itemlist
+                (item "Greatly increased the sophistication of handling of terminal diversity.")
+                (item "Added the "
+                      (code "wyse-wy50")
+                      " and "
+                      (code "televideo-950")
+                      " [Correction: "
+                      (code "televideo-925")
+                      "] protocols, for supporting the native modes of Wyse and
+TeleVideo terminals, respectively, and compatibles.")
+                (item "More support for different key encodings and termvars.")
+                (item "Demo is now in a separate file, mainly for convenience
+in giving command lines that run it.  This breaks a command line example
+previously documented, so changed PLaneT major version, although the
+previously-published example will need to have "
+                      (code ":1")
+                      " added to it anyway.")
+                (item (racket charterm-screen-size)
+                      " now defaults to (80,24) when all else fails.")
+                (item "Documentation changes.")))
+
+     (#:planet 1:1 #:date "2012-06-17"
+               (itemlist
+                (item "For "
+                      (code "screen")
+                      " and "
+                      (code "tmux")
+                      ", now gets screen size via "
+                      (code "stty")
+                      ".  This resolves the sluggishness reported with "
+                      (code "screen")
+                      ".  [Correction: In version 1:1, this behavior is
+adaptive for all terminals, with the shortcut for "
+                      (tech "termvar")
+                      " "
+                      (code "screen")
+                      " that it doesn't bother trying the control sequence.]")
+                (item "Documentation tweaks.")))
+
+     (#:planet 1:0 #:date "2012-06-16"
+               (itemlist
+                (item "Initial version."))))
diff --git a/archive/1.vm.arc/charterm/demo.rkt b/archive/1.vm.arc/charterm/demo.rkt
new file mode 100644
index 00000000..4cbff6e5
--- /dev/null
+++ b/archive/1.vm.arc/charterm/demo.rkt
@@ -0,0 +1,306 @@
+#lang racket/base
+;; For legal info, see file "info.rkt"
+
+(require racket/cmdline
+         racket/date
+         "charterm.rkt")
+
+(define (%charterm:string-pad-or-truncate str width)
+  (let ((len (string-length str)))
+    (cond ((= len width) str)
+          ((< len width) (string-append str (make-string (- width len) #\space)))
+          (else (substring str 0 width)))))
+
+(define (%charterm:bytes-pad-or-truncate bstr width)
+  (let ((len (bytes-length bstr)))
+    (cond ((= len width) bstr)
+          ((< len width)
+           (let ((new-bstr (make-bytes width 32)))
+             (bytes-copy! new-bstr 0 bstr)
+             new-bstr))
+          (else (subbytes bstr 0 width)))))
+
+(define-struct %charterm:demo-input
+  (x y width bytes used cursor)
+  #:mutable)
+
+(define (%charterm:make-demo-input x y width bstr)
+  (let ((new-bstr (%charterm:bytes-pad-or-truncate bstr width))
+        (used     (min (bytes-length bstr) width)))
+    (make-%charterm:demo-input x
+                               y
+                               width
+                               new-bstr
+                               used
+                               used)))
+
+(define (%charterm:demo-input-redraw di)
+  (charterm-cursor (%charterm:demo-input-x di)
+                   (%charterm:demo-input-y di))
+  (charterm-normal)
+  (charterm-underline)
+  (charterm-display (%charterm:demo-input-bytes di)
+                    #:width (%charterm:demo-input-width di))
+  (charterm-normal))
+
+(define (%charterm:demo-input-put-cursor di)
+  ;; Note: Commented-out debugging code:
+  ;;
+  ;; (and #t
+  ;;      (begin (charterm-normal)
+  ;;             (charterm-cursor (+ (%charterm:demo-input-x     di)
+  ;;                                   (%charterm:demo-input-width di)
+  ;;                                   1)
+  ;;                                (%charterm:demo-input-y di))
+  ;;             (charterm-display #" cursor: "
+  ;;                               (%charterm:demo-input-cursor di)
+  ;;                               #" used: "
+  ;;                               (%charterm:demo-input-used di))
+  ;;             (charterm-clear-line-right)))
+  (charterm-cursor (+ (%charterm:demo-input-x      di)
+                      (%charterm:demo-input-cursor di))
+                   (%charterm:demo-input-y di)))
+
+(define (%charterm:demo-input-cursor-left di)
+  (let ((cursor (%charterm:demo-input-cursor di)))
+    (if (zero? cursor)
+        (begin (charterm-bell)
+               (%charterm:demo-input-put-cursor di))
+        (begin (set-%charterm:demo-input-cursor! di (- cursor 1))
+               (%charterm:demo-input-put-cursor di)))))
+
+(define (%charterm:demo-input-cursor-right di)
+  (let ((cursor (%charterm:demo-input-cursor di)))
+    (if (= cursor (%charterm:demo-input-used di))
+        (begin (charterm-bell)
+               (%charterm:demo-input-put-cursor di))
+        (begin (set-%charterm:demo-input-cursor! di (+ cursor 1))
+               (%charterm:demo-input-put-cursor di)))))
+
+(define (%charterm:demo-input-backspace di)
+  (let ((cursor (%charterm:demo-input-cursor di)))
+    (if (zero? cursor)
+        (begin (charterm-bell)
+               (%charterm:demo-input-put-cursor di))
+        (let ((bstr (%charterm:demo-input-bytes di))
+              (used (%charterm:demo-input-used di)))
+          ;; TODO: test beginning/end of buffer, of used, of width
+          (bytes-copy! bstr (- cursor 1) bstr cursor used)
+          (bytes-set! bstr (- used 1) 32)
+          (set-%charterm:demo-input-used! di (- used 1))
+          (set-%charterm:demo-input-cursor! di (- cursor 1))
+          (%charterm:demo-input-redraw di)
+          (%charterm:demo-input-put-cursor di)))))
+
+(define (%charterm:demo-input-delete di)
+  (let ((cursor (%charterm:demo-input-cursor di))
+        (used   (%charterm:demo-input-used   di)))
+    (if (= cursor used)
+        (begin (charterm-bell)
+               (%charterm:demo-input-put-cursor di))
+        (let ((bstr (%charterm:demo-input-bytes di)))
+          (or (= cursor used)
+              (bytes-copy! bstr cursor bstr (+ 1 cursor) used))
+          (bytes-set! bstr (- used 1) 32)
+          (set-%charterm:demo-input-used! di (- used 1))
+          (%charterm:demo-input-redraw     di)
+          (%charterm:demo-input-put-cursor di)))))
+
+(define (%charterm:demo-input-insert-byte di new-byte)
+  (let ((used  (%charterm:demo-input-used  di))
+        (width (%charterm:demo-input-width di)))
+    (if (= used width)
+        (begin (charterm-bell)
+               (%charterm:demo-input-put-cursor di))
+        (let ((bstr   (%charterm:demo-input-bytes  di))
+              (cursor (%charterm:demo-input-cursor di)))
+          (or (= cursor used)
+              (bytes-copy! bstr (+ cursor 1) bstr cursor used))
+          (bytes-set! bstr cursor new-byte)
+          (set-%charterm:demo-input-used! di (+ 1 used))
+          (set-%charterm:demo-input-cursor! di (+ cursor 1))
+          (%charterm:demo-input-redraw di)
+          (%charterm:demo-input-put-cursor di)))))
+
+(provide charterm-demo)
+(define (charterm-demo #:tty     (tty     #f)
+                       #:escape? (escape? #t))
+  (let ((data-row 4)
+        (di       (%charterm:make-demo-input 10 2 18 #"Hello, world!")))
+    (with-charterm
+     (let ((ct (current-charterm)))
+       (let/ec done-ec
+         (let loop-remember-read-screen-size ((last-read-col-count 0)
+                                              (last-read-row-count 0))
+
+           (let loop-maybe-check-screen-size ()
+             (let*-values (((read-col-count read-row-count)
+                            (if (or (equal? 0 last-read-col-count)
+                                    (equal? 0 last-read-row-count)
+                                    (not (charterm-byte-ready?)))
+                                (charterm-screen-size)
+                                (values last-read-col-count
+                                        last-read-row-count)))
+                           ((read-screen-size? col-count row-count)
+                            (if (and read-col-count read-row-count)
+                                (values #t
+                                        read-col-count
+                                        read-row-count)
+                                (values #f
+                                        (or read-col-count 80)
+                                        (or read-row-count 24))))
+                           ((read-screen-size-changed?)
+                            (not (and (equal? read-col-count
+                                              last-read-col-count)
+                                      (equal? read-row-count
+                                              last-read-row-count))))
+                           ((clock-col)
+                            (let ((clock-col (- col-count 8)))
+                              (if (< clock-col 15)
+                                  #f
+                                  clock-col))))
+               ;; Did screen size change?
+               (if read-screen-size-changed?
+
+                   ;; Screen size changed.
+                   (begin (charterm-clear-screen)
+                          (charterm-cursor 1 1)
+                          (charterm-inverse)
+                          (charterm-display (%charterm:string-pad-or-truncate " charterm Demo"
+                                                                              col-count))
+                          (charterm-normal)
+
+                          (charterm-cursor 1 2)
+                          (charterm-inverse)
+                          (charterm-display #" Input: ")
+                          (charterm-normal)
+                          (%charterm:demo-input-redraw di)
+
+                          (charterm-cursor 1 data-row)
+                          (if escape?
+                               (begin
+                                 (charterm-display "To quit, press ")
+                                 (charterm-bold)
+                                 (charterm-display "Esc")
+                                 (charterm-normal)
+                                 (charterm-display "."))
+                               (charterm-display "There is no escape from this demo."))
+                               
+                          (charterm-cursor 1 data-row)
+                          (charterm-insert-line)
+                          (charterm-display "termvar ")
+                          (charterm-bold)
+                          (charterm-display (charterm-termvar ct))
+                          (charterm-normal)
+                          (charterm-display ", protocol ")
+                          (charterm-bold)
+                          (charterm-display (charterm-protocol ct))
+                          (charterm-normal)
+                          (charterm-display ", keydec ")
+                          (charterm-bold)
+                          (charterm-display (charterm-keydec-id (charterm-keydec ct)))
+                          (charterm-normal)
+
+                          (charterm-cursor 1 data-row)
+                          (charterm-insert-line)
+                          (charterm-display #"Screen size: ")
+                          (charterm-bold)
+                          (charterm-display col-count)
+                          (charterm-normal)
+                          (charterm-display #" x ")
+                          (charterm-bold)
+                          (charterm-display row-count)
+                          (charterm-normal)
+                          (or read-screen-size?
+                              (charterm-display #" (guessing; terminal would not tell us)"))
+
+                          (charterm-cursor 1 data-row)
+                          (charterm-insert-line)
+                          (charterm-display #"Widths:")
+                          (for-each (lambda (bytes)
+                                      (charterm-display #" [")
+                                      (charterm-underline)
+                                      (charterm-display bytes #:width 3)
+                                      (charterm-normal)
+                                      (charterm-display #"]"))
+                                    '(#"" #"a" #"ab" #"abc" #"abcd"))
+
+                          ;; (and (eq? 'wy50 (charterm-protocol ct))
+                          ;;      (begin
+                          ;;        (charterm-cursor 1 data-row)
+                          ;;        (charterm-insert-line)
+                          ;;        (charterm-display #"Wyse WY-50 delete character: ab*c\010\010\eW")))
+
+                          (loop-remember-read-screen-size read-col-count
+                                                          read-row-count))
+                   ;; Screen size didn't change (or we didn't check).
+                   (begin
+                     (and clock-col
+                          (begin (charterm-inverse)
+                                 (charterm-cursor clock-col 1)
+                                 (charterm-display (parameterize ((date-display-format 'iso-8601))
+                                                     (substring (date->string (current-date) #t)
+                                                                11)))
+                                 (charterm-normal)))
+
+                     (let loop-fast-next-key ()
+                       (%charterm:demo-input-put-cursor di)
+                       (let ((keyinfo (charterm-read-keyinfo #:timeout 1)))
+                         (if keyinfo
+                             (let ((keycode (charterm-keyinfo-keycode keyinfo)))
+                               (charterm-cursor 1 data-row)
+                               (charterm-insert-line)
+                               (charterm-display "Read key: ")
+                               (charterm-bold)
+                               (charterm-display (or (charterm-keyinfo-keylabel keyinfo) "???"))
+                               (charterm-normal)
+                               (charterm-display (format " ~S"
+                                                         `(,(charterm-keyinfo-keyset-id    keyinfo)
+                                                           ,(charterm-keyinfo-bytelang     keyinfo)
+                                                           ,(charterm-keyinfo-bytelist     keyinfo)
+                                                           ,@(charterm-keyinfo-all-keycodes keyinfo))))
+                               (if (char? keycode)
+                                   (let ((key-num (char->integer keycode)))
+                                     (if (<= 32 key-num 126)
+                                         (begin (%charterm:demo-input-insert-byte di key-num)
+                                                (loop-fast-next-key))
+                                         (loop-fast-next-key)))
+                                   (case keycode
+                                     ((left)
+                                      (%charterm:demo-input-cursor-left di)
+                                      (loop-fast-next-key))
+                                     ((right)
+                                      (%charterm:demo-input-cursor-right di)
+                                      (loop-fast-next-key))
+                                     ((backspace)
+                                      (%charterm:demo-input-backspace di)
+                                      (loop-fast-next-key))
+                                     ((delete)
+                                      (%charterm:demo-input-delete di)
+                                      (loop-fast-next-key))
+                                     ((escape)
+                                      (if escape?
+                                          (begin
+                                            (charterm-clear-screen)
+                                            (charterm-display "You have escaped the charterm demo!")
+                                            (charterm-newline)
+                                            (done-ec))
+                                          (loop-fast-next-key)))
+                                     (else (loop-fast-next-key)))))
+                             (begin
+                               ;; (charterm-display "Timeout.")
+                               (loop-maybe-check-screen-size)))))))))))))))
+
+(provide main)
+(define (main . args)
+  ;; TODO: Accept TTY as an argument.
+  (let ((tty     #f)
+        (escape? #t))
+    (command-line #:program "(charterm Demo)"
+                  #:once-each
+                  (("--tty" "-t") arg "The TTY to use (default: /dev/tty)." (set! tty arg))
+                  #:once-any
+                  (("--escape"    "-e") "Esc key quits program (default)." (set! escape? #t))
+                  (("--no-escape" "-n") "Esc key does not quit program."   (set! escape? #f)))
+    (charterm-demo #:tty     tty
+                   #:escape? escape?)))
diff --git a/archive/1.vm.arc/charterm/doc.scrbl b/archive/1.vm.arc/charterm/doc.scrbl
new file mode 100644
index 00000000..67040691
--- /dev/null
+++ b/archive/1.vm.arc/charterm/doc.scrbl
@@ -0,0 +1,7 @@
+#lang scribble/manual
+@; THIS-FILE-WAS-GENERATED-BY-MCFLY-TOOLS (planet neil/mcfly-tools:1:=12)
+@(require (for-syntax   racket/base)
+          (for-template racket/base)
+          (planet neil/mcfly:1:3/mcfly-scribble)
+          (planet neil/mcfly:1:3/mcfly-expand))
+@(mcfly-expand "charterm.rkt")
diff --git a/archive/1.vm.arc/charterm/info.rkt b/archive/1.vm.arc/charterm/info.rkt
new file mode 100644
index 00000000..64eeaefe
--- /dev/null
+++ b/archive/1.vm.arc/charterm/info.rkt
@@ -0,0 +1,29 @@
+#lang setup/infotab
+
+(define mcfly-planet       'neil/charterm:3:1)
+(define name               "CharTerm")
+(define mcfly-subtitle     "Character-cell Terminal Interface in Racket")
+(define blurb              (list name ": Character-cell Terminal Interface"))
+(define homepage           "http://www.neilvandyke.org/racket-charterm/")
+(define mcfly-author       "Neil Van Dyke")
+(define repositories       '("4.x"))
+(define categories         '(misc))
+(define can-be-loaded-with 'all)
+(define scribblings        '(("doc.scrbl" () (library))))
+(define primary-file       "main.rkt")
+(define mcfly-start        "charterm.rkt")
+(define mcfly-files        '(defaults
+                              "charterm.rkt"
+                              "demo.rkt"
+                              "test-charterm.rkt"))
+(define mcfly-license      "LGPLv3")
+
+(define mcfly-legal
+    "Copyright 2012 -- 2013 Neil Van Dyke.  This program is Free Software; you
+can redistribute it and/or modify it under the terms of the GNU Lesser General
+Public License as published by the Free Software Foundation; either version 3
+of the License, or (at your option) any later version.  This program is
+distributed in the hope that it will be useful, but without any warranty;
+without even the implied warranty of merchantability or fitness for a
+particular purpose.  See http://www.gnu.org/licenses/ for details.  For other
+licenses and consulting, please contact the author.")
diff --git a/archive/1.vm.arc/charterm/main.rkt b/archive/1.vm.arc/charterm/main.rkt
new file mode 100644
index 00000000..5566a73f
--- /dev/null
+++ b/archive/1.vm.arc/charterm/main.rkt
@@ -0,0 +1,3 @@
+#lang racket/base
+(require "charterm.rkt")
+(provide (all-from-out "charterm.rkt"))
diff --git a/archive/1.vm.arc/charterm/planet-docs/doc/index.html b/archive/1.vm.arc/charterm/planet-docs/doc/index.html
new file mode 100644
index 00000000..79d311c9
--- /dev/null
+++ b/archive/1.vm.arc/charterm/planet-docs/doc/index.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8" /><title>CharTerm: Character-cell Terminal Interface in Racket</title><link rel="stylesheet" type="text/css" href="scribble.css" title="default" /><link rel="stylesheet" type="text/css" href="racket.css" title="default" /><link rel="stylesheet" type="text/css" href="scribble-style.css" title="default" /><script type="text/javascript" src="scribble-common.js"></script><!--[if IE 6]><style type="text/css">.SIEHidden { overflow: hidden; }</style><![endif]--></head><body id="scribble-racket-lang-org"><div class="tocset"><div class="tocview"><div class="tocviewlist" style="margin-bottom: 1em;"><div class="tocviewtitle"><table cellspacing="0" cellpadding="0"><tr><td style="width: 1em;"><a href="javascript:void(0);" title="Expand/Collapse" class="tocviewtoggle" onclick="TocviewToggle(this,&quot;tocview_0&quot;);">&#9658;</a></td><td></td><td><a href="/" class="tocviewselflink" data-pltdoc="x">Char<span class="mywbr"> &nbsp;</span>Term:<span class="mywbr"> &nbsp;</span> Character-<wbr></wbr>cell Terminal Interface in Racket</a></td></tr></table></div><div class="tocviewsublistonly" style="display: none;" id="tocview_0"><table cellspacing="0" cellpadding="0"><tr><td align="right">1&nbsp;</td><td><a href="/#%28part._.Introduction%29" class="tocviewlink" data-pltdoc="x">Introduction</a></td></tr><tr><td align="right">2&nbsp;</td><td><a href="/#%28part._.Terminal_.Diversity%29" class="tocviewlink" data-pltdoc="x">Terminal Diversity</a></td></tr><tr><td align="right">3&nbsp;</td><td><a href="/#%28part._charterm_.Object%29" class="tocviewlink" data-pltdoc="x"><span class="RktSym">charterm</span><span class="RktMeta"></span> Object</a></td></tr><tr><td align="right">4&nbsp;</td><td><a href="/#%28part._.Terminal_.Information%29" class="tocviewlink" data-pltdoc="x">Terminal Information</a></td></tr><tr><td align="right">5&nbsp;</td><td><a href="/#%28part._.Display_.Control%29" class="tocviewlink" data-pltdoc="x">Display Control</a></td></tr><tr><td align="right">6&nbsp;</td><td><a href="/#%28part._.Keyboard_.Input%29" class="tocviewlink" data-pltdoc="x">Keyboard Input</a></td></tr><tr><td align="right">7&nbsp;</td><td><a href="/#%28part._.References%29" class="tocviewlink" data-pltdoc="x">References</a></td></tr><tr><td align="right">8&nbsp;</td><td><a href="/#%28part._.Known_.Issues%29" class="tocviewlink" data-pltdoc="x">Known Issues</a></td></tr><tr><td align="right">9&nbsp;</td><td><a href="/#%28part._.History%29" class="tocviewlink" data-pltdoc="x">History</a></td></tr><tr><td align="right">10&nbsp;</td><td><a href="/#%28part._.Legal%29" class="tocviewlink" data-pltdoc="x">Legal</a></td></tr></table></div></div></div><div class="tocsub"><table class="tocsublist" cellspacing="0"><tr><td><span class="tocsublinknumber">1<tt>&nbsp;</tt></span><a href="#(part._.Introduction)" class="tocsubseclink" data-pltdoc="x">Introduction</a></td></tr><tr><td><span class="tocsublinknumber">1.1<tt>&nbsp;</tt></span><a href="#(part._.Demo)" class="tocsubseclink" data-pltdoc="x">Demo</a></td></tr><tr><td><span class="tocsublinknumber">1.2<tt>&nbsp;</tt></span><a href="#(part._.Simple_.Example)" class="tocsubseclink" data-pltdoc="x">Simple Example</a></td></tr><tr><td><span class="tocsublinknumber">2<tt>&nbsp;</tt></span><a href="#(part._.Terminal_.Diversity)" class="tocsubseclink" data-pltdoc="x">Terminal Diversity</a></td></tr><tr><td><span class="tocsublinknumber">2.1<tt>&nbsp;</tt></span><a href="#(part._.Protocol)" class="tocsubseclink" data-pltdoc="x">Protocol</a></td></tr><tr><td><span class="tocsublinknumber">2.2<tt>&nbsp;</tt></span><a href="#(part._.Key_.Encoding)" class="tocsubseclink" data-pltdoc="x">Key Encoding</a></td></tr><tr><td><span class="tocsublinknumber">2.2.1<tt>&nbsp;</tt></span><a href="#(part._.Keylabel)" class="tocsubseclink" data-pltdoc="x">Keylabel</a></td></tr><tr><td><span class="tocsublinknumber">2.2.2<tt>&nbsp;</tt></span><a href="#(part._.Keycode)" class="tocsubseclink" data-pltdoc="x">Keycode</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keycode~3f))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keycode?</span></span></a></td></tr><tr><td><span class="tocsublinknumber">2.2.3<tt>&nbsp;</tt></span><a href="#(part._.Keyinfo)" class="tocsubseclink" data-pltdoc="x">Keyinfo</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo~3f))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyinfo?</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-keyset-id))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyinfo-<wbr></wbr>keyset-<wbr></wbr>id</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-bytelang))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyinfo-<wbr></wbr>bytelang</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-bytelist))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyinfo-<wbr></wbr>bytelist</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-keylabel))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyinfo-<wbr></wbr>keylabel</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-keycode))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyinfo-<wbr></wbr>keycode</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-all-keycodes))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyinfo-<wbr></wbr>all-<wbr></wbr>keycodes</span></span></a></td></tr><tr><td><span class="tocsublinknumber">2.2.4<tt>&nbsp;</tt></span><a href="#(part._.Keyset)" class="tocsubseclink" data-pltdoc="x">Keyset</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyset~3f))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyset?</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyset-id))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keyset-<wbr></wbr>id</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-ascii-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>ascii-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-dec-vt100-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>dec-<wbr></wbr>vt100-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-dec-vt220-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>dec-<wbr></wbr>vt220-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-screen-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>screen-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-linux-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>linux-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-x11r6-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>xterm-<wbr></wbr>x11r6-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-xfree86-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>xterm-<wbr></wbr>xfree86-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-new-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>xterm-<wbr></wbr>new-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-rxvt-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>rxvt-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-wyse-wy50-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>wyse-<wbr></wbr>wy50-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-televideo-925-keyset))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>televideo-<wbr></wbr>925-<wbr></wbr>keyset</span></span></a></td></tr><tr><td><span class="tocsublinknumber">2.2.5<tt>&nbsp;</tt></span><a href="#(part._.Keydec)" class="tocsubseclink" data-pltdoc="x">Keydec</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keydec-id))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keydec-<wbr></wbr>id</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-vt100-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>vt100-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-vt220-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>vt220-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-screen-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>screen-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-linux-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>linux-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-new-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>xterm-<wbr></wbr>new-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>xterm-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-rxvt-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>rxvt-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-wy50-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>wy50-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-tvi925-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>tvi925-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-ascii-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>ascii-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-ansi-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>ansi-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-insane-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>insane-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><span class="tocsublinknumber">2.3<tt>&nbsp;</tt></span><a href="#(part._.Termvar)" class="tocsubseclink" data-pltdoc="x">Termvar</a></td></tr><tr><td><span class="tocsublinknumber">3<tt>&nbsp;</tt></span><a href="#(part._charterm_.Object)" class="tocsubseclink" data-pltdoc="x"><span class="RktSym">charterm</span><span class="RktMeta"></span> Object</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm~3f))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm?</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-termvar))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>termvar</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-protocol))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>protocol</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keydec))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>keydec</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._current-charterm))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">current-<wbr></wbr>charterm</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._open-charterm))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">open-<wbr></wbr>charterm</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._close-charterm))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">close-<wbr></wbr>charterm</span></span></a></td></tr><tr><td><a href="#(form._((planet._main..rkt._(neil._charterm..plt._3._1))._with-charterm))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">with-<wbr></wbr>charterm</span></span></a></td></tr><tr><td><span class="tocsublinknumber">4<tt>&nbsp;</tt></span><a href="#(part._.Terminal_.Information)" class="tocsubseclink" data-pltdoc="x">Terminal Information</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-screen-size))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>screen-<wbr></wbr>size</span></span></a></td></tr><tr><td><span class="tocsublinknumber">5<tt>&nbsp;</tt></span><a href="#(part._.Display_.Control)" class="tocsubseclink" data-pltdoc="x">Display Control</a></td></tr><tr><td><span class="tocsublinknumber">5.1<tt>&nbsp;</tt></span><a href="#(part._.Cursor)" class="tocsubseclink" data-pltdoc="x">Cursor</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-cursor))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>cursor</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-newline))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>newline</span></span></a></td></tr><tr><td><span class="tocsublinknumber">5.2<tt>&nbsp;</tt></span><a href="#(part._.Displaying)" class="tocsubseclink" data-pltdoc="x">Displaying</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-display))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>display</span></span></a></td></tr><tr><td><span class="tocsublinknumber">5.3<tt>&nbsp;</tt></span><a href="#(part._.Video_.Attributes)" class="tocsubseclink" data-pltdoc="x">Video Attributes</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-normal))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>normal</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-inverse))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>inverse</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-underline))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>underline</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-blink))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>blink</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-bold))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>bold</span></span></a></td></tr><tr><td><span class="tocsublinknumber">5.4<tt>&nbsp;</tt></span><a href="#(part._.Clearing)" class="tocsubseclink" data-pltdoc="x">Clearing</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-clear-screen))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>clear-<wbr></wbr>screen</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-clear-line))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>clear-<wbr></wbr>line</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-clear-line-left))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>clear-<wbr></wbr>line-<wbr></wbr>left</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-clear-line-right))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>clear-<wbr></wbr>line-<wbr></wbr>right</span></span></a></td></tr><tr><td><span class="tocsublinknumber">5.5<tt>&nbsp;</tt></span><a href="#(part._.Line_.Insert_and_.Delete)" class="tocsubseclink" data-pltdoc="x">Line Insert and Delete</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-insert-line))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>insert-<wbr></wbr>line</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-delete-line))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>delete-<wbr></wbr>line</span></span></a></td></tr><tr><td><span class="tocsublinknumber">5.5.1<tt>&nbsp;</tt></span><a href="#(part._.Misc__.Output)" class="tocsubseclink" data-pltdoc="x">Misc. Output</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-bell))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>bell</span></span></a></td></tr><tr><td><span class="tocsublinknumber">6<tt>&nbsp;</tt></span><a href="#(part._.Keyboard_.Input)" class="tocsubseclink" data-pltdoc="x">Keyboard Input</a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-byte-ready~3f))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>byte-<wbr></wbr>ready?</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-read-key))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>read-<wbr></wbr>key</span></span></a></td></tr><tr><td><a href="#(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-read-keyinfo))" class="tocsubnonseclink" data-pltdoc="x"><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-<wbr></wbr>read-<wbr></wbr>keyinfo</span></span></a></td></tr><tr><td><span class="tocsublinknumber">7<tt>&nbsp;</tt></span><a href="#(part._.References)" class="tocsubseclink" data-pltdoc="x">References</a></td></tr><tr><td><span class="tocsublinknumber">8<tt>&nbsp;</tt></span><a href="#(part._.Known_.Issues)" class="tocsubseclink" data-pltdoc="x">Known Issues</a></td></tr><tr><td><span class="tocsublinknumber">9<tt>&nbsp;</tt></span><a href="#(part._.History)" class="tocsubseclink" data-pltdoc="x">History</a></td></tr><tr><td><span class="tocsublinknumber">10<tt>&nbsp;</tt></span><a href="#(part._.Legal)" class="tocsubseclink" data-pltdoc="x">Legal</a></td></tr></table></div></div><div class="maincolumn"><div class="main"><div class="versionbox"><span class="versionNoNav">3:1</span></div><h2><a name="(part._.Char.Term__.Character-cell_.Terminal_.Interface_in_.Racket)"></a><a name="(mod-path._(planet._neil/charterm~3a3~3a1))"></a>CharTerm: Character-cell Terminal Interface in Racket</h2><div class="SAuthorListBox"><span class="SAuthorList"><p class="author">Neil Van Dyke</p></span></div><p><div class="SIntrapara"></div><div class="SIntrapara">License: <a href="/#%28part._.Legal%29" class="plainlink" data-pltdoc="x">LGPLv3</a> <span class="hspace">&nbsp;</span> Web: <a href="http://www.neilvandyke.org/racket-charterm/" class="plainlink">http://www.neilvandyke.org/racket-charterm/</a>
+</div><div class="SIntrapara"><table cellspacing="0" class="defmodule"><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym"><a href="/servlets/doc-search.rkt?tag=KCgzKSAwICgpIDAgKCkgKCkgKHEgZm9ybSAoKGxpYiAicmFja2V0L3ByaXZhdGUvYmFzZS5y%0D%0Aa3QiKSByZXF1aXJlKSkp%0D%0A" class="RktStxLink" data-pltdoc="x">require</a></span><span class="stt"> </span><span class="RktPn">(</span><span class="RktSym">planet</span><span class="stt"> </span><span class="RktSym">neil/charterm:3:1</span><span class="RktPn">)</span><span class="RktPn">)</span></td></tr></table></div></p><h3>1<tt>&nbsp;</tt><a name="(part._.Introduction)"></a>Introduction</h3><p><div class="SIntrapara">The CharTerm package provides a Racket interface for character-cell video
+display terminals on Unix-like systems &ndash; such as for <a name="(idx._(gentag._0))"></a>GNU Screen and <a name="(idx._(gentag._1))"></a><span class="RktSym">tmux</span><span class="RktMeta"></span> sessions on <a name="(idx._(gentag._2))"></a>cloud servers, <a name="(idx._(gentag._3))"></a>XTerm windows on a workstation desktop, and some older hardware
+terminals (even the venerable <a name="(idx._(gentag._4))"></a>DEC VT100).  Currently, it implements a subset of features available on most
+terminals.</div><div class="SIntrapara">This package could be used to implement a status/management console
+for a Racket-based server process (perhaps run in GNU Screen or <span class="RktSym">tmux</span><span class="RktMeta"></span> on a server machine, to be detached and reattached from SSH
+sessions), a lightweight user interface for a systems tool, a command-line
+REPL, a text editor, creative retro uses of old equipment, and, perhaps most
+importantly, a Rogue-like application.</div><div class="SIntrapara">The CharTerm package does not include any native code (such as from <a name="(idx._(gentag._5))"></a><span class="RktSym">terminfo</span><span class="RktMeta"></span>, <a name="(idx._(gentag._6))"></a><span class="RktSym">termcap</span><span class="RktMeta"></span>, <a name="(idx._(gentag._7))"></a><span class="RktSym">curses</span><span class="RktMeta"></span>, or <a name="(idx._(gentag._8))"></a><span class="RktSym">ncurses</span><span class="RktMeta"></span>) in the Racket process,
+such as through the Racket FFI or C extensions, so there is less potential for
+a problem involving native code to threaten the reliability or security of a
+program.  CharTerm is implemented in pure Racket code except for executing <span class="RktSym">/bin/stty</span><span class="RktMeta"></span> for some purposes.  Specifically, <span class="RktSym">/bin/stty</span><span class="RktMeta"></span> at startup time and shutdown time, to set modes, and (for terminal
+types that don&rsquo;t seem to support a screen size report control sequence) when
+getting screen size.  Besides security and stability, lower dependence on
+native code might also simplify porting to host platforms that don&rsquo;t have those
+native code facilities.</div></p><h4>1.1<tt>&nbsp;</tt><a name="(part._.Demo)"></a>Demo</h4><p><div class="SIntrapara">For a demonstration, the following command, run from a terminal, should install the CharTerm package (if not already installed), and run the demo:</div><div class="SIntrapara"><span class="hspace">&nbsp;&nbsp;</span><span class="stt">racket -pm neil/charterm/demo</span></div><div class="SIntrapara">This demo reports what keys you pressed, while letting you edit a
+text field, and while displaying a clock.  The clock is updated roughly once
+per second, and is not updated during heavy keyboard input, such as when typing
+fast.  The demo responds to changing terminal sizes, such as when an XTerm is
+window is resized.  It also displays the determined terminal size, and some
+small tests of the <span class="RktPn">#:width</span> argument to <span class="RktSym">charterm-display</span>.  Exit the demo by pressing the <span style="font-weight: bold">Esc</span> key.</div><div class="SIntrapara">Note: Although this demo includes an editable text field, as proof
+of concept, the current version of CharTerm does not provide editable text fields as reusable functionality.</div></p><h4>1.2<tt>&nbsp;</tt><a name="(part._.Simple_.Example)"></a>Simple Example</h4><p><div class="SIntrapara">Here&rsquo;s your first CharTerm program:</div><div class="SIntrapara"><blockquote class="SCodeFlow"><table cellspacing="0" class="RktBlk"><tr><td><span class="RktMeta">#lang</span><span class="hspace">&nbsp;</span><span class="RktMeta"></span><a href="/servlets/doc-search.rkt?tag=KCgzKSAwICgpIDAgKCkgKCkgKHEgbW9kLXBhdGggInJhY2tldC9iYXNlIikp%0D%0A" class="RktModLink" data-pltdoc="x"><span class="RktSym">racket/base</span></a><span class="RktMeta"></span></td></tr><tr><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="RktPn">(</span><span class="RktSym">require</span><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">planet</span><span class="hspace">&nbsp;</span><span class="RktSym">neil/charterm</span><span class="RktPn">)</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="RktPn">(</span><span class="RktSym">with-charterm</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-clear-screen</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-cursor</span><span class="hspace">&nbsp;</span><span class="RktVal">10</span><span class="hspace">&nbsp;</span><span class="RktVal">5</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-display</span><span class="hspace">&nbsp;</span><span class="RktVal">"Hello, "</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-bold</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-display</span><span class="hspace">&nbsp;</span><span class="RktVal">"you"</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-normal</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-display</span><span class="hspace">&nbsp;</span><span class="RktVal">"."</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-cursor</span><span class="hspace">&nbsp;</span><span class="RktVal">1</span><span class="hspace">&nbsp;</span><span class="RktVal">1</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-display</span><span class="hspace">&nbsp;</span><span class="RktVal">"Press a key..."</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">let</span><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktPn">(</span><span class="RktSym">key</span><span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-read-key</span><span class="RktPn">)</span><span class="RktPn">)</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-cursor</span><span class="hspace">&nbsp;</span><span class="RktVal">1</span><span class="hspace">&nbsp;</span><span class="RktVal">1</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;&nbsp;</span><span class="RktPn">(</span><span class="RktSym">charterm-clear-line</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;&nbsp;</span><span class="RktPn">(</span><span class="RktSym">printf</span><span class="hspace">&nbsp;</span><span class="RktVal">"You pressed: ~S\r\n"</span><span class="hspace">&nbsp;</span><span class="RktSym">key</span><span class="RktPn">)</span><span class="RktPn">)</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Now you&rsquo;re living the dream of the &rsquo;70s.</div></p><h3>2<tt>&nbsp;</tt><a name="(part._.Terminal_.Diversity)"></a>Terminal Diversity</h3><p><div class="SIntrapara">Like people, few terminals are exactly the same.</div><div class="SIntrapara">Some key (ha) terms (ha) used by CharTerm are:</div><div class="SIntrapara"><ul><li><p><a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> &#8212;<wbr></wbr> a string value like from the Unix-like <span class="RktSym">TERM</span><span class="RktMeta"></span> environment variable, used to determine a default <a href="/#%28tech._protocol%29" class="techoutside" data-pltdoc="x"><span class="techinside">protocol</span></a> and <a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">keydec</span></a>.</p></li><li><p><a href="/#%28tech._protocol%29" class="techoutside" data-pltdoc="x"><span class="techinside">protocol</span></a> &#8212;<wbr></wbr> how to control the display, query for information, etc.</p></li><li><p><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">keydec</span></a> &#8212;<wbr></wbr> how to decode key encodings of a particular
+terminal.  A keydec is constructed from one or more keysets, can produce <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a>s or <a href="/#%28tech._keyinfo%29" class="techoutside" data-pltdoc="x"><span class="techinside">keyinfo</span></a>s.</p></li><li><p><a href="/#%28tech._keyset%29" class="techoutside" data-pltdoc="x"><span class="techinside">keyset</span></a> &#8212;<wbr></wbr> a specification of encoding some of the keys in a
+particular terminal, including <a href="/#%28tech._keylabel%29" class="techoutside" data-pltdoc="x"><span class="techinside">keylabel</span></a>s and <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a>s.</p></li><li><p><a href="/#%28tech._keylabel%29" class="techoutside" data-pltdoc="x"><span class="techinside">keylabel</span></a> &#8212;<wbr></wbr> a string for how a key is likely labeled on a
+keyboard, such as the DEC VT100 <span style="font-weight: bold">PF1</span> key would have a keylabel <span class="RktVal">"PF1"</span> for a <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a> <span class="RktVal">'</span><span class="RktVal">f1</span>.</p></li><li><p><a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a> &#8212;<wbr></wbr> a value produced by a decoded key,
+such as a character for normal printable keys, like <span class="RktVal">#\a</span> and <span class="RktVal">#\space</span>, a symbol for some recognized unprintable keys, like <span class="RktVal">'</span><span class="RktVal">escape</span> and <span class="RktVal">'</span><span class="RktVal">f1</span>, or possibly a number for unrecognized keys.</p></li><li><p><a href="/#%28tech._keyinfo%29" class="techoutside" data-pltdoc="x"><span class="techinside">keyinfo</span></a> &#8212;<wbr></wbr> an object that is used like a <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a>, except
+bundles together a keycode and a <a href="/#%28tech._keylabel%29" class="techoutside" data-pltdoc="x"><span class="techinside">keylabel</span></a>, as well as alternatate keycodes and
+information about how the key was decoded (e.g., from which <a href="/#%28tech._keyset%29" class="techoutside" data-pltdoc="x"><span class="techinside">keyset</span></a>).</p></li></ul></div><div class="SIntrapara">These terms are discussed in the following subsections.</div><div class="SIntrapara">CharTerm is developed with help of original documentation such as that
+curated by Paul Williams at <a href="http://vt100.net/">vt100.net</a>, various commentary found on the Web, observed behavior with
+modern software terminals like XTerm, various emulators for hardware terminals,
+and sometimes original hardware terminals.  Thanks to Mark Pearrow for
+contributing a TeleVideo 950, and Paul McCabe for a Wyse S50 WinTerm.</div><div class="SIntrapara">At time of this writing, the author is looking to acquire a DEC
+VT525, circa 1994, for ongoing testing.</div><div class="SIntrapara">The author welcomes feedback on useful improvements to CharTerm&rsquo;s support for terminal diversity (no pun).  If you have a terminal
+that is sending an escape sequence not recognized by the demo, you can run the
+demo with the <span class="nobreak"><span class="stt">-n</span></span> (aka <span class="nobreak"><span class="stt">--no-escape</span></span>) argument to see the exact byte sequence:</div><div class="SIntrapara"><span class="hspace">&nbsp;&nbsp;</span><span class="stt">racket -pm- neil/charterm/demo -n</span></div><div class="SIntrapara">When <span class="nobreak"><span class="stt">-n</span></span> is used, this will be indicated by the bottom-most scrolling line,
+rather than saying &ldquo;<span class="stt">To quit, press </span><span style="font-weight: bold">Esc</span><span class="stt">.</span>&rdquo; instead will say &ldquo;<span class="stt">There is no escape from this demo.</span>&rdquo; You will have to kill the process through some other means.</div></p><h4>2.1<tt>&nbsp;</tt><a name="(part._.Protocol)"></a>Protocol</h4><p><div class="SIntrapara">The first concept CharTerm has for distinguishing how to communicate with a terminal is what
+is what is called here <a name="(tech._protocol)"></a><span style="font-style: italic">protocol</span>, which concerns everything except how keyboard keys are decoded.
+The following protocols are currently implemented:</div><div class="SIntrapara"><ul><li><p><a name="(tech._ansi._protocol)"></a><span style="font-style: italic"><span class="RktSym">ansi</span><span class="RktMeta"></span> protocol</span> &#8212;<wbr></wbr> Terminals approximating [<a href="/#%28tech._ansi._x3..64%29" class="techoutside" data-pltdoc="x"><span class="techinside">ANSI X3.64</span></a>], which is most terminals in use today, including software ones
+like XTerm.  This protocol is the emphasis of this package; the other protocols
+are for unusual situations.</p></li><li><p><a name="(tech._wyse._wy50._protocol)"></a><span style="font-style: italic"><span class="RktSym">wyse-wy50</span><span class="RktMeta"></span> protocol</span> &#8212;<wbr></wbr> Terminals compatible with the Wyse WY-50.  This support is
+based on [<a href="/#%28tech._wy._50._qrg%29" class="techoutside" data-pltdoc="x"><span class="techinside">WY-50-QRG</span></a>], [<a href="/#%28tech._wy._60._ug%29" class="techoutside" data-pltdoc="x"><span class="techinside">WY-60-UG</span></a>], [<a href="/#%28tech._wy60%29" class="techoutside" data-pltdoc="x"><span class="techinside">wy60</span></a>], and [<a href="/#%28tech._powerterm%29" class="techoutside" data-pltdoc="x"><span class="techinside">PowerTerm</span></a>].  Note that video attributes are not supported, due to the WY-50&rsquo;s
+model of having video attribute changes occupy character cells; you may wish
+to run the Wyse terminal in an ANSI or VT100 mode.</p></li><li><p><a name="(tech._televideo._925._protocol)"></a><span style="font-style: italic"><span class="RktSym">televideo-925</span><span class="RktMeta"></span> protocol</span> &#8212;<wbr></wbr> Terminals compatible with the TeleVideo 925.  This support is based on [<a href="/#%28tech._tvi._925._iug%29" class="techoutside" data-pltdoc="x"><span class="techinside">TVI-925-IUG</span></a>] and behavior of [<a href="/#%28tech._powerterm%29" class="techoutside" data-pltdoc="x"><span class="techinside">PowerTerm</span></a>].  Note that video attributes are not supported, due to the 925&rsquo;s
+model of having video attribute changes occupy character cells; you may wish to
+run your TeleVideo terminal in ANSI or VT100 mode, if it has one.</p></li><li><p><a name="(tech._ascii._protocol)"></a><span style="font-style: italic"><span class="RktSym">ascii</span><span class="RktMeta"></span> protocol</span> &#8212;<wbr></wbr> Terminals that support ASCII but not much else that we know about.</p></li></ul></div></p><h4>2.2<tt>&nbsp;</tt><a name="(part._.Key_.Encoding)"></a>Key Encoding</h4><p><div class="SIntrapara">While most video display control, they seem to vary more by key
+encoding.</div><div class="SIntrapara">The CharTerm author was motivated to increase the sophistication of its
+keyboard handling after a series of revelations on the Sunday of the long
+weekend in which CharTerm was initially written.  The first was discovering that four of the
+function keys that had been working fine in <span class="RktSym">rxvt</span><span class="RktMeta"></span> did not work in XTerm.  Dave Gilbert somewhat demystified this by
+pointing out that the original VT100 had only four function keys, which set
+into motion an unfortunate series of bad decisions by various developers of
+terminal software to be needlessly incompatible with each other.  After
+Googling, a horrifying 2005 Web post by Phil Gregory [<a href="/#%28tech._gregory%29" class="techoutside" data-pltdoc="x"><span class="techinside">Gregory</span></a>], which showed that key encoding among XTerm variants was even
+worse than one could ever fear.  Even if one already knew how much subtleties
+of old terminals varied (e.g., auto-newline behavior, whether an attribute
+change consumed a space, etc.), this incompatibility in newer software was
+surprising. Then, on a hunch, I tried the Linux Console on a Debian Squeeze
+machine, which surely is ANSI, and found, however, that it generated <span style="font-style: italic">yet different</span> byte sequences, for the first <span style="font-style: italic">five</span> (not four) function keys.  Then I compared all to the [<a href="/#%28tech._ecma._48%29" class="techoutside" data-pltdoc="x"><span class="techinside">ECMA-48</span></a>] standard, which turns out to be nigh-inscrutable, so which might
+help explain why everyone became so anti-social.</div><div class="SIntrapara">CharTerm now provides the abstractions of <a href="/#%28tech._keyset%29" class="techoutside" data-pltdoc="x"><span class="techinside">keysets</span></a> and <a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">keydecs</span></a> to deal with this diversity in a maintainable way.</div></p><h5>2.2.1<tt>&nbsp;</tt><a name="(part._.Keylabel)"></a>Keylabel</h5><p>A <a name="(tech._keylabel)"></a><span style="font-style: italic">keylabel</span> is a Racket string for how a key is likely labeled on a particular terminal&rsquo;s keyboard.  Different keyboards may have different keylabels for the same <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a>.  For example, a VT100 has a <span style="font-weight: bold">PF1</span> key (keylabel <span class="RktVal">"PF1"</span>, keycode <span class="RktVal">'</span><span class="RktVal">f1</span>), while many other keyboards would label the key <span style="font-weight: bold">F1</span> (keylabel <span class="RktVal">"F1"</span>, keycode <span class="RktVal">'</span><span class="RktVal">f1</span>).  The keylabel currently is most useful for documenting and debugging, although it could later be used when giving instructions to the user, such as knowing whether to tell the user the <span style="font-weight: bold">Return</span> key or the <span style="font-weight: bold">Enter</span> key; the <span style="font-weight: bold">Backspace</span> or the <span style="font-weight: bold">Rubout</span> key; etc.</p><h5>2.2.2<tt>&nbsp;</tt><a name="(part._.Keycode)"></a>Keycode</h5><p><div class="SIntrapara">A <a name="(tech._keycode)"></a><span style="font-style: italic">keycode</span> is a value representing a key read from a terminal, which can be a Racket character, symbol, or number.  Keys corresponding to printable characters have keycodes as Racket characters.  Some keys corresponding to special non-printable characters can have keycodes of Racket symbols, such as <span class="RktVal">'</span><span class="RktVal">return</span>, <span class="RktVal">'</span><span class="RktVal">f1</span>, <span class="RktVal">'</span><span class="RktVal">up</span>, etc.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keycode~3f))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keycode?</span></span><span class="hspace">&nbsp;</span><span class="RktVar">x</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">boolean?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">x</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">any/c</span></td></tr></table></blockquote></div><div class="SIntrapara">Predicate for whether or not <span class="RktVar">x</span> is a valid keycode.</div></p><h5>2.2.3<tt>&nbsp;</tt><a name="(part._.Keyinfo)"></a>Keyinfo</h5><p><div class="SIntrapara">A <a name="(tech._keyinfo)"></a><span style="font-style: italic">keyinfo</span> represents a <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a> for a key, a <a href="/#%28tech._keylabel%29" class="techoutside" data-pltdoc="x"><span class="techinside">keylabel</span></a>, and how it is encoded as bytes.  It is represented in Racket as
+a <span class="RktSym">charterm-keyinfo</span> object.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo~3f))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyinfo?</span></span><span class="hspace">&nbsp;</span><span class="RktVar">x</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">boolean?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">x</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">any/c</span></td></tr></table></blockquote></div><div class="SIntrapara">Predicate for whether or not <span class="RktSym">x</span> is a <span class="RktSym">charterm-keyinfo</span> object.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-keyset-id))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyinfo-keyset-id</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ki</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">symbol?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ki</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyinfo?</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-bytelang))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyinfo-bytelang</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ki</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">string?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ki</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyinfo?</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-bytelist))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyinfo-bytelist</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ki</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">listof</span><span class="hspace">&nbsp;</span><span class="RktSym">byte?</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ki</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyinfo?</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-keylabel))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyinfo-keylabel</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ki</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">string?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ki</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyinfo?</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-keycode))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyinfo-keycode</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ki</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keycode?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ki</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyinfo?</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyinfo-all-keycodes))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyinfo-all-keycodes</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ki</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">listof</span><span class="hspace">&nbsp;</span><span class="RktSym">charterm-keycode?</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ki</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyinfo?</span></td></tr></table></blockquote></div><div class="SIntrapara">Get information from a <span class="RktSym">charterm-keyinfo</span> object.</div></p><h5>2.2.4<tt>&nbsp;</tt><a name="(part._.Keyset)"></a>Keyset</h5><p><div class="SIntrapara">A <a name="(tech._keyset)"></a><span style="font-style: italic">keyset</span> is a specification of keys on a particular keyboard, including their <a href="/#%28tech._keylabel%29" class="techoutside" data-pltdoc="x"><span class="techinside">keylabel</span></a>, encoding as bytes, and primary and alternate <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycodes</span></a>.</div><div class="SIntrapara">The means of constructing a keyset is currently internal to this package.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyset~3f))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyset?</span></span><span class="hspace">&nbsp;</span><span class="RktVar">x</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">boolean?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">x</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">any/c</span></td></tr></table></blockquote></div><div class="SIntrapara">Predicate for whether or not <span class="RktVar">x</span> is a keyset.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keyset-id))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keyset-id</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ks</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">symbol?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ks</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></td></tr></table></blockquote></div><div class="SIntrapara">Get a symbol identifying the keyset.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-ascii-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-ascii-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the old [<a href="/#%28tech._ascii%29" class="techoutside" data-pltdoc="x"><span class="techinside">ASCII</span></a>] standard.  When defining a <a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">keydec</span></a>, this is good to have as a final keyset, after the others.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-dec-vt100-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-dec-vt100-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the DEC VT100.  This currently defines the four function
+keys (labeled on the keyboard, <span style="font-weight: bold">PF1</span> through <span style="font-weight: bold">PF4</span>) as <span class="RktVal">'</span><span class="RktVal">f1</span> through <span class="RktVal">'</span><span class="RktVal">f4</span>, and the arrow keys.  [<a href="/#%28tech._vt100._ug%29" class="techoutside" data-pltdoc="x"><span class="techinside">VT100-UG</span></a>] and [<a href="/#%28tech._powerterm%29" class="techoutside" data-pltdoc="x"><span class="techinside">PowerTerm</span></a>] were used as references.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-dec-vt220-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-dec-vt220-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the DEC VT220.  This currently defines function keys <span style="font-weight: bold">F1</span> through <span style="font-weight: bold">F20</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-screen-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-screen-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the <a href="http://en.wikipedia.org/wiki/GNU_Screen">GNU Screen</a> terminal multiplexer, according to [<a href="/#%28tech._gregory%29" class="techoutside" data-pltdoc="x"><span class="techinside">Gregory</span></a>].  Also used by <a href="http://en.wikipedia.org/wiki/Tmux"><span class="RktSym">tmux</span><span class="RktMeta"></span></a>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-linux-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-linux-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the Linux console.  Currently defines function keys <span style="font-weight: bold">F1</span> through <span style="font-weight: bold">F5</span> only, since the rest will be inherited from other keysets.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-x11r6-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-xterm-x11r6-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the XTerm in X11R6, according to [<a href="/#%28tech._gregory%29" class="techoutside" data-pltdoc="x"><span class="techinside">Gregory</span></a>].</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-xfree86-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-xterm-xfree86-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the XFree86 XTerm, according to [<a href="/#%28tech._gregory%29" class="techoutside" data-pltdoc="x"><span class="techinside">Gregory</span></a>].</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-new-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-xterm-new-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the current <span class="RktSym">xterm-new</span><span class="RktMeta"></span>, often called simply <span class="RktSym">xterm</span><span class="RktMeta"></span>, as developed by Thomas E. Dickey, and documented in [<a href="/#%28tech._xterm._ctlseq%29" class="techoutside" data-pltdoc="x"><span class="techinside">XTerm-ctlseqs</span></a>].  Several also came from decompiling a <span class="RktSym">terminfo</span><span class="RktMeta"></span> entry.  Thanks to Dickey for his emailed help.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-rxvt-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-rxvt-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the <a href="http://en.wikipedia.org/wiki/Rxvt"><span class="RktSym">rxvt</span><span class="RktMeta"></span></a> terminal emulator.  These come from [<a href="/#%28tech._gregory%29" class="techoutside" data-pltdoc="x"><span class="techinside">Gregory</span></a>], and
+currently define function keys <span class="RktVal">'</span><span class="RktVal">f1</span> through <span class="RktVal">'</span><span class="RktVal">f44</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-wyse-wy50-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-wyse-wy50-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the Wyse WY-50, based on [<a href="/#%28tech._wy._50._qrg%29" class="techoutside" data-pltdoc="x"><span class="techinside">WY-50-QRG</span></a>] and looking at photos of WY-50 keyboard, and tested in [<a href="/#%28tech._wy60%29" class="techoutside" data-pltdoc="x"><span class="techinside">wy60</span></a>] and [<a href="/#%28tech._powerterm%29" class="techoutside" data-pltdoc="x"><span class="techinside">PowerTerm</span></a>].  The shifted function keys are provided as both <span class="RktVal">'</span><span class="RktVal">shift-f1</span> through <span class="RktVal">'</span><span class="RktVal">shift-16</span>, and <span class="RktVal">'</span><span class="RktVal">f17</span> through <span class="RktVal">'</span><span class="RktVal">f31</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-televideo-925-keyset))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-televideo-925-keyset</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keyset?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">From the TeleVideo 925, based on [<a href="/#%28tech._tvi._925._iug%29" class="techoutside" data-pltdoc="x"><span class="techinside">TVI-925-IUG</span></a>], [<a href="/#%28tech._powerterm%29" class="techoutside" data-pltdoc="x"><span class="techinside">PowerTerm</span></a>], and from looking at a TeleVideo 950 keyboard.</div></p><h5>2.2.5<tt>&nbsp;</tt><a name="(part._.Keydec)"></a>Keydec</h5><p><div class="SIntrapara">A <a name="(tech._keydec)"></a><span style="font-style: italic">keydec</span> object is a key decoder for a specific variety of terminal, such
+as for a specific <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a>.  A keydec is used to turn received key encodings from a terminal into <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a> or <a href="/#%28tech._keyinfo%29" class="techoutside" data-pltdoc="x"><span class="techinside">keyinfo</span></a> values.  A keydec is constructed from a prioritized list of <a href="/#%28tech._keyset%29" class="techoutside" data-pltdoc="x"><span class="techinside">keyset</span></a> objects, with earlier-listed keysets taking priority of
+later-listed keysets when there is conflict between them as to how to decode a
+particular byte sequence.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keydec-id))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keydec-id</span></span><span class="hspace">&nbsp;</span><span class="RktVar">kd</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">symbol?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">kd</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></td></tr></table></blockquote></div><div class="SIntrapara">Gets the ID symbol of the <a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">keydec</span></a> being used.</div><div class="SIntrapara"><span class="SSubSubSubSection">ANSI Keydecs</span></div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-vt100-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-vt100-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"vt100"</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-vt220-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-vt220-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"vt220"</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-screen-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-screen-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"screen"</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-linux-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-linux-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"linux"</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-new-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-xterm-new-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"xterm-new"</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-xterm-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-xterm-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"xterm"</span>.  Currently same as the keydec for <span class="RktSym">xterm</span><span class="RktMeta"></span>, except for a different ID.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-rxvt-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-rxvt-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"rxvt"</span>.</div><div class="SIntrapara"><span class="SSubSubSubSection">Wyse Keydecs</span></div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-wy50-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-wy50-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"wy50"</span>.</div><div class="SIntrapara"><span class="SSubSubSubSection">TeleVideo Keydecs</span></div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-tvi925-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-tvi925-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"tvi925"</span>.</div><div class="SIntrapara"><span class="SSubSubSubSection">ASCII Keydecs</span></div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-ascii-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-ascii-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktVal">"ascii"</span>.</div><div class="SIntrapara"><span class="SSubSubSubSection">Default Keydecs</span></div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-ansi-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-ansi-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for any presumed ANSI-ish terminal, combining many ANSI-ish <a href="/#%28tech._keyset%29" class="techoutside" data-pltdoc="x"><span class="techinside">keysets</span></a>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>value</p></div></div><p class="RForeground"><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-insane-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-insane-keydec</span></span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm-keydec?</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara"><a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">Keydec</span></a> for the uniquely desperate situation of wanting to possibly have
+extensive key decoding for a terminal that might not even be ansi, but be
+Wyse, TeleVideo, or some other ASCII.</div></p><h4>2.3<tt>&nbsp;</tt><a name="(part._.Termvar)"></a>Termvar</h4><p>A <a name="(tech._termvar)"></a><span style="font-style: italic">termvar</span> is what the <span class="RktSym">charterm</span><span class="RktMeta"></span> package calls the value of the Unix-like <span class="RktSym">TERM</span><span class="RktMeta"></span> environment variable.  Each <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> has a default <a href="/#%28tech._protocol%29" class="techoutside" data-pltdoc="x"><span class="techinside">protocol</span></a> and <a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">keydec</span></a>.  Note, however, that <span class="RktSym">TERM</span><span class="RktMeta"></span> is not always a precise indicator of the best protocol and keydec,
+but by default we work with what we have.</p><h3>3<tt>&nbsp;</tt><a name="(part._charterm_.Object)"></a><span class="RktSym">charterm</span><span class="RktMeta"></span> Object</h3><p><div class="SIntrapara">The <span class="RktSym">charterm</span> object captures the state of a session with a particular terminal.</div><div class="SIntrapara">A <span class="RktSym">charterm</span> object is also a synchronizable event, so it can be used with
+procedures such as <span class="RktSym">sync</span>.  As an event, it becomes ready when there is at least one byte
+available for reading from the terminal, and its synchronization result is
+itself.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm~3f))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm?</span></span><span class="hspace">&nbsp;</span><span class="RktVar">x</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">boolean?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">x</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">any/c</span></td></tr></table></blockquote></div><div class="SIntrapara">Predicate for whether or not <span class="RktVar">x</span> is a <span class="RktSym">charterm</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-termvar))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-termvar</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">string?</span><span class="RktPn">)</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span></td></tr></table></blockquote></div><div class="SIntrapara">Gets the <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-protocol))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-protocol</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">symbol?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span></td></tr></table></blockquote></div><div class="SIntrapara">Gets the <a href="/#%28tech._protocol%29" class="techoutside" data-pltdoc="x"><span class="techinside">protocol</span></a>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-keydec))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-keydec</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">symbol?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span></td></tr></table></blockquote></div><div class="SIntrapara">Gets the <a href="/#%28tech._keydec%29" class="techoutside" data-pltdoc="x"><span class="techinside">keydec</span></a>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>parameter</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._current-charterm))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">current-charterm</span></span><span class="RktPn"></span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="RktPn">)</span></p></blockquote></td></tr><tr><td><span class="RktPn">(</span><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">current-charterm</span></span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym"><a href="/servlets/doc-search.rkt?tag=KCgzKSAwICgpIDAgKCkgKCkgKHEgZGVmICgocXVvdGUgIyVrZXJuZWwpIHZvaWQ%2FKSkp%0D%0A" class="RktValLink" data-pltdoc="x">void?</a></span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">This parameter provides the default <span class="RktSym">charterm</span> for most of the other procedures.  It is usually set automatically by <span class="RktSym">call-with-charterm</span>, <span class="RktSym">with-charterm</span>, <span class="RktSym">open-charterm</span>, and <span class="RktSym">close-charterm</span>.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><table cellspacing="0" class="prototype RForeground"><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._open-charterm))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">open-charterm</span></span></td><td><span class="hspace">&nbsp;</span>[</td><td><span class="RktPn">#:tty</span><span class="hspace">&nbsp;</span><span class="RktVar">tty</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="RktPn">#:current?</span><span class="hspace">&nbsp;</span><span class="RktVar">current?</span>]<span class="RktPn">)</span></td><td><span class="hspace">&nbsp;</span></td><td>&rarr;</td><td><span class="hspace">&nbsp;</span></td><td><span class="RktSym">charterm?</span></td></tr></table></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">tty</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">path-string?</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">#f</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">current?</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">boolean?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">#t</span></td></tr></table></blockquote></div><div class="SIntrapara">Returns an open <span class="RktSym">charterm</span> object, by opening I/O ports on the terminal device at <span class="RktVar">tty</span> (or, if <span class="RktVal">#f</span>, file <span class="stt">"/dev/tty"</span>), and setting raw mode and disabling echo (via <span class="stt">"/bin/stty"</span>).  If <span class="RktVar">current?</span> is true, the <span class="RktSym">current-charterm</span> parameter is also set to this object.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._close-charterm))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">close-charterm</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Closes <span class="RktVar">ct</span> by closing the I/O ports, and undoing <span class="RktSym">open-charterm</span>&rsquo;s changes via <span class="stt">"/bin/stty"</span>.  If <span class="RktSym">current-charterm</span> is set to <span class="RktVar">ct</span>, then that parameter will be changed to <span class="RktVal">#f</span> for good measure.  You might wish to use <span class="RktSym">with-charterm</span> instead of worrying about calling <span class="RktSym">close-charterm</span> directly.</div><div class="SIntrapara">Note: If you exit your Racket process without properly closing the <span class="RktSym">charterm</span>, your terminal may be left in a crazy state.  You can fix it with
+the command:</div><div class="SIntrapara"><span class="hspace">&nbsp;&nbsp;</span><span class="stt">stty sane</span></div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>syntax</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(form._((planet._main..rkt._(neil._charterm..plt._3._1))._with-charterm))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">with-charterm</span></span><span class="hspace">&nbsp;</span><span class="RktVar">expr?</span><span class="hspace">&nbsp;</span><span class="RktMeta">...</span><span class="RktPn">)</span></p></blockquote></td></tr></table></blockquote></div><div class="SIntrapara">Opens a <span class="RktSym">charterm</span> and evaluates the body expressions in sequence with <span class="RktSym">current-charterm</span> set appropriately.  When control jumps out of the body, in a
+manner of speaking, the <span class="RktSym">charterm</span> is closed.</div></p><h3>4<tt>&nbsp;</tt><a name="(part._.Terminal_.Information)"></a>Terminal Information</h3><p><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-screen-size))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-screen-size</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span></p></blockquote></td></tr><tr><td><table cellspacing="0" class="prototype"><tr><td><span class="hspace">&nbsp;</span></td><td>&rarr;</td><td><span class="hspace">&nbsp;</span></td><td><table cellspacing="0"><tr><td><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">exact-nonnegative-integer?</span><span class="RktPn">)</span></td></tr><tr><td><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">exact-nonnegative-integer?</span><span class="RktPn">)</span></td></tr></table></td></tr></table></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Attempts to get the screen size, in character columns and rows.
+It may do this through a control sequence or through <span class="RktSym">/bin/stty</span><span class="RktMeta"></span>.  If unable to get a value, then default of (80,24) is used.</div><div class="SIntrapara">The current behavior in this version of <span class="RktSym">charterm</span><span class="RktMeta"></span> is to adaptively try different methods of getting screen size,
+and to remember what worked for the next time this procedure is called for <span class="RktVar">ct</span>.  For terminals that are identified as <span class="RktSym">screen</span><span class="RktMeta"></span> by the <span class="RktSym">TERM</span><span class="RktMeta"></span> environment variable (e.g., terminal emulators like GNU Screen
+and <span class="RktSym">tmux</span><span class="RktMeta"></span>), the current behavior is to not try the control sequence (which
+causes a 1-second delay waiting for a terminal response that never arrives),
+and to just use <span class="RktSym">stty</span><span class="RktMeta"></span>.  For all other terminals, the control sequence is tried first, before trying <span class="RktSym">stty</span><span class="RktMeta"></span>.  If neither the control sequence nor <span class="RktSym">stty</span><span class="RktMeta"></span> work, then neither method is tried again for <span class="RktVar">ct</span>, and instead the procedure always returns (<span class="RktVal">#f</span>, <span class="RktVal">#f</span>).  This behavior very well might change in future versions of <span class="RktSym">charterm</span><span class="RktMeta"></span>, and the author welcomes feedback on which methods work with
+which terminals.</div></p><h3>5<tt>&nbsp;</tt><a name="(part._.Display_.Control)"></a>Display Control</h3><h4>5.1<tt>&nbsp;</tt><a name="(part._.Cursor)"></a>Cursor</h4><p><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-cursor))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-cursor</span></span><span class="hspace">&nbsp;</span><span class="RktVar">x</span><span class="hspace">&nbsp;</span><span class="RktVar">y</span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">x</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">exact-positive-integer?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">y</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">exact-positive-integer?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Positions the cursor at column <span class="RktVar">x</span>, row <span class="RktVar">y</span>, with the upper-left character cell being (1, 1).</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-newline))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-newline</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Sends a newline to the terminal.  This is typically a CR-LF
+sequence.</div></p><h4>5.2<tt>&nbsp;</tt><a name="(part._.Displaying)"></a>Displaying</h4><p><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><table cellspacing="0" class="prototype RForeground"><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-display))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-display</span></span></td><td><span class="hspace">&nbsp;</span>[</td><td><span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="RktPn">#:width</span><span class="hspace">&nbsp;</span><span class="RktVar">width</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="RktPn">#:pad</span><span class="hspace">&nbsp;</span><span class="RktVar">pad</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="RktPn">#:truncate</span><span class="hspace">&nbsp;</span><span class="RktVar">truncate</span>]</td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="RktVar">arg</span><span class="hspace">&nbsp;</span><span class="RktMeta">...</span><span class="RktPn">)</span></td><td><span class="hspace">&nbsp;</span></td><td>&rarr;</td><td><span class="hspace">&nbsp;</span></td><td><span class="RktSym">void?</span></td></tr></table></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">width</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">exact-positive-integer?</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">#f</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">pad</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">'</span><span class="RktVal">width</span><span class="hspace">&nbsp;</span><span class="RktSym">boolean?</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">'</span><span class="RktVal">width</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">truncate</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">'</span><span class="RktVal">width</span><span class="hspace">&nbsp;</span><span class="RktSym">boolean?</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">'</span><span class="RktVal">width</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">arg</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">any/c</span></td></tr></table></blockquote></div><div class="SIntrapara">Displays each <span class="RktVar">arg</span> on the terminal, as if formatted by <span class="RktSym">display</span>, with the exception that unprintable or non-ASCII characters
+might not be displayed.  (The exact behavior of what is permitted is expected
+to change in a later version of CharTerm, so avoid trying to send your own control sequences or using
+newlines, making assumptions about non-ASCII characters, etc.)</div><div class="SIntrapara">If <span class="RktVar">width</span> is a number, then <span class="RktVar">pad</span> and <span class="RktVar">truncate</span> specify whether or not to pad with spaces or truncate the output, respectively, to <span class="RktVar">width</span> characters.  When <span class="RktVar">pad</span> or <span class="RktVar">width</span> is <span class="RktVal">'</span><span class="RktVal">width</span>, that is a convenience meaning &ldquo;true if, and only if, <span class="RktVar">width</span> is not <span class="RktVal">#f</span>.&rdquo;</div></p><h4>5.3<tt>&nbsp;</tt><a name="(part._.Video_.Attributes)"></a>Video Attributes</h4><p><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-normal))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-normal</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-inverse))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-inverse</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-underline))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-underline</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-blink))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-blink</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-bold))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-bold</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Sets the <a name="(tech._video._attribute)"></a><span style="font-style: italic">video attributes</span> for subsequent writes to the terminal.  In this version of <span class="RktSym">charterm</span><span class="RktMeta"></span>, each is mutually-exclusive, so, for example, setting <span style="font-style: italic">bold</span> clears <span style="font-style: italic">inverse</span>. Note that that video attributes are currently supported only for protocol <span class="RktVal">'</span><span class="RktVal">ansi</span>, due to limitations of the TeleVideo and Wyse models for
+video attributes.</div></p><h4>5.4<tt>&nbsp;</tt><a name="(part._.Clearing)"></a>Clearing</h4><p><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-clear-screen))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-clear-screen</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Clears the screen, including first setting the video attributes to
+normal, and positioning the cursor at (1, 1).</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-clear-line))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-clear-line</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-clear-line-left))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-clear-line-left</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-clear-line-right))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-clear-line-right</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Clears text from the line with the cursor, or part of the line with the cursor.</div></p><h4>5.5<tt>&nbsp;</tt><a name="(part._.Line_.Insert_and_.Delete)"></a>Line Insert and Delete</h4><p><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-insert-line))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-insert-line</span></span><span class="hspace">&nbsp;</span>[<span class="RktVar">count</span><span class="hspace">&nbsp;</span><span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">count</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">exact-positive-integer?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">1</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Inserts <span class="RktVar">count</span> blank lines at cursor.  Note that not all terminals support
+this.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-delete-line))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-delete-line</span></span><span class="hspace">&nbsp;</span>[<span class="RktVar">count</span><span class="hspace">&nbsp;</span><span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">count</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">exact-positive-integer?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">1</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Deletes <span class="RktVar">count</span> blank lines at cursor.  Note that not all terminals support
+this.</div></p><h5>5.5.1<tt>&nbsp;</tt><a name="(part._.Misc__.Output)"></a>Misc. Output</h5><p><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-bell))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-bell</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">void?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Rings the terminal bell.  This bell ringing might manifest as a
+beep, a flash of the screen, or nothing.</div></p><h3>6<tt>&nbsp;</tt><a name="(part._.Keyboard_.Input)"></a>Keyboard Input</h3><p><div class="SIntrapara">Normally you will get keyboard input using the <span class="RktSym">charterm-read-key</span> procedure.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><p class="RForeground"><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-byte-ready~3f))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-byte-ready?</span></span><span class="hspace">&nbsp;</span>[<span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span>]<span class="RktPn">)</span><span class="hspace">&nbsp;</span>&rarr;<span class="hspace">&nbsp;</span><span class="RktSym">boolean?</span></p></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr></table></blockquote></div><div class="SIntrapara">Returns true/false for whether at least one byte is ready for
+reading (either in a buffer or on the port) from <span class="RktVar">ct</span>.  Note that, since some keys are encoded as multiple bytes, just
+because this procedure returns true doesn&rsquo;t mean that <span class="RktSym">charterm-read-key</span> won&rsquo;t block temporarily because it sees part of a potential
+multiple-byte key encoding.</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><table cellspacing="0" class="prototype RForeground"><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-read-key))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-read-key</span></span></td><td><span class="hspace">&nbsp;</span>[</td><td><span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="RktPn">#:timeout</span><span class="hspace">&nbsp;</span><span class="RktVar">timeout</span>]<span class="RktPn">)</span></td><td><span class="hspace">&nbsp;</span></td><td>&rarr;</td><td><span class="hspace">&nbsp;</span></td><td><span class="RktPn">(</span><span class="RktSym">or</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">char?</span><span class="hspace">&nbsp;</span><span class="RktSym">symbol?</span><span class="RktPn">)</span></td></tr></table></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">timeout</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">positive?</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">#f</span></td></tr></table></blockquote></div><div class="SIntrapara">Reads a key from <span class="RktVar">ct</span>, blocking indefinitely or until sometime after <span class="RktVar">timeout</span> seconds has been reached, if <span class="RktVar">timeout</span> is non-<span class="RktVal">#f</span>.  If timeout is reached, <span class="RktVal">#f</span> is returned.</div><div class="SIntrapara">Many keys are returned as characters, especially ones that
+correspond to printable characters.  For example, the unshifted <span style="font-weight: bold">Q</span> key is returned as character <span class="RktVal">#\q</span>.  Some other keys are returned as symbols, such as <span class="RktVal">'</span><span class="RktVal">return</span>, <span class="RktVal">'</span><span class="RktVal">escape</span>, <span class="RktVal">'</span><span class="RktVal">f1</span>, <span class="RktVal">'</span><span class="RktVal">shift-f12</span>, <span class="RktVal">'</span><span class="RktVal">right</span>, and many others.</div><div class="SIntrapara">Since some keys are sent as ambiguous sequences, <span class="RktSym">charterm-read-key</span> employs separate timeouts internally, such as to disambuate
+the <span style="font-weight: bold">Esc</span> key (byte sequence 27) from what on some terminals would be
+the <span style="font-weight: bold">F10</span> key (bytes sequence 27, 91, 50, 49, 126).</div><div class="SIntrapara"><blockquote class="SVInsetFlow"><table cellspacing="0" class="boxed RBoxed"><tr><td><blockquote class="SubFlow"><div class="RBackgroundLabel SIEHidden"><div class="RBackgroundLabelInner"><p>procedure</p></div></div><table cellspacing="0" class="prototype RForeground"><tr><td><span class="RktPn">(</span><a name="(def._((planet._main..rkt._(neil._charterm..plt._3._1))._charterm-read-keyinfo))"></a><span title="Provided from: (planet neil/charterm:3:1)"><span class="RktSym">charterm-read-keyinfo</span></span></td><td><span class="hspace">&nbsp;</span>[</td><td><span class="RktPn">#:charterm</span><span class="hspace">&nbsp;</span><span class="RktVar">ct</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td></tr><tr><td><span class="hspace">&nbsp;</span></td><td><span class="hspace">&nbsp;</span></td><td><span class="RktPn">#:timeout</span><span class="hspace">&nbsp;</span><span class="RktVar">timeout</span>]<span class="RktPn">)</span></td><td><span class="hspace">&nbsp;</span></td><td>&rarr;</td><td><span class="hspace">&nbsp;</span></td><td><span class="RktSym">charterm-keyinfo?</span></td></tr></table></blockquote></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">ct</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktSym">charterm?</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">current-charterm</span><span class="RktPn">)</span></td></tr><tr><td><span class="hspace">&nbsp;&nbsp;</span><span class="RktVar">timeout</span><span class="hspace">&nbsp;</span>:<span class="hspace">&nbsp;</span><span class="RktPn">(</span><span class="RktSym">or/c</span><span class="hspace">&nbsp;</span><span class="RktVal">#f</span><span class="hspace">&nbsp;</span><span class="RktSym">positive?</span><span class="RktPn">)</span><span class="hspace">&nbsp;</span>=<span class="hspace">&nbsp;</span><span class="RktVal">#f</span></td></tr></table></blockquote></div><div class="SIntrapara">Like <span class="RktSym">charterm-read-keyinfo</span> except instead of returning a <a href="/#%28tech._keycode%29" class="techoutside" data-pltdoc="x"><span class="techinside">keycode</span></a>, it returns a <a href="/#%28tech._keyinfo%29" class="techoutside" data-pltdoc="x"><span class="techinside">keyinfo</span></a>.</div></p><h3>7<tt>&nbsp;</tt><a name="(part._.References)"></a>References</h3><p><div class="SIntrapara">[<a name="(tech._ansi._x3..64)"></a><span style="font-style: italic">ANSI X3.64</span>] <a href="http://en.wikipedia.org/wiki/ANSI_escape_code"><span class="url">http://en.wikipedia.org/wiki/ANSI_escape_code</span></a></div><div class="SIntrapara">[<a name="(tech._ascii)"></a><span style="font-style: italic">ASCII</span>] <a href="http://en.wikipedia.org/wiki/Ascii"><span class="url">http://en.wikipedia.org/wiki/Ascii</span></a></div><div class="SIntrapara">[<a name="(tech._ecma._43)"></a><span style="font-style: italic">ECMA-43</span>] <a href="http://www.ecma-international.org/publications/standards/Ecma-043.htm"><span style="font-style: italic">Standard ECMA-43: 8-bit Coded Character Set Structure and Rules</span></a>, 3rd Ed., 1991-12</div><div class="SIntrapara">[<a name="(tech._ecma._48)"></a><span style="font-style: italic">ECMA-48</span>] <a href="http://www.ecma-international.org/publications/standards/Ecma-048.htm"><span style="font-style: italic">Standard ECMA-48: Control Functions for Coded Character Sets</span></a>, 5th Ed., 1991-06</div><div class="SIntrapara">[<a name="(tech._gregory)"></a><span style="font-style: italic">Gregory</span>] Phil Gregory, &ldquo;<a href="http://aperiodic.net/phil/archives/Geekery/term-function-keys.html">Terminal Function Key Escape Codes</a>,&rdquo; 2005-12-13 Web post, as viewed on 2012-06</div><div class="SIntrapara">[<a name="(tech._powerterm)"></a><span style="font-style: italic">PowerTerm</span>] Ericom PowerTerm InterConnect 8.2.0.1000 terminal emulator, as run on Wyse S50 WinTerm</div><div class="SIntrapara">[<a name="(tech._tvi._925._iug)"></a><span style="font-style: italic">TVI-925-IUG</span>] <a href="http://vt100.net/televideo/tvi925_ig.pdf"><span style="font-style: italic">TeleVideo Model 925 CRT Terminal Installation and User&rsquo;s Guide</span></a></div><div class="SIntrapara">[<a name="(tech._tvi._950._om)"></a><span style="font-style: italic">TVI-950-OM</span>] <a href="http://www.mirrorservice.org/sites/www.bitsavers.org/pdf/televideo/Operators_Manual_Model_950_1981.pdf"><span style="font-style: italic">TeleVideo Operator&rsquo;s Manual Model 950</span></a>, 1981</div><div class="SIntrapara">[<a name="(tech._vt100._tm)"></a><span style="font-style: italic">VT100-TM</span>] Digital Equipment Corp., <a href="http://vt100.net/docs/vt100-tm/"><span style="font-style: italic">VT100 Series Technical Manual</span></a>, 2nd Ed., 1980-09</div><div class="SIntrapara">[<a name="(tech._vt100._ug)"></a><span style="font-style: italic">VT100-UG</span>] Digital Equipment Corp., <a href="http://vt100.net/docs/vt100-ug/"><span style="font-style: italic">VT100 User Guide</span></a>, 3rd Ed., 1981-06</div><div class="SIntrapara">[<a name="(tech._vt100._wp)"></a><span style="font-style: italic">VT100-WP</span>] Wikipedia, <a href="http://en.wikipedia.org/wiki/VT100">VT100</a></div><div class="SIntrapara">[<a name="(tech._wy._50._qrg)"></a><span style="font-style: italic">WY-50-QRG</span>] <a href="http://vt100.net/wyse/wy-50-qrg/wy-50-qrg.pdf"><span style="font-style: italic">Wyse WY-50 Display Terminal Quick-Reference Guide</span></a></div><div class="SIntrapara">[<a name="(tech._wy._60._ug)"></a><span style="font-style: italic">WY-60-UG</span>] <a href="http://vt100.net/wyse/wy-60-ug/wy-60-ug.pdf"><span style="font-style: italic">Wyse WY-60 User&rsquo;s Guide</span></a></div><div class="SIntrapara">[<a name="(tech._wy60)"></a><span style="font-style: italic">wy60</span>] <a href="http://code.google.com/p/wy60/"><span class="RktSym">wy60</span><span class="RktMeta"></span> terminal emulator</a></div><div class="SIntrapara">[<a name="(tech._xterm._ctlseq)"></a><span style="font-style: italic">XTerm-ctlseqs</span>] Edward Moy, Stephen Gildea, Thomas Dickey, &ldquo;<a href="http://invisible-island.net/xterm/ctlseqs/ctlseqs.html">Xterm Control Sequences</a>,&rdquo; 2012</div><div class="SIntrapara">[<a name="(tech._xterm._dickey)"></a><span style="font-style: italic">XTerm-Dickey</span>] <a href="http://invisible-island.net/xterm/"><span class="url">http://invisible-island.net/xterm/</span></a></div><div class="SIntrapara">[<a name="(tech._xterm._faq)"></a><span style="font-style: italic">XTerm-FAQ</span>] Thomas E. Dickey, &ldquo;<a href="http://invisible-island.net/xterm/xterm.faq.html">XTerm FAQ</a>,&rdquo; dated 2012</div><div class="SIntrapara">[<a name="(tech._xterm._wp)"></a><span style="font-style: italic">XTerm-WP</span>] Wikipedia, <a href="http://en.wikipedia.org/wiki/Xterm">xterm</a></div></p><h3>8<tt>&nbsp;</tt><a name="(part._.Known_.Issues)"></a>Known Issues</h3><ul><li><p>Need to support ANSI alternate CSI for 8-bit terminals, even
+before supporting 8-bit characters and multibyte.</p></li><li><p>Only supports ASCII characters.  Adding UTF-8 support, for terminal emulators
+that support it, would be nice.</p></li><li><p>Expose the character-decoding mini-language as a configurable
+option.  Perhaps wait until we implement timeout-based disambiguation at
+arbitrary points in the the DFA rather than just at the top.  Also, might be
+better to resolve multi-byte characters first, in case that affects the
+mini-language.</p></li><li><p>More controls for terminal features can be added.</p></li><li><p>Currently only implemented to work on Unix-like systems like
+GNU/Linux.</p></li><li><p>Implement text input controls, either as part of this library or
+another, using <span class="RktSym">charterm-demo</span> as a starting point.</p></li></ul><h3>9<tt>&nbsp;</tt><a name="(part._.History)"></a>History</h3><ul><li><p><div class="SIntrapara">PLaneT 3:1 &#8212;<wbr></wbr> 2013-05-13</div><div class="SIntrapara"><ul><li><p>Now uses lowercase <span class="RktSym"><span class="nobreak">-f</span></span><span class="RktMeta"></span> argument on MacOS X.  (Thanks to Jens Axel S&#248;gaard for reporting.)</p></li><li><p>Documentation tweaks.</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 3:0 &#8212;<wbr></wbr> 2012-07-13</div><div class="SIntrapara"><ul><li><p>Changed &ldquo;<span class="RktSym">ansi-ish</span><span class="RktMeta"></span>&rdquo; in identifiers to &ldquo;<span class="RktSym">ansi</span><span class="RktMeta"></span>&rdquo;, hence the PLaneT major version number change.</p></li><li><p>Documentation tweaks.</p></li><li><p>Renamed package from &ldquo;<span class="RktSym">charterm</span><span class="RktMeta"></span>&rdquo; to &ldquo;CharTerm&rdquo;.</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 2:5 &#8212;<wbr></wbr> 2012-06-28</div><div class="SIntrapara"><ul><li><p>A <span class="RktSym">charterm</span> object is now a synchronizable event.</p></li><li><p>Documentation tweaks.</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 2:4 &#8212;<wbr></wbr> 2012-06-25</div><div class="SIntrapara"><ul><li><p>Documentation fix for return type of <span class="RktSym">charterm-read-keyinfo</span>.</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 2:3 &#8212;<wbr></wbr> 2012-06-25</div><div class="SIntrapara"><ul><li><p>Fixed problem determining screen size on some
+XTerms.  (Thanks to Eli Barzilay for reporting.)</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 2:2 &#8212;<wbr></wbr> 2012-06-25</div><div class="SIntrapara"><ul><li><p>Added another variation of encoding for XTerm arrow,
+Home, and End keys.  (Thanks to Eli Barzilay.)</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 2:1 &#8212;<wbr></wbr> 2012-06-24</div><div class="SIntrapara"><ul><li><p>Corrected PLaneT version number in <span class="RktSym">require</span> in an example.</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 2:0 &#8212;<wbr></wbr> 2012-06-24</div><div class="SIntrapara"><ul><li><p>Greatly increased the sophistication of handling of terminal diversity.</p></li><li><p>Added the <span class="RktSym">wyse-wy50</span><span class="RktMeta"></span> and <span class="RktSym">televideo-950</span><span class="RktMeta"></span> [Correction: <span class="RktSym">televideo-925</span><span class="RktMeta"></span>] protocols, for supporting the native modes of Wyse and
+TeleVideo terminals, respectively, and compatibles.</p></li><li><p>More support for different key encodings and termvars.</p></li><li><p>Demo is now in a separate file, mainly for convenience
+in giving command lines that run it.  This breaks a command line example
+previously documented, so changed PLaneT major version, although the
+previously-published example will need to have <span class="RktSym">:1</span><span class="RktMeta"></span> added to it anyway.</p></li><li><p><span class="RktSym">charterm-screen-size</span> now defaults to (80,24) when all else fails.</p></li><li><p>Documentation changes.</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 1:1 &#8212;<wbr></wbr> 2012-06-17</div><div class="SIntrapara"><ul><li><p>For <span class="RktSym">screen</span><span class="RktMeta"></span> and <span class="RktSym">tmux</span><span class="RktMeta"></span>, now gets screen size via <span class="RktSym">stty</span><span class="RktMeta"></span>.  This resolves the sluggishness reported with <span class="RktSym">screen</span><span class="RktMeta"></span>.  [Correction: In version 1:1, this behavior is
+adaptive for all terminals, with the shortcut for <a href="/#%28tech._termvar%29" class="techoutside" data-pltdoc="x"><span class="techinside">termvar</span></a> <span class="RktSym">screen</span><span class="RktMeta"></span> that it doesn&rsquo;t bother trying the control sequence.]</p></li><li><p>Documentation tweaks.</p></li></ul></div></p></li><li><p><div class="SIntrapara">PLaneT 1:0 &#8212;<wbr></wbr> 2012-06-16</div><div class="SIntrapara"><ul><li><p>Initial version.</p></li></ul></div></p></li></ul><h3>10<tt>&nbsp;</tt><a name="(part._.Legal)"></a>Legal</h3><p>Copyright 2012 &ndash; 2013 Neil Van Dyke.  This program is Free Software; you
+can redistribute it and/or modify it under the terms of the GNU Lesser General
+Public License as published by the Free Software Foundation; either version 3
+of the License, or (at your option) any later version.  This program is
+distributed in the hope that it will be useful, but without any warranty;
+without even the implied warranty of merchantability or fitness for a
+particular purpose.  See http://www.gnu.org/licenses/ for details.  For other
+licenses and consulting, please contact the author.</p></div></div><div id="contextindicator">&nbsp;</div></body></html>
\ No newline at end of file
diff --git a/archive/1.vm.arc/charterm/planet-docs/doc/racket.css b/archive/1.vm.arc/charterm/planet-docs/doc/racket.css
new file mode 100644
index 00000000..021e4da5
--- /dev/null
+++ b/archive/1.vm.arc/charterm/planet-docs/doc/racket.css
@@ -0,0 +1,234 @@
+
+/* See the beginning of "scribble.css". */
+
+/* Monospace: */
+.RktIn, .RktRdr, .RktPn, .RktMeta,
+.RktMod, .RktKw, .RktVar, .RktSym,
+.RktRes, .RktOut, .RktCmt, .RktVal,
+.RktBlk {
+  font-family: monospace;
+  white-space: inherit;
+}
+
+/* Serif: */
+.inheritedlbl {
+  font-family: serif;
+}
+
+/* Sans-serif: */
+.RBackgroundLabelInner {
+  font-family: sans-serif;
+}
+
+/* ---------------------------------------- */
+/* Inherited methods, left margin */
+
+.inherited {
+  width: 100%;
+  margin-top: 0.5em;
+  text-align: left;
+  background-color: #ECF5F5;
+}
+
+.inherited td {
+  font-size: 82%;
+  padding-left: 1em;
+  text-indent: -0.8em;
+  padding-right: 0.2em;
+}
+
+.inheritedlbl {
+  font-style: italic;
+}
+
+/* ---------------------------------------- */
+/* Racket text styles */
+
+.RktIn {
+  color: #cc6633;
+  background-color: #eeeeee;
+}
+
+.RktInBG {
+  background-color: #eeeeee;
+}
+
+.RktRdr {
+}
+
+.RktPn {
+  color: #843c24;
+}
+
+.RktMeta {
+  color: black;
+}
+
+.RktMod {
+  color: black;
+}
+
+.RktOpt {
+  color: black;
+}
+
+.RktKw {
+  color: black;
+  /* font-weight: bold; */
+}
+
+.RktErr {
+  color: red;
+  font-style: italic;
+}
+
+.RktVar {
+  color: #262680;
+  font-style: italic;
+}
+
+.RktSym {
+  color: #262680;
+}
+
+.RktValLink {
+  text-decoration: none;
+  color: blue;
+}
+
+.RktModLink {
+  text-decoration: none;
+  color: blue;
+}
+
+.RktStxLink {
+  text-decoration: none;
+  color: black;
+  /* font-weight: bold; */
+}
+
+.RktRes {
+  color: #0000af;
+}
+
+.RktOut {
+  color: #960096;
+}
+
+.RktCmt {
+  color: #c2741f;
+}
+
+.RktVal {
+  color: #228b22;
+}
+
+/* ---------------------------------------- */
+/* Some inline styles */
+
+.together {
+  width: 100%;
+}
+
+.prototype, .argcontract, .RBoxed {
+  white-space: nowrap;
+}
+
+.prototype td {
+  vertical-align: text-top;
+}
+.longprototype td {
+  vertical-align: bottom;
+}
+
+.RktBlk {
+  white-space: inherit;
+  text-align: left;
+}
+
+.RktBlk tr {
+  white-space: inherit;
+}
+
+.RktBlk td {
+  vertical-align: baseline;
+  white-space: inherit;
+}
+
+.argcontract td {
+  vertical-align: text-top;
+}
+
+.highlighted {
+  background-color: #ddddff;
+}
+
+.defmodule {
+  width: 100%;
+  background-color: #F5F5DC;
+}
+
+.specgrammar {
+  float: right;
+}
+
+.RBibliography td {
+  vertical-align: text-top;
+}
+
+.leftindent {
+ margin-left: 1em;
+ margin-right: 0em;
+}
+
+.insetpara {
+ margin-left: 1em;
+ margin-right: 1em;
+}
+
+.Rfilebox {
+}
+
+.Rfiletitle {
+  text-align: right;
+  margin: 0em 0em 0em 0em;
+}
+
+.Rfilename {
+  border-top: 1px solid #6C8585;
+  border-right: 1px solid #6C8585;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+  background-color: #ECF5F5;
+}
+
+.Rfilecontent {
+  margin: 0em 0em 0em 0em;
+}
+
+/* ---------------------------------------- */
+/* For background labels */
+
+.RBackgroundLabel {
+   float: right;
+   width: 0px;
+   height: 0px;
+}
+
+.RBackgroundLabelInner {
+   position: relative;
+   width: 25em;
+   left: -25.5em;
+   top: 0px;
+   text-align: right;
+   color: white;
+   z-index: 0;
+   font-weight: bold;
+}
+
+.RForeground {
+   position: relative;
+   left: 0px;
+   top: 0px;
+   z-index: 1;
+}
diff --git a/archive/1.vm.arc/charterm/planet-docs/doc/scribble-common.js b/archive/1.vm.arc/charterm/planet-docs/doc/scribble-common.js
new file mode 100644
index 00000000..00eec767
--- /dev/null
+++ b/archive/1.vm.arc/charterm/planet-docs/doc/scribble-common.js
@@ -0,0 +1,153 @@
+// Common functionality for PLT documentation pages
+
+// Page Parameters ------------------------------------------------------------
+
+var page_query_string =
+  (location.href.search(/\?([^#]+)(?:#|$)/) >= 0) && RegExp.$1;
+
+var page_args =
+  ((function(){
+      if (!page_query_string) return [];
+      var args = page_query_string.split(/[&;]/);
+      for (var i=0; i<args.length; i++) {
+        var a = args[i];
+        var p = a.indexOf('=');
+        if (p >= 0) args[i] = [a.substring(0,p), a.substring(p+1)];
+        else args[i] = [a, false];
+      }
+      return args;
+    })());
+
+function GetPageArg(key, def) {
+  for (var i=0; i<page_args.length; i++)
+    if (page_args[i][0] == key) return unescape(page_args[i][1]);
+  return def;
+}
+
+function MergePageArgsIntoLink(a) {
+  if (page_args.length == 0 ||
+      (!a.attributes["data-pltdoc"]) || (a.attributes["data-pltdoc"].value == ""))
+    return;
+  a.href.search(/^([^?#]*)(?:\?([^#]*))?(#.*)?$/);
+  if (RegExp.$2.length == 0) {
+    a.href = RegExp.$1 + "?" + page_query_string + RegExp.$3;
+  } else {
+    // need to merge here, precedence to arguments that exist in `a'
+    var i, j;
+    var prefix = RegExp.$1, str = RegExp.$2, suffix = RegExp.$3;
+    var args = str.split(/[&;]/);
+    for (i=0; i<args.length; i++) {
+      j = args[i].indexOf('=');
+      if (j) args[i] = args[i].substring(0,j);
+    }
+    var additions = "";
+    for (i=0; i<page_args.length; i++) {
+      var exists = false;
+      for (j=0; j<args.length; j++)
+        if (args[j] == page_args[i][0]) { exists = true; break; }
+      if (!exists) str += "&" + page_args[i][0] + "=" + page_args[i][1];
+    }
+    a.href = prefix + "?" + str + suffix;
+  }
+}
+
+// Cookies --------------------------------------------------------------------
+
+function GetCookie(key, def) {
+  var i, cookiestrs;
+  try {
+    if (document.cookie.length <= 0) return def;
+    cookiestrs = document.cookie.split(/; */);
+  } catch (e) { return def; }
+  for (i = 0; i < cookiestrs.length; i++) {
+    var cur = cookiestrs[i];
+    var eql = cur.indexOf('=');
+    if (eql >= 0 && cur.substring(0,eql) == key)
+      return unescape(cur.substring(eql+1));
+  }
+  return def;
+}
+
+function SetCookie(key, val) {
+  var d = new Date();
+  d.setTime(d.getTime()+(365*24*60*60*1000));
+  try {
+    document.cookie =
+      key + "=" + escape(val) + "; expires="+ d.toGMTString() + "; path=/";
+  } catch (e) {}
+}
+
+// note that this always stores a directory name, ending with a "/"
+function SetPLTRoot(ver, relative) {
+  var root = location.protocol + "//" + location.host
+           + NormalizePath(location.pathname.replace(/[^\/]*$/, relative));
+  SetCookie("PLT_Root."+ver, root);
+}
+
+// adding index.html works because of the above
+function GotoPLTRoot(ver, relative) {
+  var u = GetCookie("PLT_Root."+ver, null);
+  if (u == null) return true; // no cookie: use plain up link
+  // the relative path is optional, default goes to the toplevel start page
+  if (!relative) relative = "index.html";
+  location = u + relative;
+  return false;
+}
+
+// Utilities ------------------------------------------------------------------
+
+var normalize_rxs = [/\/\/+/g, /\/\.(\/|$)/, /\/[^\/]*\/\.\.(\/|$)/];
+function NormalizePath(path) {
+  var tmp, i;
+  for (i = 0; i < normalize_rxs.length; i++)
+    while ((tmp = path.replace(normalize_rxs[i], "/")) != path) path = tmp;
+  return path;
+}
+
+// `noscript' is problematic in some browsers (always renders as a
+// block), use this hack instead (does not always work!)
+// document.write("<style>mynoscript { display:none; }</style>");
+
+// Interactions ---------------------------------------------------------------
+
+function DoSearchKey(event, field, ver, top_path) {
+  var val = field.value;
+  if (event && event.keyCode == 13) {
+    var u = GetCookie("PLT_Root."+ver, null);
+    if (u == null) u = top_path; // default: go to the top path
+    u += "search/index.html?q=" + escape(val);
+    if (page_query_string) u += "&" + page_query_string;
+    location = u;
+    return false;
+  }
+  return true;
+}
+
+function TocviewToggle(glyph, id) {
+  var s = document.getElementById(id).style;
+  var expand = s.display == "none";
+  s.display = expand ? "block" : "none";
+  glyph.innerHTML = expand ? "&#9660;" : "&#9658;";
+}
+
+// Page Init ------------------------------------------------------------------
+
+// Note: could make a function that inspects and uses window.onload to chain to
+// a previous one, but this file needs to be required first anyway, since it
+// contains utilities for all other files.
+var on_load_funcs = [];
+function AddOnLoad(fun) { on_load_funcs.push(fun); }
+window.onload = function() {
+  for (var i=0; i<on_load_funcs.length; i++) on_load_funcs[i]();
+};
+
+AddOnLoad(function(){
+    var links = document.getElementsByTagName("a");
+    for (var i=0; i<links.length; i++) MergePageArgsIntoLink(links[i]);
+    var label = GetPageArg("ctxtname",false);
+    if (!label) return;
+    var indicator = document.getElementById("contextindicator");
+    if (!indicator) return;
+    indicator.innerHTML = label;
+    indicator.style.display = "block";
+  });
diff --git a/archive/1.vm.arc/charterm/planet-docs/doc/scribble-style.css b/archive/1.vm.arc/charterm/planet-docs/doc/scribble-style.css
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/archive/1.vm.arc/charterm/planet-docs/doc/scribble-style.css
diff --git a/archive/1.vm.arc/charterm/planet-docs/doc/scribble.css b/archive/1.vm.arc/charterm/planet-docs/doc/scribble.css
new file mode 100644
index 00000000..d521d28f
--- /dev/null
+++ b/archive/1.vm.arc/charterm/planet-docs/doc/scribble.css
@@ -0,0 +1,487 @@
+
+/* CSS seems backward: List all the classes for which we want a
+   particular font, so that the font can be changed in one place.  (It
+   would be nicer to reference a font definition from all the places
+   that we want it.)
+
+   As you read the rest of the file, remember to double-check here to
+   see if any font is set. */
+
+/* Monospace: */
+.maincolumn, .refpara, .refelem, .tocset, .stt, .hspace, .refparaleft, .refelemleft {
+  font-family: monospace;
+}
+
+/* Serif: */
+.main, .refcontent, .tocview, .tocsub, i {
+  font-family: serif;
+}
+
+/* Sans-serif: */
+.version, .versionNoNav {
+  font-family: sans-serif;
+}
+
+/* ---------------------------------------- */
+
+p, .SIntrapara {
+  display: block;
+  margin: 1em 0;
+}
+
+h2 { /* per-page main title */
+  margin-top: 0;
+}
+
+h3, h4, h5, h6, h7, h8 {
+  margin-top: 1.75em;
+  margin-bottom: 0.5em;
+}
+
+.SSubSubSubSection {
+  font-weight: bold;
+  font-size: 0.83em; /* should match h5; from HTML 4 reference */
+}
+
+/* Needed for browsers like Opera, and eventually for HTML 4 conformance.
+   This means that multiple paragraphs in a table element do not have a space
+   between them. */
+table p {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+/* ---------------------------------------- */
+/* Main */
+
+body {
+  color: black;
+  background-color: #ffffff;
+}
+
+table td {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.maincolumn {
+  width: 43em;
+  margin-right: -40em;
+  margin-left: 15em;
+}
+
+.main {
+  text-align: left;
+}
+
+/* ---------------------------------------- */
+/* Navigation */
+
+.navsettop, .navsetbottom {
+  background-color: #f0f0e0;
+  padding: 0.25em 0 0.25em 0;
+}
+
+.navsettop {
+  margin-bottom: 1.5em;
+  border-bottom: 2px solid #e0e0c0;
+}
+
+.navsetbottom {
+  margin-top: 2em;
+  border-top: 2px solid #e0e0c0;
+}
+
+.navleft {
+  margin-left: 1ex;
+  position: relative;
+  float: left;
+  white-space: nowrap;
+}
+.navright {
+  margin-right: 1ex;
+  position: relative;
+  float: right;
+  white-space: nowrap;
+}
+.nonavigation {
+  color: #e0e0e0;
+}
+
+.searchform {
+  display: inline;
+  margin: 0;
+  padding: 0;
+}
+
+.searchbox {
+  width: 16em;
+  margin: 0px;
+  padding: 0px;
+  background-color: #eee;
+  border: 1px solid #ddd;
+  text-align: center;
+  vertical-align: middle;
+}
+
+#contextindicator {
+  position: fixed;
+  background-color: #c6f;
+  color: #000;
+  font-family: monospace;
+  font-weight: bold;
+  padding: 2px 10px;
+  display: none;
+  right: 0;
+  bottom: 0;
+}
+
+/* ---------------------------------------- */
+/* Version */
+
+.versionbox {
+  position: relative;
+  float: right;
+  left: 2em;
+  height: 0em;
+  width: 13em;
+  margin: 0em -13em 0em 0em;
+}
+.version {
+  font-size: small;
+}
+.versionNoNav {
+  font-size: xx-small; /* avoid overlap with author */
+}
+
+.version:before, .versionNoNav:before {
+  content: "Version ";
+}
+
+/* ---------------------------------------- */
+/* Margin notes */
+
+.refpara, .refelem {
+  position: relative;
+  float: right;
+  left: 2em;
+  height: 0em;
+  width: 13em;
+  margin: 0em -13em 0em 0em;
+}
+
+.refpara, .refparaleft {
+  top: -1em;
+}
+
+.refcolumn {
+  background-color: #F5F5DC;
+  display: block;
+  position: relative;
+  width: 13em;
+  font-size: 85%;
+  border: 0.5em solid #F5F5DC;
+  margin: 0 0 0 0;
+}
+
+.refcontent {
+  margin: 0 0 0 0;
+}
+
+.refcontent p {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.refparaleft {
+  position: relative;
+  float: left;
+  right: 2em;
+  height: 0em;
+  width: 13em;
+  margin: 0em 0em 0em -13em;
+}
+
+.refcolumnleft, .refelemleft {
+  background-color: #F5F5DC;
+  display: block;
+  position: relative;
+  width: 13em;
+  font-size: 85%;
+  border: 0.5em solid #F5F5DC;
+  margin: 0 0 0 0;
+}
+
+
+/* ---------------------------------------- */
+/* Table of contents, inline */
+
+.toclink {
+  text-decoration: none;
+  color: blue;
+  font-size: 85%;
+}
+
+.toptoclink {
+  text-decoration: none;
+  color: blue;
+  font-weight: bold;
+}
+
+/* ---------------------------------------- */
+/* Table of contents, left margin */
+
+.tocset {
+  position: relative;
+  float: left;
+  width: 12.5em;
+  margin-right: 2em;
+}
+.tocset td {
+  vertical-align: text-top;
+}
+
+.tocview {
+  text-align: left;
+  background-color: #f0f0e0;
+}
+
+.tocsub {
+  text-align: left;
+  margin-top: 0.5em;
+  background-color: #f0f0e0;
+}
+
+.tocviewlist, .tocsublist {
+  margin-left: 0.2em;
+  margin-right: 0.2em;
+  padding-top: 0.2em;
+  padding-bottom: 0.2em;
+}
+.tocviewlist table {
+  font-size: 82%;
+}
+
+.tocviewsublist, .tocviewsublistonly, .tocviewsublisttop, .tocviewsublistbottom {
+  margin-left: 0.4em;
+  border-left: 1px solid #bbf;
+  padding-left: 0.8em;
+}
+.tocviewsublist {
+  margin-bottom: 1em;
+}
+.tocviewsublist table,
+.tocviewsublistonly table,
+.tocviewsublisttop table,
+.tocviewsublistbottom table {
+  font-size: 75%;
+}
+
+.tocviewtitle * {
+  font-weight: bold;
+}
+
+.tocviewlink {
+  text-decoration: none;
+  color: blue;
+}
+
+.tocviewselflink {
+  text-decoration: underline;
+  color: blue;
+}
+
+.tocviewtoggle {
+  text-decoration: none;
+  color: blue;
+  font-size: 75%; /* looks better, and avoids bounce when toggling sub-sections due to font alignments */
+}
+
+.tocsublist td {
+  padding-left: 1em;
+  text-indent: -1em;
+}
+
+.tocsublinknumber {
+  font-size: 82%;
+}
+
+.tocsublink {
+  font-size: 82%;
+  text-decoration: none;
+}
+
+.tocsubseclink {
+  font-size: 82%;
+  text-decoration: none;
+}
+
+.tocsubnonseclink {
+  font-size: 82%;
+  text-decoration: none;
+  padding-left: 0.5em;
+}
+
+.tocsubtitle {
+  font-size: 82%;
+  font-style: italic;
+  margin: 0.2em;
+}
+
+.sepspace {
+  font-size: 40%;
+}
+
+.septitle {
+  font-size: 70%;
+}
+
+/* ---------------------------------------- */
+/* Some inline styles */
+
+.indexlink {
+  text-decoration: none;
+}
+
+.nobreak {
+  white-space: nowrap;
+}
+
+.stt {
+}
+
+.title {
+  font-size: 200%;
+  font-weight: normal;
+  margin-top: 2.8em;
+  text-align: center;
+}
+
+pre { margin-left: 2em; }
+blockquote { margin-left: 2em; }
+
+ol          { list-style-type: decimal; }
+ol ol       { list-style-type: lower-alpha; }
+ol ol ol    { list-style-type: lower-roman; }
+ol ol ol ol { list-style-type: upper-alpha; }
+
+i {
+}
+
+.SCodeFlow {
+  display: block;
+  margin-left: 1em;
+  margin-bottom: 0em;
+  margin-right: 1em;
+  margin-top: 0em;
+  white-space: nowrap;  
+}
+
+.SVInsetFlow {
+  display: block;
+  margin-left: 0em;
+  margin-bottom: 0em;
+  margin-right: 0em;
+  margin-top: 0em;
+}
+
+.SubFlow {
+  display: block;
+  margin: 0em;
+}
+
+.boxed {
+  width: 100%;
+  background-color: #E8E8FF;
+}
+
+.hspace {
+}
+
+.slant {
+  font-style: oblique;
+}
+
+.badlink {
+  text-decoration: underline;
+  color: red;
+}
+
+.plainlink {
+  text-decoration: none;
+  color: blue;
+}
+
+.techoutside       { text-decoration: underline; color: #b0b0b0; }
+.techoutside:hover { text-decoration: underline; color: blue; }
+
+/* .techinside:hover doesn't work with FF, .techinside:hover>
+   .techinside doesn't work with IE, so use both (and IE doesn't
+   work with inherit in the second one, so use blue directly) */
+.techinside                    { color: black; }
+.techinside:hover              { color: blue; }
+.techoutside:hover>.techinside { color: inherit; }
+
+.SCentered {
+  text-align: center;
+}
+
+.imageleft {
+  float: left;
+  margin-right: 0.3em;
+}
+
+.Smaller{
+  font-size: 82%;
+}
+
+.Larger{
+  font-size: 122%;
+}
+
+/* A hack, inserted to break some Scheme ids: */
+.mywbr {
+  width: 0;
+  font-size: 1px;
+}
+
+.compact li p {
+  margin: 0em;
+  padding: 0em;
+}
+
+.noborder img {
+  border: 0;
+}
+
+.SAuthorListBox {
+  position: relative;
+  float: right;
+  left: 2em;
+  top: -2.5em;
+  height: 0em;
+  width: 13em;
+  margin: 0em -13em 0em 0em;
+}
+.SAuthorList {
+  font-size: 82%;
+}
+.SAuthorList:before {
+  content: "by ";
+}
+.author {
+  display: inline;
+  white-space: nowrap;
+}
+
+/* print styles : hide the navigation elements */
+@media print {
+  .tocset,
+  .navsettop,
+  .navsetbottom { display: none; }
+  .maincolumn {
+    width: auto;
+    margin-right: 13em;
+    margin-left: 0;
+  }
+}
diff --git a/archive/1.vm.arc/charterm/test-charterm.rkt b/archive/1.vm.arc/charterm/test-charterm.rkt
new file mode 100644
index 00000000..04eb376f
--- /dev/null
+++ b/archive/1.vm.arc/charterm/test-charterm.rkt
@@ -0,0 +1,20 @@
+#lang racket/base
+;; For legal info, see file "charterm.rkt".
+
+;; (require (planet neil/charterm:1))
+(require "charterm.rkt")
+
+(with-charterm
+ (charterm-clear-screen)
+ (charterm-cursor 10 5)
+ (charterm-display "Hello, ")
+ (charterm-bold)
+ (charterm-display "you")
+ (charterm-normal)
+ (charterm-display ".")
+ (charterm-cursor 1 1)
+ (charterm-display "Press a key...")
+ (let ((key (charterm-read-key)))
+   (charterm-cursor 1 1)
+   (charterm-clear-line)
+   (printf "You pressed: ~S\r\n" key)))
diff --git a/archive/1.vm.arc/chessboard.arc.t b/archive/1.vm.arc/chessboard.arc.t
new file mode 100644
index 00000000..eb365b69
--- /dev/null
+++ b/archive/1.vm.arc/chessboard.arc.t
@@ -0,0 +1,239 @@
+(selective-load "mu.arc" section-level)
+(set allow-raw-addresses*)
+(add-code:readfile "chessboard.mu")
+(freeze function*)
+(load-system-functions)
+
+(reset2)
+(new-trace "read-move-legal")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (stdin:channel-address <- init-channel 1:literal)
+  (r:integer/routine <- fork read-move:fn nil:literal/globals 2000:literal/limit stdin:channel-address)
+  (c:character <- copy ((#\a literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (c:character <- copy ((#\2 literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (c:character <- copy ((#\- literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (c:character <- copy ((#\a literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (c:character <- copy ((#\4 literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (c:character <- copy ((#\newline literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (sleep until-routine-done:literal r:integer/routine)
+)
+(each routine completed-routines*
+;?   (prn "  " routine)
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~ran-to-completion 'read-move)
+  (prn "F - chessboard accepts legal moves (<rank><file>-<rank><file>)"))
+; todo: we can't test that keys pressed are printed to screen
+; but that's at a lower level
+;? (quit)
+
+(reset2)
+(new-trace "read-move-incomplete")
+; initialize some variables at specific raw locations
+;? (prn "== init")
+(run-code test-init
+  (1:channel-address/raw <- init-channel 1:literal)
+  (2:terminal-address/raw <- init-fake-terminal 20:literal 10:literal)
+  (3:string-address/raw <- get 2:terminal-address/raw/deref data:offset))
+(wipe completed-routines*)
+; the component under test; we'll be running this repeatedly
+(let read-move-routine (make-routine 'read-move memory*.1 memory*.2)
+;?   (prn "== first key")
+  (run-code send-first-key
+    (default-space:space-address <- new space:literal 30:literal/capacity)
+    (c:character <- copy ((#\a literal)))
+    (x:tagged-value <- save-type c:character)
+    (1:channel-address/raw/deref <- write 1:channel-address/raw x:tagged-value))
+  (wipe completed-routines*)
+  ; check that read-move consumes it and then goes to sleep
+  (enq read-move-routine running-routines*)
+  (run-more)
+  (when (ran-to-completion 'read-move)
+    (prn "F - chessboard waits after first letter of move"))
+  (wipe completed-routines*)
+  ; send in a few more letters
+;?   (prn "== more keys")
+  (restart read-move-routine)
+  (run-code send-more-keys
+    (default-space:space-address <- new space:literal 30:literal/capacity)
+    (c:character <- copy ((#\2 literal)))
+    (x:tagged-value <- save-type c:character)
+    (1:channel-address/raw/deref <- write 1:channel-address/raw x:tagged-value)
+    (c:character <- copy ((#\- literal)))
+    (x:tagged-value <- save-type c:character)
+    (1:channel-address/raw/deref <- write 1:channel-address/raw x:tagged-value)
+    (c:character <- copy ((#\a literal)))
+    (x:tagged-value <- save-type c:character)
+    (1:channel-address/raw/deref <- write 1:channel-address/raw x:tagged-value)
+    (c:character <- copy ((#\4 literal)))
+    (x:tagged-value <- save-type c:character)
+    (1:channel-address/raw/deref <- write 1:channel-address/raw x:tagged-value))
+  ; check that read-move consumes them and then goes to sleep
+  (when (ran-to-completion 'read-move)
+    (prn "F - chessboard waits after each subsequent letter of move until the last"))
+  (wipe completed-routines*)
+  ; send final key
+;?   (prn "== final key")
+  (restart read-move-routine)
+;?   (set dump-trace*)
+  (run-code send-final-key
+    (default-space:space-address <- new space:literal 30:literal/capacity)
+    (c:character <- copy ((#\newline literal)))
+    (x:tagged-value <- save-type c:character)
+    (1:channel-address/raw/deref <- write 1:channel-address/raw x:tagged-value))
+  ; check that read-move consumes it and -- this time -- returns
+  (when (~ran-to-completion 'read-move)
+    (prn "F - 'read-move' completes after final letter of move"))
+)
+
+(reset2)
+(new-trace "read-move-quit")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (stdin:channel-address <- init-channel 1:literal)
+  (dummy:terminal-address <- init-fake-terminal 20:literal 10:literal)
+  (r:integer/routine <- fork-helper read-move:fn nil:literal/globals 2000:literal/limit stdin:channel-address dummy:terminal-address)
+  (c:character <- copy ((#\q literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (sleep until-routine-done:literal r:integer/routine)
+)
+(when (~ran-to-completion 'read-move)
+  (prn "F - chessboard quits on move starting with 'q'"))
+
+(reset2)
+(new-trace "read-illegal-file")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (stdin:channel-address <- init-channel 1:literal)
+  (dummy:terminal-address <- init-fake-terminal 20:literal 10:literal)
+  (r:integer/routine <- fork-helper read-file:fn nil:literal/globals 2000:literal/limit stdin:channel-address dummy:terminal-address)
+  (c:character <- copy ((#\i literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (sleep until-routine-done:literal r:integer/routine)
+)
+;? (each routine completed-routines*
+;?   (prn "  " routine))
+(when (or (ran-to-completion 'read-file)
+          (let routine routine-running!read-file
+            (~posmatch "file too high" rep.routine!error)))
+  (prn "F - 'read-file' checks that file lies between 'a' and 'h'"))
+
+(reset2)
+(new-trace "read-illegal-rank")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (stdin:channel-address <- init-channel 1:literal)
+  (dummy:terminal-address <- init-fake-terminal 20:literal 10:literal)
+  (r:integer/routine <- fork-helper read-rank:fn nil:literal/globals 2000:literal/limit stdin:channel-address dummy:terminal-address)
+  (c:character <- copy ((#\9 literal)))
+  (x:tagged-value <- save-type c:character)
+  (stdin:channel-address/deref <- write stdin:channel-address x:tagged-value)
+  (sleep until-routine-done:literal r:integer/routine)
+)
+(when (or (ran-to-completion 'read-rank)
+          (let routine routine-running!read-rank
+            (~posmatch "rank too high" rep.routine!error)))
+  (prn "F - 'read-rank' checks that rank lies between '1' and '8'"))
+
+(reset2)
+(new-trace "print-board")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+;?   ($print (("init-array\n" literal))) ;? 1
+  (initial-position:integer-array-address <- init-array ((#\R literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\r literal))
+                                              ((#\N literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\n literal))
+                                              ((#\B literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\b literal))
+                                              ((#\Q literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\q literal))
+                                              ((#\K literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\k literal))
+                                              ((#\B literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\b literal))
+                                              ((#\N literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\n literal))
+                                              ((#\R literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\r literal)))
+;?   ($print (("init-board\n" literal))) ;? 1
+  (b:board-address <- init-board initial-position:integer-array-address)
+  (screen:terminal-address <- init-fake-terminal 20:literal 10:literal)
+  (print-board screen:terminal-address b:board-address)
+  (1:string-address/raw <- get screen:terminal-address/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+;? (prn memory*.1)
+(when (~screen-contains memory*.1 20
+          (+ "8 | r n b q k b n r "
+             "7 | p p p p p p p p "
+             "6 | _ _ _ _ _ _ _ _ "
+             "5 | _ _ _ _ _ _ _ _ "
+             "4 | _ _ _ _ _ _ _ _ "
+             "3 | _ _ _ _ _ _ _ _ "
+             "2 | P P P P P P P P "
+             "1 | R N B Q K B N R "
+             "  +---------------- "
+             "    a b c d e f g h "))
+  (prn "F - print-board works; chessboard begins at @memory*.1"))
+
+; todo: how to fold this more elegantly with the previous test?
+(reset2)
+(new-trace "make-move")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; fake screen
+  (screen:terminal-address <- init-fake-terminal 20:literal 10:literal)
+  ; initial position
+  (initial-position:integer-array-address <- init-array ((#\R literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\r literal))
+                                              ((#\N literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\n literal))
+                                              ((#\B literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\b literal))
+                                              ((#\Q literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\q literal))
+                                              ((#\K literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\k literal))
+                                              ((#\B literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\b literal))
+                                              ((#\N literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\n literal))
+                                              ((#\R literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\r literal)))
+  (b:board-address <- init-board initial-position:integer-array-address)
+  ; move: a2-a4
+  (m:move-address <- new move:literal)
+  (f:integer-integer-pair-address <- get-address m:move-address/deref from:offset)
+  (dest:integer-address <- get-address f:integer-integer-pair-address/deref 0:offset)
+  (dest:integer-address/deref <- copy 0:literal)  ; from-file: a
+  (dest:integer-address <- get-address f:integer-integer-pair-address/deref 1:offset)
+  (dest:integer-address/deref <- copy 1:literal)  ; from-rank: 2
+  (t0:integer-integer-pair-address <- get-address m:move-address/deref to:offset)
+  (dest:integer-address <- get-address t0:integer-integer-pair-address/deref 0:offset)
+  (dest:integer-address/deref <- copy 0:literal)  ; to-file: a
+  (dest:integer-address <- get-address t0:integer-integer-pair-address/deref 1:offset)
+  (dest:integer-address/deref <- copy 3:literal)  ; to-rank: 4
+  (b:board-address <- make-move b:board-address m:move-address)
+  (print-board screen:terminal-address b:board-address)
+  (1:string-address/raw <- get screen:terminal-address/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+;? (prn memory*.1)
+(when (~screen-contains memory*.1 20
+          (+ "8 | r n b q k b n r "
+             "7 | p p p p p p p p "
+             "6 | _ _ _ _ _ _ _ _ "
+             "5 | _ _ _ _ _ _ _ _ "
+             "4 | P _ _ _ _ _ _ _ "
+             "3 | _ _ _ _ _ _ _ _ "
+             "2 | _ P P P P P P P "
+             "1 | R N B Q K B N R "
+             "  +---------------- "
+             "    a b c d e f g h "))
+  (prn "F - make-move works; chessboard begins at @memory*.1"))
+
+(reset2)
diff --git a/archive/1.vm.arc/chessboard.mu b/archive/1.vm.arc/chessboard.mu
new file mode 100644
index 00000000..45fc12da
--- /dev/null
+++ b/archive/1.vm.arc/chessboard.mu
@@ -0,0 +1,259 @@
+;; data structure: board
+(primitive square)
+(address square-address (square))  ; pointer. verbose but sadly necessary for now
+(array file (square))  ; ranks and files are arrays of squares
+(address file-address (file))
+(address file-address-address (file-address))  ; pointer to a pointer
+(array board (file-address))
+(address board-address (board))
+
+(function init-board [
+  (default-space:space-address <- new space:literal 30:literal)
+  (initial-position:integer-array-address <- next-input)
+  ; assert(length(initial-position) == 64)
+;?   ($print initial-position:integer-array-address/deref) ;? 1
+  (len:integer <- length initial-position:integer-array-address/deref)
+;?   ($print len:integer) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (correct-length?:boolean <- equal len:integer 64:literal)
+  (assert correct-length?:boolean (("chessboard had incorrect size" literal)))
+  (b:board-address <- new board:literal 8:literal)
+  (col:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- equal col:integer 8:literal)
+    (break-if done?:boolean)
+    (file:file-address-address <- index-address b:board-address/deref col:integer)
+    (file:file-address-address/deref <- init-file initial-position:integer-array-address col:integer)
+    (col:integer <- add col:integer 1:literal)
+    (loop)
+  }
+  (reply b:board-address)
+])
+
+(function init-file [
+  (default-space:space-address <- new space:literal 30:literal)
+  (position:integer-array-address <- next-input)
+  (index:integer <- next-input)
+  (index:integer <- multiply index:integer 8:literal)
+  (result:file-address <- new file:literal 8:literal)
+  (row:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- equal row:integer 8:literal)
+    (break-if done?:boolean)
+    (dest:square-address <- index-address result:file-address/deref row:integer)
+    (dest:square-address/deref <- index position:integer-array-address/deref index:integer)
+    (row:integer <- add row:integer 1:literal)
+    (index:integer <- add index:integer 1:literal)
+    (loop)
+  }
+  (reply result:file-address)
+])
+
+(function print-board [
+  (default-space:space-address <- new space:literal 30:literal)
+  (screen:terminal-address <- next-input)
+  (b:board-address <- next-input)
+  (row:integer <- copy 7:literal)
+  ; print each row
+  { begin
+    (done?:boolean <- less-than row:integer 0:literal)
+    (break-if done?:boolean)
+    ; print rank number as a legend
+    (rank:integer <- add row:integer 1:literal)
+    (print-integer screen:terminal-address rank:integer)
+    (s:string-address <- new " | ")
+    (print-string screen:terminal-address s:string-address)
+    ; print each square in the row
+    (col:integer <- copy 0:literal)
+    { begin
+      (done?:boolean <- equal col:integer 8:literal)
+      (break-if done?:boolean)
+      (f:file-address <- index b:board-address/deref col:integer)
+      (s:square <- index f:file-address/deref row:integer)
+      (print-character screen:terminal-address s:square)
+      (print-character screen:terminal-address ((#\space literal)))
+      (col:integer <- add col:integer 1:literal)
+      (loop)
+    }
+    (row:integer <- subtract row:integer 1:literal)
+    (cursor-to-next-line screen:terminal-address)
+    (loop)
+  }
+  ; print file letters as legend
+  (s:string-address <- new "  +----------------")
+  (print-string screen:terminal-address s:string-address)
+  (cursor-to-next-line screen:terminal-address)
+  (s:string-address <- new "    a b c d e f g h")
+  (print-string screen:terminal-address s:string-address)
+  (cursor-to-next-line screen:terminal-address)
+])
+
+;; data structure: move
+(and-record move [
+  from:integer-integer-pair
+  to:integer-integer-pair
+])
+
+(address move-address (move))
+
+(function read-move [
+  (default-space:space-address <- new space:literal 30:literal)
+  (stdin:channel-address <- next-input)
+  (from-file:integer <- read-file stdin:channel-address)
+  { begin
+    (break-if from-file:integer)
+    (reply nil:literal)
+  }
+  (from-rank:integer <- read-rank stdin:channel-address)
+  (expect-stdin stdin:channel-address ((#\- literal)))
+  (to-file:integer <- read-file stdin:channel-address)
+  (to-rank:integer <- read-rank stdin:channel-address)
+  (expect-stdin stdin:channel-address ((#\newline literal)))
+  ; construct the move object
+  (result:move-address <- new move:literal)
+  (f:integer-integer-pair-address <- get-address result:move-address/deref from:offset)
+  (dest:integer-address <- get-address f:integer-integer-pair-address/deref 0:offset)
+  (dest:integer-address/deref <- copy from-file:integer)
+  (dest:integer-address <- get-address f:integer-integer-pair-address/deref 1:offset)
+  (dest:integer-address/deref <- copy from-rank:integer)
+  (t0:integer-integer-pair-address <- get-address result:move-address/deref to:offset)
+  (dest:integer-address <- get-address t0:integer-integer-pair-address/deref 0:offset)
+  (dest:integer-address/deref <- copy to-file:integer)
+  (dest:integer-address <- get-address t0:integer-integer-pair-address/deref 1:offset)
+  (dest:integer-address/deref <- copy to-rank:integer)
+  (reply result:move-address)
+])
+
+; todo: assumes stdin is always at raw address 1
+(function read-file [
+  (default-space:space-address <- new space:literal 30:literal)
+  (stdin:channel-address <- next-input)
+  (x:tagged-value stdin:channel-address/deref <- read stdin:channel-address)
+;?   ($print x:tagged-value) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (a:character <- copy ((#\a literal)))
+  (file-base:integer <- character-to-integer a:character)
+  (c:character <- maybe-coerce x:tagged-value character:literal)
+;?   ($print (("AAA " literal))) ;? 1
+;?   ($print c:character) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  { begin
+    (quit:boolean <- equal c:character ((#\q literal)))
+    (break-unless quit:boolean)
+    (reply nil:literal)
+  }
+  (file:integer <- character-to-integer c:character)
+  (file:integer <- subtract file:integer file-base:integer)
+  ; assert('a' <= from-file <= 'h')
+  (above-min:boolean <- greater-or-equal file:integer 0:literal)
+  (assert above-min:boolean (("file too low" literal)))
+  (below-max:boolean <- lesser-or-equal file:integer 7:literal)
+  (assert below-max:boolean (("file too high" literal)))
+  (reply file:integer)
+])
+
+(function read-rank [
+  (default-space:space-address <- new space:literal 30:literal)
+  (stdin:channel-address <- next-input)
+  (x:tagged-value stdin:channel-address/deref <- read stdin:channel-address)
+  (c:character <- maybe-coerce x:tagged-value character:literal)
+;?   ($print (("BBB " literal))) ;? 1
+;?   ($print c:character) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  { begin
+    (quit:boolean <- equal c:character ((#\q literal)))
+    (break-unless quit:boolean)
+    (reply nil:literal)
+  }
+  (rank:integer <- character-to-integer c:character)
+  (one:character <- copy ((#\1 literal)))
+  (rank-base:integer <- character-to-integer one:character)
+  (rank:integer <- subtract rank:integer rank-base:integer)
+  ; assert('1' <= rank <= '8')
+  (above-min:boolean <- greater-or-equal rank:integer 0:literal)
+  (assert above-min:boolean (("rank too low" literal)))
+  (below-max:boolean <- lesser-or-equal rank:integer 7:literal)
+  (assert below-max:boolean (("rank too high" literal)))
+  (reply rank:integer)
+])
+
+; slurp a character and check that it matches
+(function expect-stdin [
+  (default-space:space-address <- new space:literal 30:literal)
+  (stdin:channel-address <- next-input)
+  (x:tagged-value stdin:channel-address/deref <- read stdin:channel-address)
+  (c:character <- maybe-coerce x:tagged-value character:literal)
+  (expected:character <- next-input)
+  (match?:boolean <- equal c:character expected:character)
+  (assert match?:boolean (("expected character not found" literal)))
+])
+
+(function make-move [
+  (default-space:space-address <- new space:literal 30:literal)
+  (b:board-address <- next-input)
+  (m:move-address <- next-input)
+  (x:integer-integer-pair <- get m:move-address/deref from:offset)
+  (from-file:integer <- get x:integer-integer-pair 0:offset)
+  (from-rank:integer <- get x:integer-integer-pair 1:offset)
+  (f:file-address <- index b:board-address/deref from-file:integer)
+  (src:square-address <- index-address f:file-address/deref from-rank:integer)
+  (x:integer-integer-pair <- get m:move-address/deref to:offset)
+  (to-file:integer <- get x:integer-integer-pair 0:offset)
+  (to-rank:integer <- get x:integer-integer-pair 1:offset)
+  (f:file-address <- index b:board-address/deref to-file:integer)
+  (dest:square-address <- index-address f:file-address/deref to-rank:integer)
+  (dest:square-address/deref <- copy src:square-address/deref)
+  (src:square-address/deref <- copy ((#\_ literal)))
+  (reply b:board-address)
+])
+
+(function chessboard [
+  (default-space:space-address <- new space:literal 30:literal)
+  (initial-position:integer-array-address <- init-array ((#\R literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\r literal))
+                                                        ((#\N literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\n literal))
+                                                        ((#\B literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\b literal))
+                                                        ((#\Q literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\q literal))
+                                                        ((#\K literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\k literal))
+                                                        ((#\B literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\b literal))
+                                                        ((#\N literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\n literal))
+                                                        ((#\R literal)) ((#\P literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\_ literal)) ((#\p literal)) ((#\r literal)))
+  (b:board-address <- init-board initial-position:integer-array-address)
+  (cursor-mode)
+  ; hook up stdin
+  (stdin:channel-address <- init-channel 1:literal)
+  (fork-helper send-keys-to-stdin:fn nil:literal/globals nil:literal/limit nil:literal/keyboard stdin:channel-address)
+  ; buffer stdin
+  (buffered-stdin:channel-address <- init-channel 1:literal)
+  (fork-helper buffer-lines:fn nil:literal/globals nil:literal/limit stdin:channel-address buffered-stdin:channel-address)
+  ($print (("Stupid text-mode chessboard. White pieces in uppercase; black pieces in lowercase. No checking for legal moves." literal)))
+  (cursor-to-next-line nil:literal/terminal)
+  { begin
+    (cursor-to-next-line nil:literal/terminal)
+    (print-board nil:literal/terminal b:board-address)
+    (cursor-to-next-line nil:literal/terminal)
+    ($print (("Type in your move as <from square>-<to square>. For example: 'a2-a4'. Then press <enter>." literal)))
+    (cursor-to-next-line nil:literal/terminal)
+    ($print (("Hit 'q' to exit." literal)))
+    (cursor-to-next-line nil:literal/terminal)
+    ($print (("move: " literal)))
+    (m:move-address <- read-move buffered-stdin:channel-address)
+;?     (retro-mode) ;? 1
+;?     ($print stdin:channel-address) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+;?     ($print buffered-stdin:channel-address) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+;?     ($dump-memory) ;? 1
+;?     (cursor-mode) ;? 1
+    (break-unless m:move-address)
+    (b:board-address <- make-move b:board-address m:move-address)
+    (loop)
+  }
+  (retro-mode)
+])
+
+(function main [
+  (chessboard)
+])
+
+; todo:
+;   backspace, ctrl-u
diff --git a/archive/1.vm.arc/color-repl.mu b/archive/1.vm.arc/color-repl.mu
new file mode 100644
index 00000000..ced6a89f
--- /dev/null
+++ b/archive/1.vm.arc/color-repl.mu
@@ -0,0 +1,498 @@
+; a simple line editor for reading lisp expressions.
+; colors strings and comments. nested parens get different colors.
+;
+; needs to do its own raw keyboard/screen management since we need to decide
+; how to color each key right as it is printed.
+; lots of logic devoted to handling backspace correctly.
+
+; keyboard screen abort continuation -> string
+(function read-expression [
+  (default-space:space-address <- new space:literal 60:literal)
+  (k:keyboard-address <- next-input)
+  (screen:terminal-address <- next-input)
+  (abort:continuation <- next-input)
+  (history:buffer-address <- next-input)  ; buffer of strings
+  (history-length:integer <- get history:buffer-address/deref length:offset)
+  (current-history-index:integer <- copy history-length:integer)
+  (result:buffer-address <- init-buffer 10:literal)  ; string to maybe add to
+  (open-parens:integer <- copy 0:literal)  ; for balancing parens and tracking nesting depth
+  ; we can change color when backspacing over parens or comments or strings,
+  ; but we need to know that they aren't escaped
+  (escapes:buffer-address <- init-buffer 5:literal)
+  ; to not return after just a comment
+  (not-empty?:boolean <- copy nil:literal)
+  { begin
+    ; repeatedly read keys from the keyboard
+    ;   test: 34<enter>
+    (done?:boolean <- process-key default-space:space-address k:keyboard-address screen:terminal-address)
+    (loop-unless done?:boolean)
+  }
+  ; trim trailing newline in result (easier history management below)
+  { begin
+    (l:character <- last result:buffer-address)
+    (trailing-newline?:boolean <- equal l:character ((#\newline literal)))
+    (break-unless trailing-newline?:boolean)
+    (len:integer-address <- get-address result:buffer-address/deref length:offset)
+    (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
+  }
+  ; test: 3<enter> => size of s is 2
+  (s:string-address <- to-array result:buffer-address)
+  (reply s:string-address)
+])
+
+(function process-key [  ; return t to signal end of expression
+  (default-space:space-address <- new space:literal 60:literal)
+  (0:space-address/names:read-expression <- next-input)
+  (k:keyboard-address <- next-input)
+  (screen:terminal-address <- next-input)
+  (c:character <- wait-for-key k:keyboard-address silent:literal/terminal)
+  (len:integer-address <- get-address result:buffer-address/space:1/deref length:offset)
+  (maybe-cancel-this-expression c:character abort:continuation/space:1)
+  ; check for ctrl-d and exit
+  { begin
+    (eof?:boolean <- equal c:character ((ctrl-d literal)))
+    (break-unless eof?:boolean)
+    ; return empty expression
+    (s:string-address-address <- get-address result:buffer-address/space:1/deref data:offset)
+    (s:string-address-address/deref <- copy nil:literal)
+    (reply t:literal)
+  }
+  ; check for backspace
+  ;   test: 3<backspace>4<enter>
+  ;   todo: backspace past newline
+  { begin
+    (backspace?:boolean <- equal c:character ((#\backspace literal)))
+    (break-unless backspace?:boolean)
+    (print-character screen:terminal-address c:character/backspace)
+    { begin
+      ; delete last character if any
+      (zero?:boolean <- lesser-or-equal len:integer-address/deref 0:literal)
+      (break-if zero?:boolean)
+      (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
+      ; switch colors
+      ;   test: "a"<backspace>bc"
+      ;   test: "a\"<backspace>bc"
+      { begin
+        (backspaced-over-close-quote?:boolean <- backspaced-over-unescaped? result:buffer-address/space:1 ((#\" literal)) escapes:buffer-address/space:1)  ; "
+        (break-unless backspaced-over-close-quote?:boolean)
+        (slurp-string result:buffer-address/space:1 escapes:buffer-address/space:1 abort:continuation/space:1 k:keyboard-address screen:terminal-address)
+        (reply nil:literal)
+      }
+      ;   test: (+ 1 (<backspace>2)
+      ;   test: (+ 1 #\(<backspace><backspace><backspace>2)
+      { begin
+        (backspaced-over-open-paren?:boolean <- backspaced-over-unescaped? result:buffer-address/space:1 ((#\( literal)) escapes:buffer-address/space:1)
+        (break-unless backspaced-over-open-paren?:boolean)
+        (open-parens:integer/space:1 <- subtract open-parens:integer/space:1 1:literal)
+        (reply nil:literal)
+      }
+      ;   test: (+ 1 2)<backspace> 3)
+      ;   test: (+ 1 2#\)<backspace><backspace><backspace> 3)
+      { begin
+        (backspaced-over-close-paren?:boolean <- backspaced-over-unescaped? result:buffer-address/space:1 ((#\) literal)) escapes:buffer-address/space:1)
+        (break-unless backspaced-over-close-paren?:boolean)
+        (open-parens:integer/space:1 <- add open-parens:integer/space:1 1:literal)
+        (reply nil:literal)
+      }
+    }
+    (reply nil:literal)
+  }
+  ; up arrow; switch to previous item in history
+  { begin
+    (up-arrow?:boolean <- equal c:character ((up literal)))
+    (break-unless up-arrow?:boolean)
+    ; if history exists
+    ;   test: <up><enter>  up without history has no effect
+    { begin
+      (empty-history?:boolean <- lesser-or-equal history-length:integer/space:1 0:literal)
+      (break-unless empty-history?:boolean)
+      (reply nil:literal)
+    }
+    ; if pointer not already at start of history
+    ;   test: 34<enter><up><up><enter>  up past history has no effect
+    { begin
+      (at-history-start?:boolean <- lesser-or-equal current-history-index:integer/space:1 0:literal)
+      (break-unless at-history-start?:boolean)
+      (reply nil:literal)
+    }
+    ; then update history index, copy into current buffer
+    ;   test: 34<enter><up><enter>  up restores previous command
+    ;   test todo: 34<enter>23<up>34<down><enter>  up doesn't mess up typing on current line
+    ;   test todo: 34<enter><up>5<enter><up><up>  commands don't modify history
+    ;   test todo: multi-line expressions
+    ; identify the history item
+    (current-history-index:integer/space:1 <- subtract current-history-index:integer/space:1 1:literal)
+    (switch-to-history 0:space-address screen:terminal-address)
+    ; <enter> is trimmed in the history expression, so wait for the human to
+    ; hit <enter> again or backspace to make edits
+    (reply nil:literal)
+  }
+  ; down arrow; switch to next item in history
+  { begin
+    (down-arrow?:boolean <- equal c:character ((down literal)))
+    (break-unless down-arrow?:boolean)
+    ; if history exists
+    ;   test: <down><enter>  down without history has no effect
+    { begin
+      (empty-history?:boolean <- lesser-or-equal history-length:integer/space:1 0:literal)
+      (break-unless empty-history?:boolean)
+      (reply nil:literal)
+    }
+    ; if pointer not already at end of history
+    ;   test: 34<enter><down><down><enter>  up past history has no effect
+    { begin
+      (x:integer <- subtract history-length:integer/space:1 1:literal)
+      (before-history-end?:boolean <- greater-or-equal current-history-index:integer/space:1 x:integer)
+      (break-unless before-history-end?:boolean)
+      (reply nil:literal)
+    }
+    ; then update history index, copy into current buffer
+    ;   test: 34<enter><up><enter>  up restores previous command
+    ;   test todo: 34<enter>23<up>34<down><enter>  up doesn't mess up typing on current line
+    ;   test todo: 34<enter><up>5<enter><up><up>  commands don't modify history
+    ;   test todo: multi-line expressions
+    ; identify the history item
+    (current-history-index:integer/space:1 <- add current-history-index:integer/space:1 1:literal)
+    (switch-to-history 0:space-address screen:terminal-address)
+    ; <enter> is trimmed in the history expression, so wait for the human to
+    ; hit <enter> again or backspace to make edits
+    (reply nil:literal)
+  }
+  ; if it's a newline, decide whether to return
+  ;   test: <enter>34<enter>
+  { begin
+    (newline?:boolean <- equal c:character ((#\newline literal)))
+    (break-unless newline?:boolean)
+    (print-character screen:terminal-address c:character/newline)
+    (at-top-level?:boolean <- lesser-or-equal open-parens:integer/space:1 0:literal)
+    (end-expression?:boolean <- and at-top-level?:boolean not-empty?:boolean/space:1)
+    (reply end-expression?:boolean)
+  }
+  ; printable character; save
+;?   ($print (("append\n" literal))) ;? 2
+  (result:buffer-address/space:1 <- append result:buffer-address/space:1 c:character)
+;?   ($print (("done\n" literal))) ;? 2
+  ; if it's backslash, read, save and print one additional character
+  ;   test: (prn #\()
+  { begin
+    (backslash?:boolean <- equal c:character ((#\\ literal)))
+    (break-unless backslash?:boolean)
+    (print-character screen:terminal-address c:character/backslash 7:literal/white)
+    (result:buffer-address/space:1 escapes:buffer-address/space:1 <- slurp-escaped-character result:buffer-address/space:1 7:literal/white escapes:buffer-address/space:1 abort:continuation/space:1 k:keyboard-address screen:terminal-address)
+    (reply nil:literal)
+  }
+  ; if it's a semi-colon, parse a comment
+  { begin
+    (comment?:boolean <- equal c:character ((#\; literal)))
+    (break-unless comment?:boolean)
+    (print-character screen:terminal-address c:character/semi-colon 4:literal/fg/blue)
+    (comment-read?:boolean <- slurp-comment result:buffer-address/space:1 escapes:buffer-address/space:1 abort:continuation/space:1 k:keyboard-address screen:terminal-address)
+    ; return if comment was read (i.e. consumed a newline)
+    ; test: ;a<backspace><backspace> (shouldn't end command until <enter>)
+    { begin
+      (break-if comment-read?:boolean)
+      (reply nil:literal)
+    }
+    ; and we're not within parens
+    ;   test: (+ 1 2)  ; comment<enter>
+    ;   test: (+ 1<enter>; abc<enter>2)<enter>
+    ;   test: ; comment<enter>(+ 1 2)<enter>
+    ;   too expensive to build: 3<backspace>; comment<enter>(+ 1 2)<enter>
+    (at-top-level?:boolean <- lesser-or-equal open-parens:integer/space:1 0:literal)
+    (end-expression?:boolean <- and at-top-level?:boolean not-empty?:boolean/space:1)
+    (reply end-expression?:boolean)
+  }
+  ; if it's not whitespace, set not-empty? and continue
+  { begin
+    (space?:boolean <- equal c:character ((#\space literal)))
+    (break-if space?:boolean)
+    (newline?:boolean <- equal c:character ((#\newline literal)))
+    (break-if newline?:boolean)
+    (tab?:boolean <- equal c:character ((tab literal)))
+    (break-if tab?:boolean)
+    (not-empty?:boolean/space:1 <- copy t:literal)
+    ; fall through
+  }
+  ; if it's a quote, parse a string
+  { begin
+    (string-started?:boolean <- equal c:character ((#\" literal)))  ; for vim: "
+    (break-unless string-started?:boolean)
+    (print-character screen:terminal-address c:character/open-quote 6:literal/fg/cyan)
+    (slurp-string result:buffer-address/space:1 escapes:buffer-address/space:1 abort:continuation/space:1 k:keyboard-address screen:terminal-address)
+    (reply nil:literal)
+  }
+  ; color parens by depth, so they're easy to balance
+  ;   test: (+ 1 1)<enter>
+  ;   test: (def foo () (+ 1 (* 2 3)))<enter>
+  { begin
+    (open-paren?:boolean <- equal c:character ((#\( literal)))
+    (break-unless open-paren?:boolean)
+    (_ color-code:integer <- divide-with-remainder open-parens:integer/space:1 3:literal)  ; 3 distinct colors for parens
+    (color-code:integer <- add color-code:integer 1:literal)
+    (print-character screen:terminal-address c:character/open-paren color-code:integer)
+    (open-parens:integer/space:1 <- add open-parens:integer/space:1 1:literal)
+;?     ($print open-parens:integer/space:1) ;? 2
+    (reply nil:literal)
+  }
+  { begin
+    (close-paren?:boolean <- equal c:character ((#\) literal)))
+    (break-unless close-paren?:boolean)
+    (open-parens:integer/space:1 <- subtract open-parens:integer/space:1 1:literal)
+    (_ color-code:integer <- divide-with-remainder open-parens:integer/space:1 3:literal)  ; 3 distinct colors for parens
+    (color-code:integer <- add color-code:integer 1:literal)
+    (print-character screen:terminal-address c:character/close-paren color-code:integer)
+;?     ($print open-parens:integer/space:1) ;? 2
+    (reply nil:literal)
+  }
+  ; if all else fails, print the character without color
+  (print-character screen:terminal-address c:character/regular)
+  ;   todo: error on space outside parens, like python
+  ;   todo: []
+  ;   todo: history on up/down
+  (reply nil:literal)
+])
+
+(function switch-to-history [
+  (default-space:space-address <- new space:literal 30:literal)
+  (0:space-address/names:read-expression <- next-input)
+  (screen:terminal-address <- next-input)
+  (clear-repl-state 0:space-address)
+  (curr-history:string-address <- buffer-index history:buffer-address/space:1 current-history-index:integer/space:1)
+  (curr-history-len:integer <- length curr-history:string-address/deref)
+  ; and retype it into the current expression
+  (hist:keyboard-address <- init-keyboard curr-history:string-address)
+  (hist-index:integer-address <- get-address hist:keyboard-address/deref index:offset)
+  { begin
+    (done?:boolean <- greater-or-equal hist-index:integer-address/deref curr-history-len:integer)
+    (break-if done?:boolean)
+    (sub-return:boolean <- process-key 0:space-address hist:keyboard-address screen:terminal-address)
+    (assert-false sub-return:boolean (("recursive call to process keys thought it was done" literal)))
+    (loop)
+  }
+])
+
+(function clear-repl-state [
+  (default-space:space-address/names:read-expression <- next-input)
+  ; clear result
+  (len:integer-address <- get-address result:buffer-address/deref length:offset)
+  (backspace-over len:integer-address/deref screen:terminal-address)
+  (len:integer-address/deref <- copy 0:literal)
+  ; clear other state accumulated for the existing expression
+  (open-parens:integer <- copy 0:literal)
+  (escapes:buffer-address <- init-buffer 5:literal)
+  (not-empty?:boolean <- copy nil:literal)
+])
+
+(function backspace-over [
+  (default-space:space-address <- new space:literal 30:literal)
+  (len:integer <- next-input)
+  (screen:terminal-address <- next-input)
+  { begin
+    (done?:boolean <- lesser-or-equal len:integer 0:literal)
+    (break-if done?:boolean)
+    (print-character screen:terminal-address ((#\backspace literal)))
+    (len:integer <- subtract len:integer 1:literal)
+    (loop)
+  }
+])
+
+; list of characters, list of indices of escaped characters, abort continuation
+; -> whether a comment was consumed (can also return by backspacing past comment leader ';')
+(function slurp-comment [
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (escapes:buffer-address <- next-input)
+  (abort:continuation <- next-input)
+  (k:keyboard-address <- next-input)
+  (screen:terminal-address <- next-input)
+  ; test: ; abc<enter>
+  { begin
+    next-key-in-comment
+    (c:character <- wait-for-key k:keyboard-address silent:literal/terminal)
+    (maybe-cancel-this-expression c:character abort:continuation screen:terminal-address)  ; test: check needs to come before print
+    (print-character screen:terminal-address c:character 4:literal/fg/blue)
+    ; handle backspace
+    ;   test: ; abc<backspace><backspace>def<enter>
+    ;   todo: how to exit comment?
+    { begin
+      (backspace?:boolean <- equal c:character ((#\backspace literal)))
+      (break-unless backspace?:boolean)
+      (len:integer-address <- get-address in:buffer-address/deref length:offset)
+      ; buffer has to have at least the semi-colon so can't be empty
+      (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
+      ; if we erase start of comment, return
+      (comment-deleted?:boolean <- backspaced-over-unescaped? in:buffer-address ((#\; literal)) escapes:buffer-address)  ; "
+      (jump-unless comment-deleted?:boolean next-key-in-comment:offset)  ; loop
+      (reply nil:literal/read-comment?)
+    }
+    (in:buffer-address <- append in:buffer-address c:character)
+    (newline?:boolean <- equal c:character ((#\newline literal)))
+    (loop-unless newline?:boolean)
+  }
+  (reply t:literal/read-comment?)
+])
+
+(function slurp-string [
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (escapes:buffer-address <- next-input)
+  (abort:continuation <- next-input)
+  (k:keyboard-address <- next-input)
+  (screen:terminal-address <- next-input)
+  ; test: "abc"
+  { begin
+    next-key-in-string
+    (c:character <- wait-for-key k:keyboard-address silent:literal/terminal)
+    (maybe-cancel-this-expression c:character abort:continuation screen:terminal-address)  ; test: check needs to come before print
+    (print-character screen:terminal-address c:character 6:literal/fg/cyan)
+    ; handle backspace
+    ;   test: "abc<backspace>d"
+    ;   todo: how to exit string?
+    { begin
+      (backspace?:boolean <- equal c:character ((#\backspace literal)))
+      (break-unless backspace?:boolean)
+      (len:integer-address <- get-address in:buffer-address/deref length:offset)
+      ; typed a quote before calling slurp-string, so can't be empty
+      (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
+      ; if we erase start of string, return
+      ;   test: "<backspace>34
+      (string-deleted?:boolean <- backspaced-over-unescaped? in:buffer-address ((#\" literal)) escapes:buffer-address)  ; "
+;?       ($print string-deleted?:boolean) ;? 1
+      (jump-if string-deleted?:boolean end:offset)  ; break
+      (jump next-key-in-string:offset)  ; loop
+    }
+    (in:buffer-address <- append in:buffer-address c:character)
+    ; break on quote -- unless escaped by backslash
+    ;   test: "abc\"ef"
+    { begin
+      (backslash?:boolean <- equal c:character ((#\\ literal)))
+      (break-unless backslash?:boolean)
+      (in:buffer-address escapes:buffer-address <- slurp-escaped-character in:buffer-address 6:literal/cyan escapes:buffer-address abort:continuation k:keyboard-address screen:terminal-address)
+      (jump next-key-in-string:offset)  ; loop
+    }
+    ; if not backslash
+    (end-quote?:boolean <- equal c:character ((#\" literal)))  ; for vim: "
+    (loop-unless end-quote?:boolean)
+  }
+  end
+])
+
+; buffer to add character to, color to print it in to the screen, abort continuation
+(function slurp-escaped-character [
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (color-code:integer <- next-input)
+  (escapes:buffer-address <- next-input)
+  (abort:continuation <- next-input)
+  (k:keyboard-address <- next-input)
+  (screen:terminal-address <- next-input)
+  (c:character <- wait-for-key k:keyboard-address silent:literal/terminal)
+  (maybe-cancel-this-expression c:character abort:continuation screen:terminal-address)  ; test: check needs to come before print
+  (print-character screen:terminal-address c:character color-code:integer)
+  (len:integer-address <- get-address in:buffer-address/deref length:offset)
+  (escapes:buffer-address <- append escapes:buffer-address len:integer-address/deref)
+;?   ($print (("+" literal))) ;? 1
+  ; handle backspace
+  ;   test: "abc\<backspace>def"
+  ;   test: #\<backspace>
+  { begin
+    (backspace?:boolean <- equal c:character ((#\backspace literal)))
+    (break-unless backspace?:boolean)
+    ; just typed a backslash, so buffer can't be empty
+    (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
+    (elen:integer-address <- get-address escapes:buffer-address/deref length:offset)
+    (elen:integer-address/deref <- subtract elen:integer-address/deref 1:literal)
+;?     ($print (("-" literal))) ;? 1
+    (reply in:buffer-address/same-as-arg:0 escapes:buffer-address/same-as-arg:2)
+  }
+  ; if not backspace, save and return
+  (in:buffer-address <- append in:buffer-address c:character)
+  (reply in:buffer-address/same-as-arg:0 escapes:buffer-address/same-as-arg:2)
+])
+
+(function backspaced-over-unescaped? [
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (expected:character <- next-input)
+  (escapes:buffer-address <- next-input)
+  ; char just backspaced over matches
+  { begin
+    (c:character <- past-last in:buffer-address)
+    (char-match?:boolean <- equal c:character expected:character)
+    (break-if char-match?:boolean)
+    (reply nil:literal)
+  }
+  ; and char before cursor is not an escape
+  { begin
+    (most-recent-escape:integer <- last escapes:buffer-address)
+    (last-idx:integer <- get in:buffer-address/deref length:offset)
+;?     ($print most-recent-escape:integer) ;? 1
+;?     ($print last-idx:integer) ;? 1
+    (was-unescaped?:boolean <- not-equal last-idx:integer most-recent-escape:integer)
+    (break-if was-unescaped?:boolean)
+    (reply nil:literal)
+  }
+  (reply t:literal)
+])
+
+; return the character past the end of the buffer, if there's room
+(function past-last [
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (n:integer <- get in:buffer-address/deref length:offset)
+  (s:string-address <- get in:buffer-address/deref data:offset)
+  (capacity:integer <- length s:string-address/deref)
+  { begin
+    (no-space?:boolean <- greater-or-equal n:integer capacity:integer)
+    (break-unless no-space?:boolean)
+    (reply ((#\null literal)))
+  }
+  (result:character <- index s:string-address/deref n:integer)
+  (reply result:character)
+])
+
+(function maybe-cancel-this-expression [
+  ; check for ctrl-g and abort
+  (default-space:space-address <- new space:literal 30:literal)
+  (c:character <- next-input)
+  (abort:continuation <- next-input)
+  (screen:terminal-address <- next-input)
+  { begin
+    (interrupt?:boolean <- equal c:character ((ctrl-g literal)))
+    (break-unless interrupt?:boolean)
+    (print-character screen:terminal-address ((#\^ literal)))
+    (print-character screen:terminal-address ((#\G literal)))
+    (print-character screen:terminal-address ((#\newline literal)))
+    (continue-from abort:continuation)
+  }
+])
+
+(function main [
+  (default-space:space-address <- new space:literal 30:literal)
+  (cursor-mode)
+  ($print (("connected to anarki! type in an expression, then hit enter. ctrl-d exits. ctrl-g clears the current expression." literal)))
+  (print-character nil:literal/terminal ((#\newline literal)))
+  ; todo: ctrl-g shouldn't clear history
+  (abort:continuation <- current-continuation)
+  (history:buffer-address <- init-buffer 5:literal)  ; buffer of buffers of strings, one per expression typed in
+  { begin
+    (s:string-address <- read-expression nil:literal/keyboard nil:literal/terminal abort:continuation history:buffer-address)
+    (break-unless s:string-address)
+;?     (x:integer <- length s:string-address/deref) ;? 1
+;?     ($print x:integer) ;? 1
+;?     ($print ((#\newline literal))) ;? 1
+    (history:buffer-address <- append history:buffer-address s:string-address)
+;?     (len:integer <- get history:buffer-address/deref length:offset) ;? 1
+;?     ($print len:integer) ;? 1
+;?     ($print ((#\newline literal))) ;? 1
+    (retro-mode)  ; print errors cleanly
+;?       (print-string nil:literal/terminal s:string-address) ;? 1
+      (t:string-address <- $eval s:string-address)
+    (cursor-mode)
+    ($print (("=> " literal)))
+    (print-string nil:literal/terminal t:string-address)
+    (print-character nil:literal/terminal ((#\newline literal)))
+    (print-character nil:literal/terminal ((#\newline literal)))  ; empty line separates each expression and result
+    (loop)
+  }
+])
diff --git a/archive/1.vm.arc/counters.mu b/archive/1.vm.arc/counters.mu
new file mode 100644
index 00000000..0e414513
--- /dev/null
+++ b/archive/1.vm.arc/counters.mu
@@ -0,0 +1,33 @@
+(function init-counter [
+  (default-space:space-address <- new space:literal 30:literal)
+  (n:integer <- next-input)
+  (reply default-space:space-address)
+ ])
+
+(function increment-counter [
+  (default-space:space-address <- new space:literal 30:literal)
+  (0:space-address/names:init-counter <- next-input)  ; setup outer space; it *must* come from 'init-counter'
+  (x:integer <- next-input)
+  (n:integer/space:1 <- add n:integer/space:1 x:integer)
+  (reply n:integer/space:1)
+ ])
+
+(function main [
+  (default-space:space-address <- new space:literal 30:literal)
+  ; counter A
+  (a:space-address <- init-counter 34:literal)
+  ; counter B
+  (b:space-address <- init-counter 23:literal)
+  ; increment both by 2 but in different ways
+  (increment-counter a:space-address 1:literal)
+  (bres:integer <- increment-counter b:space-address 2:literal)
+  (ares:integer <- increment-counter a:space-address 1:literal)
+  ; check results
+  ($print (("Contents of counters a: " literal)))
+  (print-integer nil:literal/terminal ares:integer)
+  ($print ((" b: " literal)))
+  (print-integer nil:literal/terminal bres:integer)
+  ($print (("\n" literal)))
+ ])
+
+; compare http://www.paulgraham.com/accgen.html
diff --git a/archive/1.vm.arc/edit.arc.t b/archive/1.vm.arc/edit.arc.t
new file mode 100644
index 00000000..ff039602
--- /dev/null
+++ b/archive/1.vm.arc/edit.arc.t
@@ -0,0 +1,33 @@
+(selective-load "mu.arc" section-level)
+(set allow-raw-addresses*)
+
+(section 100
+
+(reset)
+(new-trace "new-screen")
+(add-code:readfile "edit.mu")
+(add-code
+  '((function test-new-screen [
+      (1:screen-address/global <- new-screen 5:literal 5:literal)
+     ])))
+;? (each stmt function*!new-screen
+;?   (prn stmt))
+(let routine make-routine!test-new-screen
+  (let before rep.routine!alloc
+;?     (= dump-trace* (obj blacklist '("sz" "m" "setm" "addr" "cvt0" "cvt1")))
+    (run 'test-new-screen)
+;?     (prn memory*)
+;?     (prn memory*.2001)
+    (when (~is (memory* memory*.1) 5)  ; number of rows
+      (prn "F - newly-allocated screen doesn't have the right number of rows: @(memory* memory*!2001)"))
+    (let row-pointers (let base (+ 1 memory*.1)
+                        (range base (+ base 4)))
+  ;?     (prn row-pointers)
+      (when (some nil (map memory* row-pointers))
+        (prn "F - newly-allocated screen didn't initialize all of its row pointers"))
+      (when (~all 5 (map memory* (map memory* row-pointers)))
+        (prn "F - newly-allocated screen didn't initialize all of its row lengths")))))
+
+(reset)
+
+)  ; section 100 for all editor code
diff --git a/archive/1.vm.arc/edit.mu b/archive/1.vm.arc/edit.mu
new file mode 100644
index 00000000..ebf43161
--- /dev/null
+++ b/archive/1.vm.arc/edit.mu
@@ -0,0 +1,18 @@
+; a screen is an array of pointers to lines, in turn arrays of characters
+
+(function new-screen [
+  (default-space:space-address <- new space:literal 30:literal)
+  (nrows:integer <- next-input)
+  (ncols:integer <- next-input)
+  (result:screen-address <- new screen:literal nrows:integer)
+  (rowidx:integer <- copy 0:literal)
+  { begin
+    (curr-line-address-address:line-address-address <- index-address result:screen-address/deref rowidx:integer)
+    (curr-line-address-address:line-address-address/deref <- new line:literal ncols:integer)
+    (curr-line-address:line-address <- copy curr-line-address-address:line-address-address/deref)
+    (rowidx:integer <- add rowidx:integer 1:literal)
+    (x:boolean <- not-equal rowidx:integer nrows:integer)
+    (loop-if x:boolean)
+  }
+  (reply result:screen-address)
+])
diff --git a/archive/1.vm.arc/exuberant-ctags-rc b/archive/1.vm.arc/exuberant-ctags-rc
new file mode 100644
index 00000000..7d99b0b8
--- /dev/null
+++ b/archive/1.vm.arc/exuberant-ctags-rc
@@ -0,0 +1,7 @@
+--langdef=mu
+--langmap=mu:.mu
+--regex-mu=/^\(function[ \t]+([^ \t\[]+)/\1/d,definition/
+--regex-mu=/^\(recipe[ \t]+([^ \t\[]+)/\1/d,definition/
+--regex-mu=/^\(and-record[ \t]+([^ \t\[]+)/\1/t,type/
+--regex-mu=/^\(address[ \t]+([^ \t\[]+)/\1/t,type/
+--regex-mu=/^\(array[ \t]+([^ \t\[]+)/\1/t,type/
diff --git a/archive/1.vm.arc/factorial.mu b/archive/1.vm.arc/factorial.mu
new file mode 100644
index 00000000..96a28fd3
--- /dev/null
+++ b/archive/1.vm.arc/factorial.mu
@@ -0,0 +1,22 @@
+(function factorial [
+  (default-space:space-address <- new space:literal 30:literal)
+  (n:integer <- next-input)
+  { begin
+    ; if n=0 return 1
+    (zero?:boolean <- equal n:integer 0:literal)
+    (break-unless zero?:boolean)
+    (reply 1:literal)
+  }
+  ; return n*factorial(n-1)
+  (x:integer <- subtract n:integer 1:literal)
+  (subresult:integer <- factorial x:integer)
+  (result:integer <- multiply subresult:integer n:integer)
+  (reply result:integer)
+])
+
+(function main [
+  (1:integer <- factorial 5:literal)
+  ($print (("result: " literal)))
+  (print-integer nil:literal/terminal 1:integer)
+  ($print (("\n" literal)))
+])
diff --git a/archive/1.vm.arc/fork.mu b/archive/1.vm.arc/fork.mu
new file mode 100644
index 00000000..8d6463a8
--- /dev/null
+++ b/archive/1.vm.arc/fork.mu
@@ -0,0 +1,18 @@
+(function main [
+  (fork thread2:fn)
+  (default-space:space-address <- new space:literal 2:literal)
+  (x:integer <- copy 34:literal)
+  { begin
+    (print-integer nil:literal/terminal x:integer)
+    (loop)
+  }
+])
+
+(function thread2 [
+  (default-space:space-address <- new space:literal 2:literal)
+  (y:integer <- copy 35:literal)
+  { begin
+    (print-integer nil:literal/terminal y:integer)
+    (loop)
+  }
+])
diff --git a/archive/1.vm.arc/generic.mu b/archive/1.vm.arc/generic.mu
new file mode 100644
index 00000000..1c4b9bb0
--- /dev/null
+++ b/archive/1.vm.arc/generic.mu
@@ -0,0 +1,30 @@
+; To demonstrate generic functions, we'll construct a factorial function with
+; separate base and recursive clauses. Compare factorial.mu.
+
+; factorial n = n*factorial(n-1)
+(function factorial [
+  (default-space:space-address <- new space:literal 30:literal)
+  (n:integer <- input 0:literal)
+  (x:integer <- subtract n:integer 1:literal)
+  (subresult:integer <- factorial x:integer)
+  (result:integer <- multiply subresult:integer n:integer)
+  (reply result:integer)
+])
+
+; factorial 0 = 1
+(function factorial [
+  (default-space:space-address <- new space:literal 30:literal)
+  (n:integer <- input 0:literal)
+  { begin
+    (zero?:boolean <- equal n:integer 0:literal)
+    (break-unless zero?:boolean)
+    (reply 1:literal)
+  }
+])
+
+(function main [
+  (1:integer <- factorial 5:literal)
+  ($print (("result: " literal)))
+  (print-integer nil:literal/terminal 1:integer)
+  ($print (("\n" literal)))
+])
diff --git a/archive/1.vm.arc/graphics.mu b/archive/1.vm.arc/graphics.mu
new file mode 100644
index 00000000..f25395ef
--- /dev/null
+++ b/archive/1.vm.arc/graphics.mu
@@ -0,0 +1,23 @@
+; open a viewport, print coordinates of mouse clicks
+; currently need to ctrl-c to exit after closing the viewport
+(function main [
+  (window-on (("practice" literal)) 300:literal 300:literal)
+  { begin
+    (pos:integer-integer-pair click?:boolean <- mouse-position)
+    (loop-unless click?:boolean)
+    (x:integer <- get pos:integer-integer-pair 0:offset)
+    (y:integer <- get pos:integer-integer-pair 1:offset)
+;?     ($print (("AAA " literal)))
+;?     ($print x:integer)
+;?     ($print ((", " literal)))
+;?     ($print y:integer)
+;?     ($print (("\n" literal)))
+    (print-integer nil:literal/terminal x:integer)
+    (print-character nil:literal/terminal ((#\, literal)))
+    (print-character nil:literal/terminal ((#\space literal)))
+    (print-integer nil:literal/terminal y:integer)
+    (print-character nil:literal/terminal ((#\newline literal)))
+    (loop)
+  }
+  (window-off)
+])
diff --git a/archive/1.vm.arc/highlights b/archive/1.vm.arc/highlights
new file mode 100644
index 00000000..bb81fb56
--- /dev/null
+++ b/archive/1.vm.arc/highlights
@@ -0,0 +1,21 @@
+" vim: ft=vim
+" Data-flow highlighting: http://www.reddit.com/r/programming/comments/1w76um/coding_in_color/cezpios
+
+highlight highlight_97a5a5e3 ctermfg=205
+call matchadd('highlight_97a5a5e3', '\<ncols\>')
+highlight highlight_1f88e41c ctermfg=139
+call matchadd('highlight_1f88e41c', '\<nrows\>')
+highlight highlight_6da20a96 ctermfg=141
+call matchadd('highlight_6da20a96', '\<rowidx\>')
+highlight highlight_ae83eebb ctermfg=149
+call matchadd('highlight_ae83eebb', 'curr-line-address-address')
+highlight highlight_bb695e14 ctermfg=36
+call matchadd('highlight_bb695e14', '\<default-scope\>')
+highlight highlight_1e44ab4f ctermfg=208
+call matchadd('highlight_1e44ab4f', '\<first-arg\>')
+highlight highlight_3323f077 ctermfg=208
+call matchadd('highlight_3323f077', '\<first-arg-box\>')
+highlight highlight_74fc42b2 ctermfg=220
+call matchadd('highlight_74fc42b2', 'second-arg')
+highlight highlight_ff6f0571 ctermfg=220
+call matchadd('highlight_ff6f0571', 'second-arg-box')
diff --git a/archive/1.vm.arc/load.arc b/archive/1.vm.arc/load.arc
new file mode 100644
index 00000000..b9037aa4
--- /dev/null
+++ b/archive/1.vm.arc/load.arc
@@ -0,0 +1,28 @@
+; support for dividing arc files into sections of different level, and
+; selectively loading just sections at or less than a given level
+
+; usage:
+;   load.arc [level] [arc files] -- [mu files]
+
+(def selective-load (file (o level 999))
+;?   (prn "loading @file at level @level")
+  (fromfile file
+    (whilet expr (read)
+;?       (prn car.expr)
+      (if (is 'section expr.0)
+        (when (<= expr.1 level)
+          (each x (cut expr 2)
+            (eval x)))
+        (eval expr))
+;?       (prn car.expr " done")
+      )))
+
+(= section-level 999)
+(point break
+(each x (map [fromstring _ (read)] cdr.argv)
+  (if (isa x 'int)
+        (= section-level x)
+      (is '-- x)
+        (break)  ; later args are mu files
+      :else
+        (selective-load string.x section-level))))
diff --git a/archive/1.vm.arc/mu b/archive/1.vm.arc/mu
new file mode 100755
index 00000000..858438b8
--- /dev/null
+++ b/archive/1.vm.arc/mu
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# To run a program:
+#   $ mu [mu files]
+# To run a file of tests (in arc):
+#   $ mu test [arc files]
+# To start an interactive session:
+#   $ mu repl
+#
+# To mess with load levels and selectively run parts of the codebase, skip
+# this script and call load.arc directly.
+
+if [[ $1 == "test" ]]
+then
+  shift
+  ./anarki/arc load.arc "$@"  # test currently assumed to be arc files rather than mu files
+elif [[ $1 == "repl" ]]
+then
+  if [ "$(type rlwrap)" ]
+  then
+    rlwrap -C mu ./anarki/arc mu.arc
+  else
+    ./anarki/arc mu.arc
+  fi
+else
+  ./anarki/arc load.arc mu.arc -- "$@"  # mu files from args
+fi
diff --git a/archive/1.vm.arc/mu.arc b/archive/1.vm.arc/mu.arc
new file mode 100644
index 00000000..2aebd3d5
--- /dev/null
+++ b/archive/1.vm.arc/mu.arc
@@ -0,0 +1,3259 @@
+(ero "initializing mu.. (takes ~5s)")
+;; profiler (http://arclanguage.org/item?id=11556)
+; Keeping this right on top as a reminder to profile before guessing at why my
+; program is slow.
+(mac proc (name params . body)
+  `(def ,name ,params ,@body nil))
+
+(mac filter-log (msg f x)
+  `(ret x@ ,x
+     (prn ,msg (,f x@))))
+
+(= times* (table))
+
+(mac deftimed (name args . body)
+  `(do
+     (def ,(sym (string name "_core")) ,args
+        ,@body)
+     (def ,name ,args
+      (let t0 (msec)
+        (ret ans ,(cons (sym (string name "_core")) args)
+          (update-time ,(string name) t0))))))
+
+(proc update-time(name t0) ; call directly in recursive functions
+  (or= times*.name (list 0 0))
+  (with ((a b)  times*.name
+         timing (- (msec) t0))
+    (= times*.name
+       (list
+         (+ a timing)
+         (+ b 1)))))
+
+(def print-times()
+  (prn (current-process-milliseconds))
+  (prn "gc " (current-gc-milliseconds))
+  (each (name time) (tablist times*)
+    (prn name " " time)))
+
+;; what happens when our virtual machine starts up
+(= initialization-fns* (queue))
+(def reset ()
+  (each f (as cons initialization-fns*)
+    (f)))
+
+(mac on-init body
+  `(enq (fn () ,@body)
+        initialization-fns*))
+
+;; persisting and checking traces for each test
+(= traces* (queue))
+(= trace-dir* ".traces/")
+(ensure-dir trace-dir*)
+(= curr-trace-file* nil)
+(on-init
+  (awhen curr-trace-file*
+    (tofile (+ trace-dir* it)
+      (each (label trace) (as cons traces*)
+        (pr label ": " trace))))
+  (= curr-trace-file* nil)
+  (= traces* (queue)))
+
+(def new-trace (filename)
+  (prn "== @filename")
+;?   )
+  (= curr-trace-file* filename))
+
+(= dump-trace* nil)
+(def trace (label . args)
+  (when (or (is dump-trace* t)
+            (and dump-trace* (is label "-"))
+            (and dump-trace* (pos label dump-trace*!whitelist))
+            (and dump-trace* (no dump-trace*!whitelist) (~pos label dump-trace*!blacklist)))
+    (apply prn label ": " args))
+  (enq (list label (apply tostring:prn args))
+       traces*)
+  (car args))
+
+(on-init
+  (wipe dump-trace*))
+
+(redef tr args  ; why am I still returning to prn when debugging? Will this help?
+  (do1 nil
+       (apply trace "-" args)))
+
+(def tr2 (msg arg)
+  (tr msg arg)
+  arg)
+
+(def check-trace-contents (msg expected-contents)
+  (unless (trace-contents-match expected-contents)
+    (prn "F - " msg)
+    (prn "  trace contents")
+    (print-trace-contents-mismatch expected-contents)))
+
+(def trace-contents-match (expected-contents)
+  (each (label msg) (as cons traces*)
+    (when (and expected-contents
+               (is label expected-contents.0.0)
+               (posmatch expected-contents.0.1 msg))
+      (pop expected-contents)))
+  (no expected-contents))
+
+(def print-trace-contents-mismatch (expected-contents)
+  (each (label msg) (as cons traces*)
+    (whenlet (expected-label expected-msg)  expected-contents.0
+      (if (and (is label expected-label)
+               (posmatch expected-msg msg))
+        (do (pr "  * ")
+            (pop expected-contents))
+        (pr "    "))
+      (pr label ": " msg)))
+  (prn "  couldn't find")
+  (each (expected-label expected-msg)  expected-contents
+    (prn "  ! " expected-label ": " expected-msg)))
+
+(def check-trace-doesnt-contain (msg (label unexpected-contents))
+  (when (some (fn ((l s))
+                (and (is l label)  (posmatch unexpected-contents msg)))
+              (as cons traces*))
+    (prn "F - " msg)
+    (prn "  trace contents")
+    (each (l msg) (as cons traces*)
+      (if (and (is l label)
+               (posmatch unexpected-contents msg))
+        (pr "  X ")
+        (pr "    "))
+      (pr label ": " msg))))
+
+;; virtual machine state
+
+; things that a future assembler will need separate memory for:
+;   code; types; args channel
+;   at compile time: mapping names to locations
+(on-init
+  (= type* (table))  ; name -> type info
+  (= memory* (table))  ; address -> value  (make this a vector?)
+  (= function* (table))  ; name -> [instructions]
+  ; transforming mu programs
+  (= location* (table))  ; function -> {name -> index into default-space}
+  (= next-space-generator* (table))  ; function -> name of function generating next space
+  ; each function's next space will usually always come from a single function
+  (= next-routine-id* 0)
+  (= continuation* (table))
+  )
+
+(on-init
+  (= type* (obj
+              ; Each type must be scalar or array, sum or product or primitive
+              type (obj size 1)  ; implicitly scalar and primitive
+              type-address (obj size 1  address t  elem '(type))
+              type-array (obj array t  elem '(type))
+              type-array-address (obj size 1  address t  elem '(type-array))
+              location (obj size 1  address t  elem '(location))  ; assume it points to an atom
+              integer (obj size 1)
+              boolean (obj size 1)
+              boolean-address (obj size 1  address t  elem '(boolean))
+              byte (obj size 1)
+              byte-address (obj  size 1  address t  elem '(byte))
+              string (obj array t  elem '(byte))  ; inspired by Go
+              ; an address contains the location of a specific type
+              string-address (obj size 1  address t  elem '(string))
+              string-address-address (obj size 1  address t  elem '(string-address))
+              string-address-array (obj array t  elem '(string-address))
+              string-address-array-address (obj size 1  address t  elem '(string-address-array))
+              string-address-array-address-address (obj size 1  address t  elem '(string-address-array-address))
+              ; 'character' will be of larger size when mu supports unicode
+              ; we're currently undisciplined about mixing 'byte' and 'character'
+              ; realistic test of indiscipline in general
+              character (obj size 1)  ; int32 like a Go rune
+              character-address (obj size 1  address t  elem '(character))
+              ; a buffer makes it easy to append to a string/array
+              ; todo: make this generic
+              ; data isn't a 'real' array: its length is stored outside it,
+              ; so for example, 'print-string' won't work on it.
+              buffer (obj size 2  and-record t  elems '((integer) (string-address))  fields '(length data))
+              buffer-address (obj size 1  address t  elem '(buffer))
+              ; a stream makes it easy to read from a string/array
+              stream (obj size 2  and-record t  elems '((integer) (string-address))  fields '(pointer data))
+              stream-address (obj size 1  address t  elem '(stream))
+              ; isolating function calls
+              space (obj array t  elem '(location))  ; by convention index 0 points to outer space
+              space-address (obj size 1  address t  elem '(space))
+              ; arrays consist of an integer length followed by that many
+              ; elements, all of the same type
+              integer-array (obj array t  elem '(integer))
+              integer-array-address (obj size 1  address t  elem '(integer-array))
+              integer-array-address-address (obj size 1  address t  elem '(integer-array-address))
+              integer-address (obj size 1  address t  elem '(integer))  ; pointer to int
+              integer-address-address (obj size 1  address t  elem '(integer-address))
+              ; and-records consist of a multiple fields of different types
+              integer-boolean-pair (obj size 2  and-record t  elems '((integer) (boolean))  fields '(int bool))
+              integer-boolean-pair-address (obj size 1  address t  elem '(integer-boolean-pair))
+              integer-boolean-pair-array (obj array t  elem '(integer-boolean-pair))
+              integer-boolean-pair-array-address (obj size 1  address t  elem '(integer-boolean-pair-array))
+              integer-integer-pair (obj size 2  and-record t  elems '((integer) (integer)))
+              integer-integer-pair-address (obj size 1  address t  elem '(integer-integer-pair))
+              integer-point-pair (obj size 2  and-record t  elems '((integer) (integer-integer-pair)))
+              integer-point-pair-address (obj size 1  address t  elem '(integer-point-pair))
+              integer-point-pair-address-address (obj size 1  address t  elem '(integer-point-pair-address))
+              ; tagged-values are the foundation of dynamic types
+              tagged-value (obj size 2  and-record t  elems '((type) (location))  fields '(type payload))
+              tagged-value-address (obj size 1  address t  elem '(tagged-value))
+              tagged-value-array (obj array t  elem '(tagged-value))
+              tagged-value-array-address (obj size 1  address t  elem '(tagged-value-array))
+              tagged-value-array-address-address (obj size 1  address t  elem '(tagged-value-array-address))
+              ; heterogeneous lists
+              list (obj size 2  and-record t  elems '((tagged-value) (list-address))  fields '(car cdr))
+              list-address (obj size 1  address t  elem '(list))
+              list-address-address (obj size 1  address t  elem '(list-address))
+              ; parallel routines use channels to synchronize
+              channel (obj size 3  and-record t  elems '((integer) (integer) (tagged-value-array-address))  fields '(first-full first-free circular-buffer))
+              ; be careful of accidental copies to channels
+              channel-address (obj size 1  address t  elem '(channel))
+              ; opaque pointer to a call stack
+              ; todo: save properly in allocated memory
+              continuation (obj size 1)
+              ; editor
+              line (obj array t  elem '(character))
+              line-address (obj size 1  address t  elem '(line))
+              line-address-address (obj size 1  address t  elem '(line-address))
+              screen (obj array t  elem '(line-address))
+              screen-address (obj size 1  address t  elem '(screen))
+              ; fake screen
+              terminal (obj size 5  and-record t  elems '((integer) (integer) (integer) (integer) (string-address))  fields '(num-rows num-cols cursor-row cursor-col data))
+              terminal-address (obj size 1  address t  elem '(terminal))
+              ; fake keyboard
+              keyboard (obj size 2  and-record t  elems '((integer) (string-address))  fields '(index data))
+              keyboard-address (obj size 1  address t  elem '(keyboard))
+              )))
+
+;; managing concurrent routines
+
+(on-init
+;?   (prn "-- resetting memory allocation")
+  (= Memory-allocated-until 1000)
+  (= Allocation-chunk 100000))
+
+; routine = runtime state for a serial thread of execution
+(def make-routine (fn-name . args)
+  (let curr-alloc Memory-allocated-until
+;?     (prn "-- allocating routine: @curr-alloc")
+    (++ Memory-allocated-until Allocation-chunk)
+    (annotate 'routine (obj alloc curr-alloc  alloc-max Memory-allocated-until
+        call-stack
+          (list (obj fn-name fn-name  pc 0  args args  caller-arg-idx 0))))
+        ; other fields we use in routine:
+        ;   sleep: conditions
+        ;   limit: number of cycles this routine can use
+        ;   running-since: start of the clock for counting cycles this routine has used
+
+    ; todo: do memory management in mu
+    ))
+
+(defextend empty (x)  (isa x 'routine)
+  (no rep.x!call-stack))
+
+(def stack (routine)
+  ((rep routine) 'call-stack))
+
+(def push-stack (routine op)
+  (push (obj fn-name op  pc 0  caller-arg-idx 0  t0 (msec))
+        rep.routine!call-stack))
+
+(def pop-stack (routine)
+;?   (update-time label.routine (msec)) ;? 1
+  (pop rep.routine!call-stack))
+
+(def top (routine)
+  stack.routine.0)
+
+(def label (routine)
+  (whenlet stack stack.routine
+    (or= stack.0!label
+         (label2 stack))))
+(def label2 (stack)
+         (string:intersperse "/" (map [_ 'fn-name] stack)));))
+
+(def body (routine)
+  (function* stack.routine.0!fn-name))
+
+(mac pc (routine (o idx 0))  ; assignable
+  `((((rep ,routine) 'call-stack) ,idx) 'pc))
+
+(mac caller-arg-idx (routine (o idx 0))  ; assignable
+  `((((rep ,routine) 'call-stack) ,idx) 'caller-arg-idx))
+
+(mac caller-args (routine)  ; assignable
+  `((((rep ,routine) 'call-stack) 0) 'args))
+(mac caller-operands (routine)  ; assignable
+  `((((rep ,routine) 'call-stack) 0) 'caller-operands))
+(mac caller-results (routine)  ; assignable
+  `((((rep ,routine) 'call-stack) 0) 'caller-results))
+
+(mac results (routine)  ; assignable
+  `((((rep ,routine) 'call-stack) 0) 'results))
+(mac reply-args (routine)  ; assignable
+  `((((rep ,routine) 'call-stack) 0) 'reply-args))
+
+(def waiting-for-exact-cycle? (routine)
+  (is 'until rep.routine!sleep.0))
+
+(def ready-to-wake-up (routine)
+  (assert no.routine*)
+  (case rep.routine!sleep.0
+    until
+      (> curr-cycle* rep.routine!sleep.1)
+    until-location-changes
+      (~is rep.routine!sleep.2 (memory* rep.routine!sleep.1))
+    until-routine-done
+      (find [and _ (is rep._!id rep.routine!sleep.1)]
+            completed-routines*)
+    ))
+
+(on-init
+  (= running-routines* (queue))  ; simple round-robin scheduler
+  ; set of sleeping routines; don't modify routines while they're in this table
+  (= sleeping-routines* (table))
+  (= completed-routines* nil)  ; audit trail
+  (= routine* nil)
+  (= abort-routine* (parameter nil))
+  (= curr-cycle* 0)
+  (= scheduling-interval* 500)
+  (= scheduler-switch-table* nil)  ; hook into scheduler for debugging
+  )
+
+; like arc's 'point' but you can also call ((abort-routine*)) in nested calls
+(mac routine-mark body
+  (w/uniq (g p)
+    `(ccc (fn (,g)
+            (parameterize abort-routine* (fn ((o ,p)) (,g ,p))
+              ,@body)))))
+
+(def run fn-names
+  (freeze function*)
+;?   (prn function*!main) ;? 1
+  (load-system-functions)
+  (apply run-more fn-names))
+
+; assume we've already frozen; throw on a few more routines and continue scheduling
+(def run-more fn-names
+  (each it fn-names
+    (enq make-routine.it running-routines*))
+  (while (~empty running-routines*)
+    (= routine* deq.running-routines*)
+    (when rep.routine*!limit
+      ; start the clock if it wasn't already running
+      (or= rep.routine*!running-since curr-cycle*))
+    (trace "schedule" label.routine*)
+    (routine-mark
+      (run-for-time-slice scheduling-interval*))
+    (update-scheduler-state)))
+
+; prepare next iteration of round-robin scheduler
+;
+; state before: routine* running-routines* sleeping-routines*
+; state after: running-routines* (with next routine to run at head) sleeping-routines*
+;
+; responsibilities:
+;   add routine* to either running-routines* or sleeping-routines* or completed-routines*
+;   wake up any necessary sleeping routines (which might be waiting for a
+;     particular time or for a particular memory location to change)
+;   detect termination: all non-helper routines completed
+;   detect deadlock: kill all sleeping routines when none can be woken
+(def update-scheduler-state ()
+  (when routine*
+;?     (prn "update scheduler state: " routine*)
+    (if
+        rep.routine*!sleep
+          (do (trace "schedule" "pushing " label.routine* " to sleep queue")
+              ; keep the clock ticking at rep.routine*!running-since
+              (set sleeping-routines*.routine*))
+        rep.routine*!error
+          (do (trace "schedule" "done with dead routine " label.routine*)
+;?               (tr rep.routine*)
+              (push routine* completed-routines*))
+        empty.routine*
+          (do (trace "schedule" "done with routine " label.routine*)
+              (push routine* completed-routines*))
+        (no rep.routine*!limit)
+          (do (trace "schedule" "scheduling " label.routine* " for further processing")
+              (enq routine* running-routines*))
+        (> rep.routine*!limit 0)
+          (do (trace "schedule" "scheduling " label.routine* " for further processing (limit)")
+              ; stop the clock and debit the time on it from the routine
+              (-- rep.routine*!limit (- curr-cycle* rep.routine*!running-since))
+              (wipe rep.routine*!running-since)
+              (if (<= rep.routine*!limit 0)
+                (do (trace "schedule" "routine ran out of time")
+                    (push routine* completed-routines*))
+                (enq routine* running-routines*)))
+        :else
+          (err "illegal scheduler state"))
+    (= routine* nil))
+  (each (routine _) routine-canon.sleeping-routines*
+    (when (aand rep.routine!limit (<= it (- curr-cycle* rep.routine!running-since)))
+      (trace "schedule" "routine timed out")
+      (wipe sleeping-routines*.routine)
+      (push routine completed-routines*)
+;?       (tr completed-routines*)
+      ))
+  (each (routine _) routine-canon.sleeping-routines*
+    (when (ready-to-wake-up routine)
+      (trace "schedule" "waking up " label.routine)
+      (wipe sleeping-routines*.routine)  ; do this before modifying routine
+      (wipe rep.routine!sleep)
+      (++ pc.routine)
+      (enq routine running-routines*)))
+  ; optimization for simulated time
+  (when (empty running-routines*)
+    (whenlet exact-sleeping-routines (keep waiting-for-exact-cycle? keys.sleeping-routines*)
+      (let next-wakeup-cycle (apply min (map [rep._!sleep 1] exact-sleeping-routines))
+        (= curr-cycle* (+ 1 next-wakeup-cycle)))
+      (trace "schedule" "skipping to cycle " curr-cycle*)
+      (update-scheduler-state)))
+  (when (and (or (~empty running-routines*)
+                 (~empty sleeping-routines*))
+             (all [rep._ 'helper] (as cons running-routines*))
+             (all [rep._ 'helper] keys.sleeping-routines*))
+    (trace "schedule" "just helpers left; stopping everything")
+    (until (empty running-routines*)
+      (push (deq running-routines*) completed-routines*))
+    (each (routine _) sleeping-routines*
+;?       (prn " " label.routine) ;? 0
+      (wipe sleeping-routines*.routine)
+      (push routine completed-routines*)))
+  (detect-deadlock)
+  )
+
+(def detect-deadlock ()
+  (when (and (empty running-routines*)
+             (~empty sleeping-routines*)
+             (~some 'literal (map (fn(_) rep._!sleep.1)
+                                  keys.sleeping-routines*)))
+    (each (routine _) sleeping-routines*
+      (wipe sleeping-routines*.routine)
+      (= rep.routine!error "deadlock detected")
+      (push routine completed-routines*))))
+
+(def die (msg)
+  (tr "die: " msg)
+  (= rep.routine*!error msg)
+  (iflet abort-continuation (abort-routine*)
+    (abort-continuation)))
+
+;; running a single routine
+
+; value of an arg or oarg, stripping away all metadata
+; wish I could have this flag an error when arg is incorrectly formed
+(mac v (operand)  ; for value
+  `((,operand 0) 0))
+
+; routines consist of instrs
+; instrs consist of oargs, op and args
+(def parse-instr (instr)
+  (iflet delim (pos '<- instr)
+    (do (when (atom (instr (+ delim 1)))
+          (err "operator not tokenized in @instr; maybe you need to freeze functions*?"))
+        (list (cut instr 0 delim)  ; oargs
+              (v (instr (+ delim 1)))  ; op
+              (cut instr (+ delim 2))))  ; args
+    (list nil (v car.instr) cdr.instr)))
+
+(def metadata (operand)
+  cdr.operand)
+
+(def ty (operand)
+  (cdr operand.0))
+
+(def literal? (operand)
+  (unless (acons ty.operand)
+    (err "no type in operand @operand"))
+  (in ty.operand.0 'literal 'offset 'fn))
+
+(def typeinfo (operand)
+  (or (type* ty.operand.0)
+      (err "unknown type @(tostring prn.operand)")))
+
+; operand accessors
+(def nondummy (operand)  ; precondition for helpers below
+  (~is '_ operand))
+
+; just for convenience, 'new' instruction sometimes takes a raw string and
+; allocates just enough space to store it
+(def not-raw-string (operand)
+  (~isa operand 'string))
+
+(def address? (operand)
+  (or (is ty.operand.0 'location)
+      typeinfo.operand!address))
+
+($:require "charterm/main.rkt")
+($:require graphics/graphics)
+;? ($:require "terminal-color/terminal-color/main.rkt") ;? 1
+(= Viewport nil)
+; http://rosettacode.org/wiki/Terminal_control/Coloured_text#Racket
+($:define (tput . xs) (system (apply ~a 'tput " " (add-between xs " "))) (void))
+($:define (foreground color) (tput 'setaf color))
+($:define (background color) (tput 'setab color))
+($:define (reset) (tput 'sgr0))
+
+(= new-string-foo* nil)
+(= last-print* 0)
+
+; run instructions from 'routine*' for 'time-slice'
+(def run-for-time-slice (time-slice)
+  (point return
+    (for ninstrs 0 (< ninstrs time-slice) (++ ninstrs)
+      (if (empty body.routine*) (err "@stack.routine*.0!fn-name not defined"))
+      ; falling out of end of function = implicit reply
+      (while (>= pc.routine* (len body.routine*))
+        (pop-stack routine*)
+        (if empty.routine* (return ninstrs))
+        (when (pos '<- (body.routine* pc.routine*))
+          (die "No results returned: @(tostring:pr (body.routine* pc.routine*))"))
+        (++ pc.routine*))
+      (++ curr-cycle*)
+      (when (no ($.current-charterm))
+        (let curr (seconds)
+          (when (~is curr last-print*)
+            (prn curr " " curr-cycle* " " len.running-routines*)
+            (= last-print* curr))))
+;?       (trace "run" "-- " int-canon.memory*) ;? 1
+;?       (trace "run" curr-cycle*)
+      (trace "run" label.routine* " " pc.routine* ": " (body.routine* pc.routine*))
+;?       (trace "run" routine*)
+      (when (atom (body.routine* pc.routine*))  ; label
+;?         (tr "label") ;? 1
+        (when (aand scheduler-switch-table*
+                    (alref it (body.routine* pc.routine*)))
+          (++ pc.routine*)
+          (trace "run" label.routine* " " pc.routine* ": " "context-switch forced " abort-routine*)
+          ((abort-routine*)))
+        (++ pc.routine*)
+        (continue))
+      (let (oarg op arg)  (parse-instr (body.routine* pc.routine*))
+;?         (tr op) ;? 1
+        (let results
+              (case op
+                ; arithmetic
+                add
+                  (+ (m arg.0) (m arg.1))
+                subtract
+                  (- (m arg.0) (m arg.1))
+                multiply
+                  (* (m arg.0) (m arg.1))
+                divide
+                  (/ (real (m arg.0)) (m arg.1))
+                divide-with-remainder
+                  (list (trunc:/ (m arg.0) (m arg.1))
+                        (mod (m arg.0) (m arg.1)))
+
+                ; boolean
+                and
+                  (and (m arg.0) (m arg.1))
+                or
+                  (or (m arg.0) (m arg.1))
+                not
+                  (not (m arg.0))
+
+                ; comparison
+                equal
+;?                   (do (prn (m arg.0) " vs " (m arg.1))
+                  (is (m arg.0) (m arg.1))
+;?                   )
+                not-equal
+                  (~is (m arg.0) (m arg.1))
+                less-than
+                  (< (m arg.0) (m arg.1))
+                greater-than
+                  (> (m arg.0) (m arg.1))
+                lesser-or-equal
+                  (<= (m arg.0) (m arg.1))
+                greater-or-equal
+                  (>= (m arg.0) (m arg.1))
+
+                ; control flow
+                jump
+                  (do (= pc.routine* (+ 1 pc.routine* (v arg.0)))
+                      (continue))
+                jump-if
+                  (when (m arg.0)
+                    (= pc.routine* (+ 1 pc.routine* (v arg.1)))
+                    (continue))
+                jump-unless  ; convenient helper
+                  (unless (m arg.0)
+                    (= pc.routine* (+ 1 pc.routine* (v arg.1)))
+                    (continue))
+
+                ; data management: scalars, arrays, and-records (structs)
+                copy
+                  (m arg.0)
+                get
+                  (with (operand  (canonize arg.0)
+                         idx  (v arg.1))
+                    (assert (iso '(offset) (ty arg.1)) "record index @arg.1 must have type 'offset'")
+                    (assert (< -1 idx (len typeinfo.operand!elems)) "@idx is out of bounds of record @operand")
+                    (m `((,(apply + v.operand
+                                    (map (fn(x) (sizeof `((_ ,@x))))
+                                         (firstn idx typeinfo.operand!elems)))
+                          ,@typeinfo.operand!elems.idx)
+                         (raw))))
+                get-address
+                  (with (operand  (canonize arg.0)
+                         idx  (v arg.1))
+                    (assert (iso '(offset) (ty arg.1)) "record index @arg.1 must have type 'offset'")
+                    (assert (< -1 idx (len typeinfo.operand!elems)) "@idx is out of bounds of record @operand")
+                    (apply + v.operand
+                             (map (fn(x) (sizeof `((_ ,@x))))
+                                  (firstn idx typeinfo.operand!elems))))
+                index
+                  (withs (operand  (canonize arg.0)
+                          elemtype  typeinfo.operand!elem
+                          idx  (m arg.1))
+;?                     (write arg.0)
+;?                     (pr " => ")
+;?                     (write operand)
+;?                     (prn)
+                    (unless (< -1 idx array-len.operand)
+                      (die "@idx is out of bounds of array @operand"))
+                    (m `((,(+ v.operand
+                              1  ; for array size
+                              (* idx (sizeof `((_ ,@elemtype)))))
+                           ,@elemtype)
+                         (raw))))
+                index-address
+                  (withs (operand  (canonize arg.0)
+                          elemtype  typeinfo.operand!elem
+                          idx  (m arg.1))
+                    (unless (< -1 idx array-len.operand)
+                      (die "@idx is out of bounds of array @operand"))
+                    (+ v.operand
+                       1  ; for array size
+                       (* idx (sizeof `((_ ,@elemtype))))))
+                new
+                  (if (isa arg.0 'string)
+                    ; special-case: allocate space for a literal string
+                    (new-string arg.0)
+                    (let type (v arg.0)
+                      (assert (iso '(literal) (ty arg.0)) "new: second arg @arg.0 must be literal")
+                      (if (no type*.type)  (err "no such type @type"))
+                      ; todo: initialize memory. currently racket does it for us
+                      (if type*.type!array
+                        (new-array type (m arg.1))
+                        (new-scalar type))))
+                sizeof
+                  (sizeof `((_ ,(m arg.0))))
+                length
+                  (let base arg.0
+                    (if (or typeinfo.base!array address?.base)
+                      array-len.base
+                      -1))
+
+                ; tagged-values require one primitive
+                save-type
+                  (annotate 'record `(,((ty arg.0) 0) ,(m arg.0)))
+
+                ; code points for characters
+                character-to-integer
+                  ($.char->integer (m arg.0))
+                integer-to-character
+                  ($.integer->char (m arg.0))
+
+                ; multiprocessing
+                fork
+                  ; args: fn globals-table args ...
+                  (let routine  (apply make-routine (m arg.0) (map m (nthcdr 3 arg)))
+                    (= rep.routine!id ++.next-routine-id*)
+                    (= rep.routine!globals (when (len> arg 1) (m arg.1)))
+                    (= rep.routine!limit (when (len> arg 2) (m arg.2)))
+                    (enq routine running-routines*)
+                    rep.routine!id)
+                fork-helper
+                  ; args: fn globals-table args ...
+                  (let routine  (apply make-routine (m arg.0) (map m (nthcdr 3 arg)))
+                    (= rep.routine!id ++.next-routine-id*)
+                    (set rep.routine!helper)
+                    (= rep.routine!globals (when (len> arg 1) (m arg.1)))
+                    (= rep.routine!limit (when (len> arg 2) (m arg.2)))
+                    (enq routine running-routines*)
+                    rep.routine!id)
+                sleep
+                  (do
+                    (case (v arg.0)
+                      for-some-cycles
+                        (let wakeup-time (+ curr-cycle* (v arg.1))
+                          (trace "run" label.routine* " " pc.routine* ": " "sleeping until " wakeup-time)
+                          (= rep.routine*!sleep `(until ,wakeup-time)))
+                      until-location-changes
+                        (= rep.routine*!sleep `(until-location-changes ,(addr arg.1) ,(m arg.1)))
+                      until-routine-done
+                        (= rep.routine*!sleep `(until-routine-done ,(m arg.1)))
+                      ; else
+                        (die "badly formed 'sleep' call @(tostring:prn (body.routine* pc.routine*))")
+                      )
+                    ((abort-routine*)))
+                assert
+                  (unless (m arg.0)
+                    (die (v arg.1)))  ; other routines will be able to look at the error status
+                assert-false
+                  (when (m arg.0)
+                    (die (v arg.1)))
+
+                ; cursor-based (text mode) interaction
+                cursor-mode
+                  ;(do1 nil (system "/bin/stty -F /dev/tty raw"))
+                  (do1 nil (if (no ($.current-charterm)) ($.open-charterm)))
+                retro-mode
+                  ;(do1 nil (system "/bin/stty -F /dev/tty sane"))
+                  (do1 nil (if ($.current-charterm) ($.close-charterm)))
+                clear-host-screen
+                  (do1 nil (pr "\e[m\e[2J\e[;H"))
+                clear-line-on-host
+                  (do1 nil (pr "\e[2K"))
+                cursor-on-host
+                  (do1 nil (pr (+ "\e[" (m arg.0) ";" (m arg.1) "H")))
+                cursor-on-host-to-next-line
+                  (do1 nil (pr "\r\n"))
+                cursor-up-on-host
+                  (do1 nil (pr (+ "\e[" (aif (len> arg 0) (or m arg.0) 1) "A")))
+                cursor-down-on-host
+                  (do1 nil (pr (+ "\e[" (aif (len> arg 0) (or m arg.0) 1) "B")))
+                cursor-right-on-host
+                  (do1 nil (pr (+ "\e[" (aif (len> arg 0) (or m arg.0) 1) "C")))
+                cursor-left-on-host
+                  (do1 nil (pr (+ "\e[" (aif (len> arg 0) (or m arg.0) 1) "D")))
+                print-character-to-host
+                  (do1 nil
+                       (assert (in (type:m arg.0) 'char 'sym) (rep (m arg.0)))
+;?                        (write (m arg.0))  (pr " => ")  (prn (type (m arg.0)))
+                       (if (no ($.current-charterm))
+                         (pr (m arg.0))
+                         (caselet x (m arg.0)
+                           ; todo: test these exceptions
+                           #\newline
+                             (pr "\r\n")
+                           #\backspace
+                             ; backspace doesn't clear after moving the cursor
+                             (pr "\b \b")
+                           ctrl-c
+                             (do ($.close-charterm)
+                                 (die "interrupted"))
+                           ;else
+                             (if (and (len> arg 2)
+                                      (m arg.2))
+                                   (do
+                                     ($.foreground (m arg.1))
+                                     ($.background (m arg.2))
+                                     (pr x)
+                                     ($.reset))
+                                 (and (len> arg 1)
+                                      (m arg.1))
+                                   (do
+                                     ($.foreground (m arg.1))
+                                     (pr x)
+                                     ($.reset))
+                                 :else
+                                   (pr x))))
+                       )
+                read-key-from-host
+                  (if ($.current-charterm)
+                        (and ($.charterm-byte-ready?)
+                             (ret result ($.charterm-read-key)
+                               (case result
+                                 ; charterm exceptions
+                                 return
+                                   (= result #\newline)
+                                 backspace
+                                   (= result #\backspace)
+                                 )))
+                      ($.graphics-open?)
+                        ($.ready-key-press Viewport))
+
+                ; graphics
+                window-on
+                  (do1 nil
+                    ($.open-graphics)
+                    (= Viewport ($.open-viewport (m arg.0)  ; name
+                                                 (m arg.1) (m arg.2))))  ; width height
+                window-off
+                  (do1 nil
+                    ($.close-viewport Viewport)  ; why doesn't this close the window? works in naked racket. not racket vs arc.
+                    ($.close-graphics)
+                    (= Viewport nil))
+                mouse-position
+                  (aif ($.ready-mouse-click Viewport)
+                    (let posn ($.mouse-click-posn it)
+                      (list (annotate 'record (list ($.posn-x posn) ($.posn-y posn))) t))
+                    (list nil nil))
+                wait-for-mouse
+                  (let posn ($.mouse-click-posn ($.get-mouse-click Viewport))
+                    (list (annotate 'record (list ($.posn-x posn) ($.posn-y posn))) t))
+                ; clear-screen in cursor mode above
+                rectangle
+                  (do1 nil
+                    (($.draw-solid-rectangle Viewport)
+                        ($.make-posn (m arg.0) (m arg.1))  ; origin
+                        (m arg.2)  (m arg.3)  ; width height
+                        (m arg.4)))  ; color
+                point
+                  (do1 nil
+                    (($.draw-pixel Viewport) ($.make-posn (m arg.0) (m arg.1))
+                                             (m arg.2)))  ; color
+
+                image
+                  (do1 nil
+                    (($.draw-pixmap Viewport) (m arg.0)  ; filename
+                                              ($.make-posn (m arg.1) (m arg.2))))
+                color-at
+                  (let pixel (($.get-color-pixel Viewport) ($.make-posn (m arg.0) (m arg.1)))
+                    (prn ($.rgb-red pixel) " " ($.rgb-blue pixel) " " ($.rgb-green pixel))
+                    ($:rgb-red pixel))
+
+                ; debugging aides
+                $dump-memory
+                  (do1 nil
+                    (prn:repr int-canon.memory*))
+                $dump-trace
+                  (tofile arg.0
+                    (each (label trace) (as cons traces*)
+                      (pr label ": " trace)))
+                $start-tracing
+                  (do1 nil
+                    (set dump-trace*))
+                $stop-tracing
+                  (do1 nil
+                    (wipe dump-trace*))
+                $dump-routine
+                  (do1 nil
+                    ($.close-charterm)
+                    (prn routine*)
+                    ($.open-charterm)
+                    )
+                $dump-channel
+                  (do1 nil
+                    ($.close-charterm)
+                    (withs (x (m arg.0)
+                            y (memory* (+ x 2)))
+                      (prn label.routine* " -- " x " -- " (list (memory* x)
+                                                                (memory* (+ x 1))
+                                                                (memory* (+ x 2)))
+                                                   " -- " (list (memory* y)
+                                                                (memory* (+ y 1))
+                                                                (repr:memory* (+ y 2))
+                                                                (memory* (+ y 3))
+                                                                (repr:memory* (+ y 4)))))
+                    ($.open-charterm)
+                    )
+                $quit
+                  (quit)
+                $wait-for-key-from-host
+                  (when ($.current-charterm)
+                    (ret result ($.charterm-read-key)
+                      (case result
+                        ; charterm exceptions
+                        return
+                          (= result #\newline)
+                        backspace
+                          (= result #\backspace)
+                        )))
+                $print
+                  (do1 nil
+;?                        (write (m arg.0))  (pr " => ")  (prn (type (m arg.0)))
+                       (if (no ($.current-charterm))
+                         (pr (m arg.0))
+                         (unless disable-debug-prints-in-console-mode*
+                           (caselet x (m arg.0)
+                             #\newline
+                               (pr "\r\n")
+                             #\backspace
+                               ; backspace doesn't clear after moving the cursor
+                               (pr "\b \b")
+                             ctrl-c
+                               (do ($.close-charterm)
+                                   (die "interrupted"))
+                             ;else
+                               (pr x)))
+                          ))
+                $write
+                  (do1 nil
+                    (write (m arg.0)))
+                $eval
+                  (new-string:repr:eval:read:to-arc-string (m arg.0))
+;?                   (let x (to-arc-string (m arg.0)) ;? 1
+;?                     (prn x) ;? 1
+;?                     (new-string:repr:eval x)) ;? 1
+
+                $clear-trace
+                  (do1 nil (wipe interactive-traces*))
+                $save-trace
+                  (let x (filter-log "CCC: " len
+                         (string
+                           (filter-log "BBB: " len
+                           (map [string:intersperse ": " _]
+                                (filter-log "AAA: " len
+                                (as cons (interactive-traces* (m arg.0)))))
+                                )))
+;?                   (let x (string:map [string:intersperse ": " _]
+;?                                      (apply join
+;?                                             (map [as cons _] rev.interactive-traces*)))
+                    (prn "computed trace; now saving to memory\n")
+;?                     (write x)(write #\newline) ;? 1
+;?                     (prn x) ;? 1
+                    (set new-string-foo*)
+                    (do1 (new-string x)
+                      (wipe new-string-foo*)))
+
+                ; first-class continuations
+                current-continuation
+                  (w/uniq continuation-name
+                    (trace "continuation" "saving @(repr rep.routine*!call-stack) to @continuation-name")
+                    (= continuation*.continuation-name (copy rep.routine*!call-stack))
+                    continuation-name)
+                continue-from
+                  (let continuation-name (m arg.0)
+                    (trace "continuation" "restoring @continuation-name")
+                    (trace "continuation" continuation*.continuation-name)
+                    (= rep.routine*!call-stack continuation*.continuation-name)
+                    (trace "continuation" "call stack is now @(repr rep.routine*!call-stack)")
+;?                     (++ pc.routine*) ;? 1
+                    (continue))
+;?                     ((abort-routine*))) ;? 1
+
+                ; user-defined functions
+                next-input
+                  (let idx caller-arg-idx.routine*
+                    (++ caller-arg-idx.routine*)
+                    (trace "arg" repr.arg " " idx " " (repr caller-args.routine*))
+                    (if (len> caller-args.routine* idx)
+                      (list caller-args.routine*.idx t)
+                      (list nil nil)))
+                input
+                  (do (assert (iso '(literal) (ty arg.0)))
+                      (= caller-arg-idx.routine* (v arg.0))
+                      (let idx caller-arg-idx.routine*
+                        (++ caller-arg-idx.routine*)
+                        (trace "arg" repr.arg " " idx " " (repr caller-args.routine*))
+                        (if (len> caller-args.routine* idx)
+                          (list caller-args.routine*.idx t)
+                          (list nil nil))))
+                rewind-inputs
+                  (do1 nil
+                       (= caller-arg-idx.routine* 0))
+                ; type and otype won't always easily compile. be careful.
+                type
+                  (ty (caller-operands.routine* (v arg.0)))
+                otype
+                  (ty (caller-results.routine* (v arg.0)))
+                prepare-reply
+                  (prepare-reply arg)
+                reply
+                  (do (when arg
+                        (prepare-reply arg))
+                      (with (results results.routine*
+                             reply-args reply-args.routine*)
+                        (pop-stack routine*)
+                        (if empty.routine* (return ninstrs))
+                        (let (call-oargs _ call-args)  (parse-instr (body.routine* pc.routine*))
+;?                           (trace "reply" repr.arg " " repr.call-oargs) ;? 1
+                          (each (dest reply-arg val)  (zip call-oargs reply-args results)
+                            (trace "run" label.routine* " " pc.routine* ": " repr.val " => " dest)
+                            (when nondummy.dest
+                              (whenlet argidx (alref metadata.reply-arg 'same-as-arg)
+                                (unless (is v.dest (v call-args.argidx))
+                                  (die "'same-as-arg' output arg in @repr.reply-args can't bind to @repr.call-oargs")))
+                              (setm dest val))))
+                        (++ pc.routine*)
+                        (while (>= pc.routine* (len body.routine*))
+                          (pop-stack routine*)
+                          (when empty.routine* (return ninstrs))
+                          (++ pc.routine*))
+                        (continue)))
+                ; else try to call as a user-defined function
+                  (do (if function*.op
+                        (with (callee-args (accum yield
+                                             (each a arg
+                                               (yield (m a))))
+                               callee-operands (accum yield
+                                                 (each a arg
+                                                   (yield a)))
+                               callee-results (accum yield
+                                                (each a oarg
+                                                  (yield a))))
+                          (push-stack routine* op)
+                          (= caller-args.routine* callee-args)
+                          (= caller-operands.routine* callee-operands)
+                          (= caller-results.routine* callee-results))
+                        (err "no such op @op"))
+                      (continue))
+                )
+              ; opcode generated some 'results'
+              ; copy to output args
+              (if (acons results)
+                (each (dest val) (zip oarg results)
+                  (unless (is dest '_)
+                    (trace "run" label.routine* " " pc.routine* ": " repr.val " => " dest)
+                    (setm dest val)))
+                (when oarg  ; must be a list
+                  (trace "run" label.routine* " " pc.routine* ": " repr.results " => " oarg.0)
+                  (setm oarg.0 results)))
+              )
+        (++ pc.routine*)))
+    (return time-slice)))
+
+(def prepare-reply (args)
+  (= results.routine*
+     (accum yield
+       (each a args
+         (yield (m a)))))
+  (= reply-args.routine* args))
+
+; helpers for memory access respecting
+;   immediate addressing - 'literal' and 'offset'
+;   direct addressing - default
+;   indirect addressing - 'deref'
+;   relative addressing - if routine* has 'default-space'
+
+(def m (loc)  ; read memory, respecting metadata
+  (point return
+    (when (literal? loc)
+      (return v.loc))
+    (when (is v.loc 'default-space)
+      (return rep.routine*!call-stack.0!default-space))
+;?     (trace "mem" loc) ;? 1
+    (assert (isa v.loc 'int) "addresses must be numeric (problem in convert-names?): @repr.loc")
+    (ret result
+      (with (n  sizeof.loc
+             addr  addr.loc)
+;?         (trace "mem" "reading " n " locations starting at " addr) ;? 1
+        (if (is 1 n)
+              memory*.addr
+            :else
+              (annotate 'record
+                        (map memory* (addrs addr n)))))
+      (trace "mem" loc " => " result))))
+
+(def setm (loc val)  ; set memory, respecting metadata
+;?   (tr 111)
+  (point return
+;?   (tr 112)
+    (when (is v.loc 'default-space)
+      (assert (is 1 sizeof.loc) "can't store compounds in default-space @loc")
+      (= rep.routine*!call-stack.0!default-space val)
+      (return))
+;?   (tr 120)
+    (assert (isa v.loc 'int) "can't store to non-numeric address (problem in convert-names?)")
+;?     (trace "mem" loc " <= " repr.val) ;? 1
+    (with (n  (if (isa val 'record) (len rep.val) 1)
+           addr  addr.loc
+           typ  typeof.loc)
+;?       (trace "mem" "size of " loc " is " n) ;? 1
+      (assert n "setm: can't compute type of @loc")
+      (assert addr "setm: null pointer @loc")
+      (if (is 1 n)
+        (do (assert (~isa val 'record) "setm: record of size 1 @(tostring prn.val)")
+            (trace "mem" loc ": " addr " <= " repr.val)
+            (= memory*.addr val))
+        (do (if type*.typ!array
+              ; size check for arrays
+              (when (~is n
+                         (+ 1  ; array length
+                            (* rep.val.0 (sizeof `((_ ,@type*.typ!elem))))))
+                (die "writing invalid array @(tostring prn.val)"))
+              ; size check for non-arrays
+              (when (~is sizeof.loc n)
+                (die "writing to incorrect size @(tostring pr.val) => @loc")))
+            (let addrs (addrs addr n)
+              (each (dest src) (zip addrs rep.val)
+                (trace "mem" loc ": " dest " <= " repr.src)
+                (= memory*.dest src))))))))
+
+(def typeof (operand)
+  (let loc absolutize.operand
+    (while (pos '(deref) metadata.loc)
+      (zap deref loc))
+    ty.loc.0))
+
+(def addr (operand)
+  (v canonize.operand))
+
+(def addrs (n sz)
+  (accum yield
+    (repeat sz
+      (yield n)
+      (++ n))))
+
+(def canonize (operand)
+;?   (tr "0: @operand")
+  (ret operand
+;?     (prn "1: " operand)
+;?     (tr "1: " operand)  ; todo: why does this die?
+    (zap absolutize operand)
+;?     (tr "2: @repr.operand")
+    (while (pos '(deref) metadata.operand)
+      (zap deref operand)
+;?       (tr "3: @repr.operand")
+      )))
+
+(def array-len (operand)
+  (trace "array-len" operand)
+  (zap canonize operand)
+  (if typeinfo.operand!array
+        (m `((,v.operand integer) ,@metadata.operand))
+      :else
+        (err "can't take len of non-array @operand")))
+
+(def sizeof (x)
+;?   (trace "sizeof" x) ;? 1
+  (assert acons.x)
+  (zap canonize x)
+  (point return
+;?   (tr "sizeof: checking @x for array")
+  (when typeinfo.x!array
+;?     (tr "sizeof: @x is an array")
+    (assert (~is '_ v.x) "sizeof: arrays require a specific variable")
+    (return (+ 1 (* array-len.x (sizeof `((_ ,@typeinfo.x!elem)))))))
+;?   (tr "sizeof: not an array")
+  (when typeinfo.x!and-record
+;?     (tr "sizeof: @x is an and-record")
+    (return (sum idfn
+                 (accum yield
+                   (each elem typeinfo.x!elems
+                     (yield (sizeof `((_ ,@elem)))))))))
+;?   (tr "sizeof: @x is a primitive")
+  (return typeinfo.x!size)))
+
+(def absolutize (operand)
+  (if (no routine*)
+        operand
+      (in v.operand '_ 'default-space)
+        operand
+      (pos '(raw) metadata.operand)
+        operand
+      (is 'global space.operand)
+        (aif rep.routine*!globals
+          `((,(+ it 1 v.operand) ,@(cdr operand.0))
+            ,@(rem [caris _ 'space] metadata.operand)
+            (raw))
+          (die "routine has no globals: @operand"))
+      :else
+        (iflet base rep.routine*!call-stack.0!default-space
+          (space-base (rem [caris _ 'space] operand)
+                      base
+                      space.operand)
+          operand)))
+
+(def space-base (operand base space)
+;?   (prn operand " " base) ;? 1
+  (if (is 0 space)
+    ; base case
+    (if (< v.operand memory*.base)
+      `((,(+ base 1 v.operand) ,@(cdr operand.0))
+        ,@metadata.operand
+        (raw))
+      (die "no room for var @operand in routine of size @memory*.base"))
+    ; recursive case
+    (space-base operand (memory* (+ base 1))  ; location 0 points to next space
+                (- space 1))))
+
+(def space (operand)
+  (or (alref metadata.operand 'space)
+      0))
+
+(def deref (operand)
+  (assert (pos '(deref) metadata.operand))
+  (assert address?.operand)
+  (cons `(,(memory* v.operand) ,@typeinfo.operand!elem)
+        (drop-one '(deref) metadata.operand)))
+
+(def drop-one (f x)
+  (when acons.x  ; proper lists only
+    (if (testify.f car.x)
+      cdr.x
+      (cons car.x (drop-one f cdr.x)))))
+
+; memory allocation
+
+(def alloc (sz)
+  (when (> sz (- rep.routine*!alloc-max rep.routine*!alloc))
+    (let curr-alloc Memory-allocated-until
+      (= rep.routine*!alloc curr-alloc)
+      (++ Memory-allocated-until Allocation-chunk)
+      (= rep.routine*!alloc-max Memory-allocated-until)))
+  (ret result rep.routine*!alloc
+    (++ rep.routine*!alloc sz)))
+
+(def new-scalar (type)
+;?   (tr "new scalar: @type")
+  (alloc (sizeof `((_ ,type)))))
+
+(def new-array (type size)
+;?   (tr "new array: @type @size")
+  (ret result (alloc (+ 1 (* (sizeof `((_ ,@type*.type!elem))) size)))
+    (= memory*.result size)))
+
+(def new-string (literal-string)
+;?   (tr "new string: @literal-string")
+  (ret result (alloc (+ 1 len.literal-string))
+    (= memory*.result len.literal-string)
+    (on c literal-string
+      (when (and new-string-foo* (is 0 (mod index 100)))
+        (prn index " " repr.c))
+      (= (memory* (+ result 1 index)) c))))
+
+(def to-arc-string (string-address)
+  (let len (memory* string-address)
+    (string:map memory* (range (+ string-address 1)
+                               (+ string-address len)))))
+
+;; desugar structured assembly based on blocks
+
+(def convert-braces (instrs)
+;?   (prn "convert-braces " instrs)
+  (let locs ()  ; list of information on each brace: (open/close pc)
+    (let pc 0
+      (loop (instrs instrs)
+        (each instr instrs
+;?           (tr instr)
+          (if (or atom.instr (~is 'begin instr.0))  ; label or regular instruction
+                (do
+                  (trace "c{0" pc " " instr " -- " locs)
+                  (++ pc))
+                ; hack: racket replaces braces with parens, so we need the
+                ; keyword 'begin' to delimit blocks.
+                ; ultimately there'll be no nesting and braces will just be
+                ; in an instr by themselves.
+              :else  ; brace
+                (do
+                  (push `(open ,pc) locs)
+                  (recur cdr.instr)
+                  (push `(close ,pc) locs))))))
+    (zap rev locs)
+;?     (tr "-")
+    (with (pc  0
+           stack  ())  ; elems are pcs
+      (accum yield
+        (loop (instrs instrs)
+          (each instr instrs
+;?             (tr "- " instr)
+            (point continue
+            (when (atom instr)  ; label
+              (yield instr)
+              (++ pc)
+              (continue))
+            (when (is car.instr 'begin)
+              (push pc stack)
+              (recur cdr.instr)
+              (pop stack)
+              (continue))
+            (with ((oarg op arg)  (parse-instr instr)
+                   yield-new-instr  (fn (new-instr)
+                                      (trace "c{1" "@pc X " instr " => " new-instr)
+                                      (yield new-instr))
+                   yield-unchanged  (fn ()
+                                      (trace "c{1" "@pc ✓ " instr)
+                                      (yield instr)))
+              (when (in op 'break 'break-if 'break-unless 'loop 'loop-if 'loop-unless)
+                (assert (is oarg nil) "@op: can't take oarg in @instr"))
+              (case op
+                break
+                  (yield-new-instr `(((jump)) ((,(close-offset pc locs (and arg (v arg.0))) offset))))
+                break-if
+                  (yield-new-instr `(((jump-if)) ,arg.0 ((,(close-offset pc locs (and cdr.arg (v arg.1))) offset))))
+                break-unless
+                  (yield-new-instr `(((jump-unless)) ,arg.0 ((,(close-offset pc locs (and cdr.arg (v arg.1))) offset))))
+                loop
+                  (yield-new-instr `(((jump)) ((,(open-offset pc stack (and arg (v arg.0))) offset))))
+                loop-if
+                  (yield-new-instr `(((jump-if)) ,arg.0 ((,(open-offset pc stack (and cdr.arg (v arg.1))) offset))))
+                loop-unless
+                  (yield-new-instr `(((jump-unless)) ,arg.0 ((,(open-offset pc stack (and cdr.arg (v arg.1))) offset))))
+                ;else
+                  (yield-unchanged)))
+            (++ pc))))))))
+
+(def close-offset (pc locs nblocks)
+  (or= nblocks 1)
+;?   (tr nblocks)
+  (point return
+;?   (tr "close " pc " " locs)
+  (let stacksize 0
+    (each (state loc) locs
+      (point continue
+;?       (tr stacksize "/" done " " state " " loc)
+      (when (<= loc pc)
+        (continue))
+;?       (tr "process " stacksize loc)
+      (if (is 'open state) (++ stacksize) (-- stacksize))
+      ; last time
+;?       (tr "process2 " stacksize loc)
+      (when (is stacksize (* -1 nblocks))
+;?         (tr "close now " loc)
+        (return (- loc pc 1))))))))
+
+(def open-offset (pc stack nblocks)
+  (or= nblocks 1)
+  (- (stack (- nblocks 1)) 1 pc))
+
+;; convert jump targets to offsets
+
+(def convert-labels (instrs)
+;?   (tr "convert-labels " instrs)
+  (let labels (table)
+    (let pc 0
+      (each instr instrs
+        (when (~acons instr)
+;?           (tr "label " pc)
+          (= labels.instr pc))
+        (++ pc)))
+    (let pc 0
+      (each instr instrs
+        (when (and acons.instr
+                   (acons car.instr)
+                   (in (v car.instr) 'jump 'jump-if 'jump-unless))
+          (each arg cdr.instr
+;?             (tr "trying " arg " " ty.arg ": " v.arg " => " (labels v.arg))
+            (when (and (is ty.arg.0 'offset)
+                       (isa v.arg 'sym)
+                       (labels v.arg))
+              (= v.arg (- (labels v.arg) pc 1)))))
+        (++ pc))))
+  instrs)
+
+;; convert symbolic names to raw memory locations
+
+(def add-next-space-generator (instrs name)
+;?   (prn "== @name")
+  (each instr instrs
+    (when acons.instr
+      (let (oargs op args)  (parse-instr instr)
+        (each oarg oargs
+          (when (and (nondummy oarg)
+                     (is v.oarg 0)
+                     (iso ty.oarg '(space-address)))
+            (assert (or (no next-space-generator*.name)
+                        (is next-space-generator*.name (alref oarg 'names)))
+                    "function can have only one next-space-generator environment")
+            (tr "next-space-generator of @name is @(alref oarg 'names)")
+            (= next-space-generator*.name (alref oarg 'names))))))))
+
+; just a helper for testing; in practice we unbundle assign-names-to-location
+; and replace-names-with-location.
+(def convert-names (instrs (o name))
+;?   (tr "convert-names " instrs)
+  (= location*.name (assign-names-to-location instrs name))
+;?   (tr "save names for function @name: @(tostring:pr location*.name)") ;? 1
+  (replace-names-with-location instrs name))
+
+(def assign-names-to-location (instrs name (o init-locations))
+  (trace "cn0" "convert-names in @name")
+;?   (prn name ": " location*) ;? 1
+  (point return
+  (ret location (or init-locations (table))
+    ; if default-space in first instruction has a name, begin with its bindings
+    (when (acons instrs.0)  ; not a label
+      (let first-oarg-of-first-instr instrs.0.0  ; hack: assumes the standard default-space boilerplate
+        (when (and (nondummy first-oarg-of-first-instr)
+                   (is 'default-space (v first-oarg-of-first-instr))
+                   (assoc 'names metadata.first-oarg-of-first-instr))
+          (let old-names (location*:alref metadata.first-oarg-of-first-instr 'names)
+            (unless old-names
+;?               (prn "@name requires bindings for @(alref metadata.first-oarg-of-first-instr 'names) which aren't computed yet. Waiting.") ;? 1
+              (return nil))
+            (= location copy.old-names))))) ; assumption: we've already converted names for 'it'
+;?     (unless empty.location (prn location)) ;? 2
+    (with (isa-field  (table)
+           idx  (+ 1  ; 0 always reserved for next space
+                   (or (apply max vals.location)  ; skip past bindings already shared from elsewhere
+                       0))
+           already-location (copy location)
+           )
+      (each instr instrs
+        (point continue
+        (when atom.instr
+          (continue))
+        (trace "cn0" instr " " canon.location " " canon.isa-field)
+        (let (oargs op args)  (parse-instr instr)
+;?           (tr "about to rename args: @op")
+          (when (in op 'get 'get-address)
+            ; special case: map field offset by looking up type table
+            (with (basetype  (typeof args.0)
+                   field  (v args.1))
+;?               (tr 111 " " args.0 " " basetype)
+              (assert type*.basetype!and-record "get on non-record @args.0")
+;?               (tr 112)
+              (trace "cn0" "field-access @field in @args.0 of type @basetype")
+              (when (isa field 'sym)
+                (unless (already-location field)
+                  (assert (or (~location field) isa-field.field) "field @args.1 is also a variable"))
+                (when (~location field)
+                  (trace "cn0" "new field; computing location")
+;?                   (tr "aa " type*.basetype)
+                  (assert type*.basetype!fields "no field names available for @instr")
+;?                   (tr "bb")
+                  (iflet idx (pos field type*.basetype!fields)
+                    (do (set isa-field.field)
+                        (trace "cn0" "field location @idx")
+                        (= location.field idx))
+                    (assert nil "couldn't find field in @instr"))))))
+          ; map args to location indices
+          (each arg args
+            (trace "cn0" "checking arg " arg)
+            (when (and nondummy.arg not-raw-string.arg (~literal? arg))
+              (assert (~isa-field v.arg) "arg @arg is also a field name")
+              (when (maybe-add arg location idx)
+                ; todo: test this
+                (err "use before set: @arg"))))
+;?           (tr "about to rename oargs")
+          ; map oargs to location indices
+          (each arg oargs
+            (trace "cn0" "checking oarg " arg)
+            (when (and nondummy.arg not-raw-string.arg)
+              (assert (~isa-field v.arg) "oarg @arg is also a field name")
+              (when (maybe-add arg location idx)
+                (trace "cn0" "location for oarg " arg ": " idx)
+                ; todo: can't allocate arrays on the stack
+                (++ idx (sizeof `((_ ,@ty.arg))))))))))))))
+
+(def replace-names-with-location (instrs name)
+  (each instr instrs
+    (when (acons instr)
+      (let (oargs op args)  (parse-instr instr)
+        (each arg args
+          (convert-name arg name))
+        (each arg oargs
+          (convert-name arg name)))))
+  (each instr instrs
+    (trace "cn1" instr))
+  instrs)
+
+(= allow-raw-addresses* nil)
+(def check-default-space (instrs name)
+  (unless allow-raw-addresses*
+    (let oarg-names (accum yield
+                      (each (oargs _ _) (map parse-instr (keep acons  ; non-label
+                                                               instrs))
+                        (each oarg oargs
+                          (when nondummy.oarg
+                            (yield v.oarg)))))
+      (when (~pos 'default-space oarg-names)
+        (prn "function @name has no default-space")))))
+
+; assign an index to an arg
+(def maybe-add (arg location idx)
+  (trace "maybe-add" arg)
+  (when (and nondummy.arg
+;?              (prn arg " " (assoc 'space arg))
+             (~assoc 'space arg)
+             (~literal? arg)
+             (~location v.arg)
+             (isa v.arg 'sym)
+             (~in v.arg 'nil 'default-space)
+             (~pos '(raw) metadata.arg))
+    (= (location v.arg) idx)))
+
+; convert the arg to corresponding index
+(def convert-name (arg default-name)
+;?   (prn "111 @arg @default-name")
+  (when (and nondummy.arg not-raw-string.arg
+             (~is ty.arg.0 'literal))  ; can't use 'literal?' because we want to rename offsets
+;?     (prn "112 @arg")
+    (let name (space-to-name arg default-name)
+;?       (prn "113 @arg @name @keys.location* @(tostring:pr location*.name)")
+;?       (when (is arg '((y integer) (space 1)))
+;?         (prn "@arg => @name"))
+      (when (aand location*.name (it v.arg))
+;?         (prn 114)
+        (zap location*.name v.arg))
+;?       (prn 115)
+      )))
+
+(def space-to-name (arg default-name)
+  (ret name default-name
+    (when (~is space.arg 'global)
+      (repeat space.arg
+        (zap next-space-generator* name)))))
+
+(proc check-numeric-address (instrs name)
+  (unless allow-raw-addresses*
+    (on instr instrs
+      (when acons.instr  ; not a label
+        (let (oargs op args)  (parse-instr instr)
+          (each arg oargs
+            (when (and acons.arg  ; not dummy _ or raw string
+                       (isa v.arg 'int)
+                       (~is v.arg 0)
+                       (~pos '(raw) metadata.arg)
+                       (~literal? arg))
+              (prn "using a raw integer address @repr.arg in @name (instruction #@index)")))
+          (each arg args
+            (when (and acons.arg  ; not dummy _ or raw string
+                       (isa v.arg 'int)
+                       (~is v.arg 0)
+                       (~pos '(raw) metadata.arg)
+                       (~literal? arg))
+              (prn "using a raw integer address @repr.arg in @name (instruction #@index)"))))))))
+
+;; literate tangling system for reordering code
+
+(def convert-quotes (instrs)
+  (let deferred (queue)
+    (each instr instrs
+      (when (acons instr)
+        (case instr.0
+          defer
+            (let (q qinstrs)  instr.1
+              (assert (is 'make-br-fn q) "defer: first arg must be [quoted]")
+              (each qinstr qinstrs
+                (enq qinstr deferred))))))
+    (accum yield
+      (each instr instrs
+        (if atom.instr  ; label
+              (yield instr)
+            (is instr.0 'defer)
+              nil  ; skip
+            (is instr.0 'reply)
+              (do
+                (when cdr.instr  ; return values
+                  (= instr.0 'prepare-reply)
+                  (yield instr))
+                (each instr (as cons deferred)
+                  (yield instr))
+                (yield '(reply)))
+            :else
+              (yield instr)))
+      (each instr (as cons deferred)
+        (yield instr)))))
+
+(on-init
+  (= before* (table))  ; label -> queue of fragments
+  (= after* (table)))  ; label -> list of fragments
+
+; see add-code below for adding to before* and after*
+
+(def insert-code (instrs (o name))
+;?   (tr "insert-code " instrs)
+  (loop (instrs instrs)
+    (accum yield
+      (each instr instrs
+        (if (and (acons instr) (~is 'begin car.instr))
+              ; simple instruction
+              (yield instr)
+            (and (acons instr) (is 'begin car.instr))
+              ; block
+              (yield `{begin ,@(recur cdr.instr)})
+            (atom instr)
+              ; label
+              (do
+;?                 (prn "tangling " instr)
+                (each fragment (as cons (or (and name (before* (sym:string name '/ instr)))
+                                            before*.instr))
+                  (each instr fragment
+                    (yield instr)))
+                (yield instr)
+                (each fragment (or (and name (after* (sym:string name '/ instr)))
+                                   after*.instr)
+                  (each instr fragment
+                    (yield instr)))))))))
+
+;; loading code into the virtual machine
+
+(def add-code (forms)
+  (each (op . rest)  forms
+    (case op
+      ; function <name> [ <instructions> ]
+      ; don't apply our lightweight tools just yet
+      function!
+        (let (name (_make-br-fn body))  rest
+          (assert (is 'make-br-fn _make-br-fn))
+          (= name (v tokenize-arg.name))
+          (= function*.name body))
+      function
+        (let (name (_make-br-fn body))  rest
+          (assert (is 'make-br-fn _make-br-fn))
+          (= name (v tokenize-arg.name))
+          (when function*.name
+            (prn "adding new clause to @name"))
+          (= function*.name (join body function*.name)))
+
+      ; and-record <type> [ <name:types> ]
+      and-record
+        (let (name (_make-br-fn fields))  rest
+          (assert (is 'make-br-fn _make-br-fn))
+          (= name (v tokenize-arg.name))
+          (let fields (map tokenize-arg fields)
+            (= type*.name (obj size len.fields
+                               and-record t
+                               ; dump all metadata for now except field name and type
+                               elems (map cdar fields)
+                               fields (map caar fields)))))
+
+      ; primitive <type>
+      primitive
+        (let (name) rest
+          (= name (v tokenize-arg.name))
+          (= type*.name (obj size 1)))
+
+      ; address <type> <elem-type>
+      address
+        (let (name types)  rest
+          (= name (v tokenize-arg.name))
+          (= type*.name (obj size 1
+                             address t
+                             elem types)))
+
+      ; array <type> <elem-type>
+      array
+        (let (name types)  rest
+          (= name (v tokenize-arg.name))
+          (= type*.name (obj array t
+                             elem types)))
+
+      ; before <label> [ <instructions> ]
+      ;
+      ; multiple before directives => code in order
+      before
+        (let (label (_make-br-fn fragment))  rest
+          (assert (is 'make-br-fn _make-br-fn))
+          ; todo: stop using '/' in non-standard manner
+          ;(= label (v tokenize-arg.label))
+          (or= before*.label (queue))
+          (enq fragment before*.label))
+
+      ; after <label> [ <instructions> ]
+      ;
+      ; multiple after directives => code in *reverse* order
+      ; (if initialization order in a function is A B, corresponding
+      ; finalization order should be B A)
+      after
+        (let (label (_make-br-fn fragment))  rest
+          (assert (is 'make-br-fn _make-br-fn))
+          ; todo: stop using '/' in non-standard manner
+          ;(= label (v tokenize-arg.label))
+          (push fragment after*.label))
+
+      ;else
+        (prn "unrecognized top-level " (cons op rest))
+      )))
+
+(def freeze (function-table)
+  (each (name body)  canon.function-table
+;?     (prn "freeze " name)
+    (= function-table.name (convert-labels:convert-braces:tokenize-args:insert-code body name)))
+  (each (name body)  canon.function-table
+    (check-default-space body name))
+  (each (name body)  canon.function-table
+    (check-numeric-address body name))
+  (each (name body)  canon.function-table
+    (add-next-space-generator body name))
+  ; keep converting names until none remain
+  ; (we need to skip unrecognized spaces)
+  (let change t
+    (while change
+      (= change nil)
+      (each (name body)  canon.function-table
+;?         (prn name) ;? 1
+        (when (no location*.name)
+          (= change t))
+        (or= location*.name (assign-names-to-location body name)))))
+;?   (each (name body)  canon.function-table ;? 1
+;?     (or= location*.name (assign-names-to-location body name))) ;? 1
+  (each (name body)  canon.function-table
+    (= function-table.name (replace-names-with-location body name)))
+  ; we could clear location* at this point, but maybe we'll find a use for it
+  )
+
+(def freeze-another (fn-name)
+  (= function*.fn-name (convert-labels:convert-braces:tokenize-args:insert-code function*.fn-name fn-name))
+  (check-default-space function*.fn-name fn-name)
+  (add-next-space-generator function*.fn-name fn-name)
+  (= location*.fn-name (assign-names-to-location function*.fn-name fn-name location*.fn-name))
+  (replace-names-with-location function*.fn-name fn-name))
+
+(def tokenize-arg (arg)
+;?   (tr "tokenize-arg " arg)
+  (if (in arg '<- '_)
+        arg
+      (isa arg 'sym)
+        (map [map [fromstring _ (read)] _]
+             (map [tokens _ #\:]
+                  (tokens string.arg #\/)))
+      :else
+        arg))
+
+(def tokenize-args (instrs)
+;?   (tr "tokenize-args " instrs)
+;?   (prn2 "@(tostring prn.instrs) => "
+  (accum yield
+    (each instr instrs
+      (if atom.instr
+            (yield instr)
+          (is 'begin instr.0)
+            (yield `{begin ,@(tokenize-args cdr.instr)})
+          :else
+            (yield (map tokenize-arg instr))))))
+;?   )
+
+(def prn2 (msg . args)
+  (pr msg)
+  (apply prn args))
+
+(def canon (table)
+  (sort (compare < [tostring (prn:car _)]) (as cons table)))
+
+(def int-canon (table)
+  (sort (compare < car) (as cons table)))
+
+(def routine-canon (routine-table)
+  (sort (compare < label:car) (as cons routine-table)))
+
+(def repr (val)
+  (tostring write.val))
+
+;; test helpers
+
+(def memory-contains (addr value)
+;?   (prn "Looking for @value starting at @addr")
+  (loop (addr addr
+         idx  0)
+;?     (prn "@idx vs @addr")
+    (if (>= idx len.value)
+          t
+        (~is memory*.addr value.idx)
+          (do1 nil
+               (prn "@addr should contain @value.idx but contains @memory*.addr"))
+        :else
+          (recur (+ addr 1) (+ idx 1)))))
+
+(def memory-contains-array (addr value)
+  (and (>= memory*.addr len.value)
+       (loop (addr (+ addr 1)  ; skip count
+              idx  0)
+         (if (>= idx len.value)
+               t
+             (~is memory*.addr value.idx)
+               nil
+             :else
+               (recur (+ addr 1) (+ idx 1))))))
+
+; like memory-contains-array but shows diffs
+(def memory-contains-array-verbose (addr value)
+  (prn "Mismatch when looking at @addr, size @memory*.addr vs @len.value")
+  (and (>= memory*.addr len.value)
+       (loop (addr (+ addr 1)  ; skip count
+              idx  0)
+         (and (< idx len.value) (prn "comparing @idx: @memory*.addr and @value.idx"))
+         (if (>= idx len.value)
+               t
+             (~is memory*.addr value.idx)
+               (do1 nil
+                    (prn "@addr should contain @(repr value.idx) but contains @(repr memory*.addr)")
+                    (recur (+ addr 1) (+ idx 1)))
+             :else
+               (recur (+ addr 1) (+ idx 1))))))
+
+; like memory-contains-array but shows diffs in 2D
+(def screen-contains (addr width value)
+  (or (memory-contains-array addr value)
+      (do1 nil
+          (prn "Mismatch detected. Screen contents:")
+          (with (row-start-addr  (+ addr 1)  ; skip count
+                 idx  0)
+            (for row 0  (< row (/ len.value width))  (do ++.row  (++ row-start-addr width))
+              (pr ". ")
+              (for col 0  (< col width)  ++.col
+                (with (expected  value.idx
+                       got  (memory* (+ col row-start-addr)))
+                  (pr got)
+                  (pr (if (is expected got) " " "X")))
+                ++.idx)
+              (prn " .")
+              )))))
+
+; run code in tests
+(mac run-code (name . body)
+  ; careful to avoid re-processing functions and adding noise to traces
+  `(do
+     (prn "-- " ',name)
+     (trace "===" ',name)
+     (wipe (function* ',name))
+     (add-code '((function ,name [ ,@body ])))
+     (freeze-another ',name)
+;?      (set dump-trace*) ;? 1
+     (run-more ',name)))
+
+; kludge to prevent reloading functions in .mu files for every test
+(def reset2 ()
+  (= memory* (table))
+  (= Memory-allocated-until 1000)
+  (awhen curr-trace-file*
+    (tofile (+ trace-dir* it)
+      (each (label trace) (as cons traces*)
+        (pr label ": " trace))))
+  (= curr-trace-file* nil)
+  (= traces* (queue))
+  (wipe dump-trace*)
+  (wipe function*!main)
+  (wipe location*!main)
+  (= running-routines* (queue))
+  (= sleeping-routines* (table))
+  (wipe completed-routines*)
+  (wipe routine*)
+  (= abort-routine* (parameter nil))
+  (= curr-cycle* 0)
+  (= scheduling-interval* 500)
+  (= scheduler-switch-table* nil)
+  )
+
+(= disable-debug-prints-in-console-mode* nil)
+(def test-only-settings ()
+  (set allow-raw-addresses*)
+  (set disable-debug-prints-in-console-mode*))
+
+(def routine-that-ran (f)
+  (find [some [is f _!fn-name] stack._]
+        completed-routines*))
+
+(def routine-running (f)
+  (or
+    (find [some [is f _!fn-name] stack._]
+          completed-routines*)
+    (find [some [is f _!fn-name] stack._]
+          (as cons running-routines*))
+    (find [some [is f _!fn-name] stack._]
+          (keys sleeping-routines*))
+    (and routine*
+         (some [is f _!fn-name] stack.routine*)
+         routine*)))
+
+(def ran-to-completion (f)
+  ; if a routine calling f ran to completion there'll be no sign of it in any
+  ; completed call-stacks.
+  (~routine-that-ran f))
+
+(def restart (routine)
+  (while (in top.routine!fn-name 'read 'write)
+    (pop-stack routine))
+  (wipe rep.routine!sleep)
+  (wipe rep.routine!error)
+  (enq routine running-routines*))
+
+(def dump (msg routine)
+  (prn "= @msg " rep.routine!sleep)
+  (prn:rem [in car._ 'sleep 'call-stack] (as cons rep.routine))
+  (each frame rep.routine!call-stack
+    (prn " @frame!fn-name")
+    (each (key val) frame
+      (unless (is key 'fn-name)
+        (prn "  " key " " val)))))
+
+;; system software
+; create once, load before every test
+
+(reset)
+(= system-function* (table))
+
+(mac init-fn (name . body)
+  (let real-name (v tokenize-arg.name)
+    `(= (system-function* ',real-name) ',body)))
+
+(def load-system-functions ()
+  (each (name f) system-function*
+    (= (function* name)
+       (system-function* name))))
+
+; allow running mu.arc without load.arc
+(unless bound!section (= section do))
+
+(section 100
+
+(init-fn maybe-coerce
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:tagged-value-address <- new tagged-value:literal)
+  (x:tagged-value-address/deref <- next-input)
+  (p:type <- next-input)
+  (xtype:type <- get x:tagged-value-address/deref type:offset)
+  (match?:boolean <- equal xtype:type p:type)
+  { begin
+    (break-if match?:boolean)
+    (reply 0:literal nil:literal)
+  }
+  (xvalue:location <- get x:tagged-value-address/deref payload:offset)
+  (reply xvalue:location match?:boolean))
+
+(init-fn init-tagged-value
+  (default-space:space-address <- new space:literal 30:literal)
+  ; assert sizeof:arg.0 == 1
+  (xtype:type <- next-input)
+  (xtypesize:integer <- sizeof xtype:type)
+  (xcheck:boolean <- equal xtypesize:integer 1:literal)
+  (assert xcheck:boolean)
+  ; todo: check that arg 0 matches the type? or is that for the future typechecker?
+  (result:tagged-value-address <- new tagged-value:literal)
+  ; result->type = arg 0
+  (resulttype:location <- get-address result:tagged-value-address/deref type:offset)
+  (resulttype:location/deref <- copy xtype:type)
+  ; result->payload = arg 1
+  (locaddr:location <- get-address result:tagged-value-address/deref payload:offset)
+  (locaddr:location/deref <- next-input)
+  (reply result:tagged-value-address))
+
+(init-fn list-next  ; list-address -> list-address
+  (default-space:space-address <- new space:literal 30:literal)
+  (base:list-address <- next-input)
+  (result:list-address <- get base:list-address/deref cdr:offset)
+  (reply result:list-address))
+
+(init-fn list-value-address  ; list-address -> tagged-value-address
+  (default-space:space-address <- new space:literal 30:literal)
+  (base:list-address <- next-input)
+  (result:tagged-value-address <- get-address base:list-address/deref car:offset)
+  (reply result:tagged-value-address))
+
+; create a list out of a list of args
+; only integers for now
+(init-fn init-list
+  (default-space:space-address <- new space:literal 30:literal)
+  ; new-list = curr = new list
+  (result:list-address <- new list:literal)
+  (curr:list-address <- copy result:list-address)
+  { begin
+    ; while read curr-value
+    (curr-value:integer exists?:boolean <- next-input)
+    (break-unless exists?:boolean)
+    ; curr.cdr = new list
+    (next:list-address-address <- get-address curr:list-address/deref cdr:offset)
+    (next:list-address-address/deref <- new list:literal)
+    ; curr = curr.cdr
+    (curr:list-address <- list-next curr:list-address)
+    ; curr.car = type:curr-value
+    (dest:tagged-value-address <- list-value-address curr:list-address)
+    (dest:tagged-value-address/deref <- save-type curr-value:integer)
+    (loop)
+  }
+  ; return new-list.cdr
+  (result:list-address <- list-next result:list-address)  ; memory leak
+  (reply result:list-address))
+
+; create an array out of a list of scalar args
+; only integers for now
+(init-fn init-array
+  (default-space:space-address <- new space:literal 30:literal)
+  (capacity:integer <- copy 0:literal)
+  { begin
+    ; while read curr-value
+    (curr-value:integer exists?:boolean <- next-input)
+    (break-unless exists?:boolean)
+    (capacity:integer <- add capacity:integer 1:literal)
+    (loop)
+  }
+  (result:integer-array-address <- new integer-array:literal capacity:integer)
+  (rewind-inputs)
+;?   (xxx:integer <- next-input) ;? 1
+;?   ($print xxx:integer) ;? 1
+;?   (rewind-inputs) ;? 1
+  (i:integer <- copy 0:literal)
+  { begin
+    ; while read curr-value
+    (done?:boolean <- greater-or-equal i:integer capacity:integer)
+    (break-if done?:boolean)
+    (curr-value:integer exists?:boolean <- next-input)
+    (assert exists?:boolean)
+    (tmp:integer-address <- index-address result:integer-array-address/deref i:integer)
+    (tmp:integer-address/deref <- copy curr-value:integer)
+    (i:integer <- add i:integer 1:literal)
+    (loop)
+  }
+  (reply result:integer-array-address))
+
+(init-fn list-length
+  (default-space:space-address <- new space:literal 30:literal)
+  (curr:list-address <- next-input)
+;?   ; recursive
+;?   { begin
+;?     ; if empty list return 0
+;?     (t1:tagged-value-address <- list-value-address curr:list-address)
+;?     (break-if t1:tagged-value-address)
+;?     (reply 0:literal)
+;?   }
+;?   ; else return 1+length(curr.cdr)
+;? ;?   ($print (("recurse\n" literal)))
+;?   (next:list-address <- list-next curr:list-address)
+;?   (sub:integer <- list-length next:list-address)
+;?   (result:integer <- add sub:integer 1:literal)
+;?   (reply result:integer))
+  ; iterative solution
+  (result:integer <- copy 0:literal)
+  { begin
+    ; while curr
+    (t1:tagged-value-address <- list-value-address curr:list-address)
+    (break-unless t1:tagged-value-address)
+    ; ++result
+    (result:integer <- add result:integer 1:literal)
+;?     ($print result:integer)
+;?     ($print (("\n" literal)))
+    ; curr = curr.cdr
+    (curr:list-address <- list-next curr:list-address)
+    (loop)
+  }
+  (reply result:integer))
+
+(init-fn init-channel
+  (default-space:space-address <- new space:literal 30:literal)
+  ; result = new channel
+  (result:channel-address <- new channel:literal)
+  ; result.first-full = 0
+  (full:integer-address <- get-address result:channel-address/deref first-full:offset)
+  (full:integer-address/deref <- copy 0:literal)
+  ; result.first-free = 0
+  (free:integer-address <- get-address result:channel-address/deref first-free:offset)
+  (free:integer-address/deref <- copy 0:literal)
+  ; result.circular-buffer = new tagged-value[arg+1]
+  (capacity:integer <- next-input)
+  (capacity:integer <- add capacity:integer 1:literal)  ; unused slot for full? below
+  (channel-buffer-address:tagged-value-array-address-address <- get-address result:channel-address/deref circular-buffer:offset)
+  (channel-buffer-address:tagged-value-array-address-address/deref <- new tagged-value-array:literal capacity:integer)
+  (reply result:channel-address))
+
+(init-fn capacity
+  (default-space:space-address <- new space:literal 30:literal)
+  (chan:channel <- next-input)
+  (q:tagged-value-array-address <- get chan:channel circular-buffer:offset)
+  (qlen:integer <- length q:tagged-value-array-address/deref)
+  (reply qlen:integer))
+
+(init-fn write
+  (default-space:space-address <- new space:literal 30:literal)
+  (chan:channel-address <- next-input)
+  (val:tagged-value <- next-input)
+  { begin
+    ; block if chan is full
+    (full:boolean <- full? chan:channel-address/deref)
+    (break-unless full:boolean)
+    (full-address:integer-address <- get-address chan:channel-address/deref first-full:offset)
+    (sleep until-location-changes:literal full-address:integer-address/deref)
+  }
+  ; store val
+  (q:tagged-value-array-address <- get chan:channel-address/deref circular-buffer:offset)
+  (free:integer-address <- get-address chan:channel-address/deref first-free:offset)
+  (dest:tagged-value-address <- index-address q:tagged-value-array-address/deref free:integer-address/deref)
+  (dest:tagged-value-address/deref <- copy val:tagged-value)
+  ; increment free
+  (free:integer-address/deref <- add free:integer-address/deref 1:literal)
+  { begin
+    ; wrap free around to 0 if necessary
+    (qlen:integer <- length q:tagged-value-array-address/deref)
+    (remaining?:boolean <- less-than free:integer-address/deref qlen:integer)
+    (break-if remaining?:boolean)
+    (free:integer-address/deref <- copy 0:literal)
+  }
+  (reply chan:channel-address/deref/same-as-arg:0))
+
+(init-fn read
+  (default-space:space-address <- new space:literal 30:literal)
+  (chan:channel-address <- next-input)
+;?   ($dump-channel chan:channel-address) ;? 2
+  { begin
+    ; block if chan is empty
+    (empty:boolean <- empty? chan:channel-address/deref)
+    (break-unless empty:boolean)
+    (free-address:integer-address <- get-address chan:channel-address/deref first-free:offset)
+    (sleep until-location-changes:literal free-address:integer-address/deref)
+  }
+  ; read result
+  (full:integer-address <- get-address chan:channel-address/deref first-full:offset)
+  (q:tagged-value-array-address <- get chan:channel-address/deref circular-buffer:offset)
+  (result:tagged-value <- index q:tagged-value-array-address/deref full:integer-address/deref)
+  ; increment full
+  (full:integer-address/deref <- add full:integer-address/deref 1:literal)
+  { begin
+    ; wrap full around to 0 if necessary
+    (qlen:integer <- length q:tagged-value-array-address/deref)
+    (remaining?:boolean <- less-than full:integer-address/deref qlen:integer)
+    (break-if remaining?:boolean)
+    (full:integer-address/deref <- copy 0:literal)
+  }
+  (reply result:tagged-value chan:channel-address/deref/same-as-arg:0))
+
+; An empty channel has first-empty and first-full both at the same value.
+(init-fn empty?
+  (default-space:space-address <- new space:literal 30:literal)
+  ; return arg.first-full == arg.first-free
+  (chan:channel <- next-input)
+  (full:integer <- get chan:channel first-full:offset)
+  (free:integer <- get chan:channel first-free:offset)
+  (result:boolean <- equal full:integer free:integer)
+  (reply result:boolean))
+
+; A full channel has first-empty just before first-full, wasting one slot.
+; (Other alternatives: https://en.wikipedia.org/wiki/Circular_buffer#Full_.2F_Empty_Buffer_Distinction)
+(init-fn full?
+  (default-space:space-address <- new space:literal 30:literal)
+  (chan:channel <- next-input)
+  ; curr = chan.first-free + 1
+  (curr:integer <- get chan:channel first-free:offset)
+  (curr:integer <- add curr:integer 1:literal)
+  { begin
+    ; if (curr == chan.capacity) curr = 0
+    (qlen:integer <- capacity chan:channel)
+    (remaining?:boolean <- less-than curr:integer qlen:integer)
+    (break-if remaining?:boolean)
+    (curr:integer <- copy 0:literal)
+  }
+  ; return chan.first-full == curr
+  (full:integer <- get chan:channel first-full:offset)
+  (result:boolean <- equal full:integer curr:integer)
+  (reply result:boolean))
+
+(init-fn string-equal
+  (default-space:space-address <- new space:literal 30:literal)
+  (a:string-address <- next-input)
+  (a-len:integer <- length a:string-address/deref)
+  (b:string-address <- next-input)
+  (b-len:integer <- length b:string-address/deref)
+  ; compare lengths
+  { begin
+    (length-equal?:boolean <- equal a-len:integer b-len:integer)
+    (break-if length-equal?:boolean)
+    (reply nil:literal)
+  }
+  ; compare each corresponding byte
+  (i:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- greater-or-equal i:integer a-len:integer)
+    (break-if done?:boolean)
+    (a2:byte <- index a:string-address/deref i:integer)
+    (b2:byte <- index b:string-address/deref i:integer)
+    { begin
+      (chars-match?:boolean <- equal a2:byte b2:byte)
+      (break-if chars-match?:boolean)
+      (reply nil:literal)
+    }
+    (i:integer <- add i:integer 1:literal)
+    (loop)
+  }
+  (reply t:literal)
+)
+
+(init-fn strcat
+  (default-space:space-address <- new space:literal 30:literal)
+  ; result = new string[a.length + b.length]
+  (a:string-address <- next-input)
+  (a-len:integer <- length a:string-address/deref)
+  (b:string-address <- next-input)
+  (b-len:integer <- length b:string-address/deref)
+  (result-len:integer <- add a-len:integer b-len:integer)
+  (result:string-address <- new string:literal result-len:integer)
+  ; copy a into result
+  (result-idx:integer <- copy 0:literal)
+  (i:integer <- copy 0:literal)
+  { begin
+    ; while (i < a.length)
+    (a-done?:boolean <- greater-or-equal i:integer a-len:integer)
+    (break-if a-done?:boolean)
+    ; result[result-idx] = a[i]
+    (out:byte-address <- index-address result:string-address/deref result-idx:integer)
+    (in:byte <- index a:string-address/deref i:integer)
+    (out:byte-address/deref <- copy in:byte)
+    ; ++i
+    (i:integer <- add i:integer 1:literal)
+    ; ++result-idx
+    (result-idx:integer <- add result-idx:integer 1:literal)
+    (loop)
+  }
+  ; copy b into result
+  (i:integer <- copy 0:literal)
+  { begin
+    ; while (i < b.length)
+    (b-done?:boolean <- greater-or-equal i:integer b-len:integer)
+    (break-if b-done?:boolean)
+    ; result[result-idx] = a[i]
+    (out:byte-address <- index-address result:string-address/deref result-idx:integer)
+    (in:byte <- index b:string-address/deref i:integer)
+    (out:byte-address/deref <- copy in:byte)
+    ; ++i
+    (i:integer <- add i:integer 1:literal)
+    ; ++result-idx
+    (result-idx:integer <- add result-idx:integer 1:literal)
+    (loop)
+  }
+  (reply result:string-address))
+
+; replace underscores in first with remaining args
+(init-fn interpolate  ; string-address template, string-address a..
+  (default-space:space-address <- new space:literal 60:literal)
+  (template:string-address <- next-input)
+  ; compute result-len, space to allocate for result
+  (tem-len:integer <- length template:string-address/deref)
+  (result-len:integer <- copy tem-len:integer)
+  { begin
+    ; while arg received
+    (a:string-address arg-received?:boolean <- next-input)
+    (break-unless arg-received?:boolean)
+;?     ($print ("arg now: " literal))
+;?     ($print a:string-address)
+;?     ($print "@":literal)
+;?     ($print a:string-address/deref)  ; todo: test (m on scoped array)
+;?     ($print "\n":literal)
+;? ;?     (assert nil:literal)
+    ; result-len = result-len + arg.length - 1 (for the 'underscore' being replaced)
+    (a-len:integer <- length a:string-address/deref)
+    (result-len:integer <- add result-len:integer a-len:integer)
+    (result-len:integer <- subtract result-len:integer 1:literal)
+;?     ($print ("result-len now: " literal))
+;?     ($print result-len:integer)
+;?     ($print "\n":literal)
+    (loop)
+  }
+  ; rewind to start of non-template args
+  (_ <- input 0:literal)
+  ; result = new string[result-len]
+  (result:string-address <- new string:literal result-len:integer)
+  ; repeatedly copy sections of template and 'holes' into result
+  (result-idx:integer <- copy 0:literal)
+  (i:integer <- copy 0:literal)
+  { begin
+    ; while arg received
+    (a:string-address arg-received?:boolean <- next-input)
+    (break-unless arg-received?:boolean)
+    ; copy template into result until '_'
+    { begin
+      ; while (i < template.length)
+      (tem-done?:boolean <- greater-or-equal i:integer tem-len:integer)
+      (break-if tem-done?:boolean 2:blocks)
+      ; while template[i] != '_'
+      (in:byte <- index template:string-address/deref i:integer)
+      (underscore?:boolean <- equal in:byte ((#\_ literal)))
+      (break-if underscore?:boolean)
+      ; result[result-idx] = template[i]
+      (out:byte-address <- index-address result:string-address/deref result-idx:integer)
+      (out:byte-address/deref <- copy in:byte)
+      ; ++i
+      (i:integer <- add i:integer 1:literal)
+      ; ++result-idx
+      (result-idx:integer <- add result-idx:integer 1:literal)
+      (loop)
+    }
+;?     ($print ("i now: " literal))
+;?     ($print i:integer)
+;?     ($print "\n":literal)
+    ; copy 'a' into result
+    (j:integer <- copy 0:literal)
+    { begin
+      ; while (j < a.length)
+      (arg-done?:boolean <- greater-or-equal j:integer a-len:integer)
+      (break-if arg-done?:boolean)
+      ; result[result-idx] = a[j]
+      (in:byte <- index a:string-address/deref j:integer)
+;?       ($print ("copying: " literal))
+;?       ($print in:byte)
+;?       ($print (" at: " literal))
+;?       ($print result-idx:integer)
+;?       ($print "\n":literal)
+      (out:byte-address <- index-address result:string-address/deref result-idx:integer)
+      (out:byte-address/deref <- copy in:byte)
+      ; ++j
+      (j:integer <- add j:integer 1:literal)
+      ; ++result-idx
+      (result-idx:integer <- add result-idx:integer 1:literal)
+      (loop)
+    }
+    ; skip '_' in template
+    (i:integer <- add i:integer 1:literal)
+;?     ($print ("i now: " literal))
+;?     ($print i:integer)
+;?     ($print "\n":literal)
+    (loop)  ; interpolate next arg
+  }
+  ; done with holes; copy rest of template directly into result
+  { begin
+    ; while (i < template.length)
+    (tem-done?:boolean <- greater-or-equal i:integer tem-len:integer)
+    (break-if tem-done?:boolean)
+    ; result[result-idx] = template[i]
+    (in:byte <- index template:string-address/deref i:integer)
+;?     ($print ("copying: " literal))
+;?     ($print in:byte)
+;?     ($print (" at: " literal))
+;?     ($print result-idx:integer)
+;?     ($print "\n":literal)
+    (out:byte-address <- index-address result:string-address/deref result-idx:integer)
+    (out:byte-address/deref <- copy in:byte)
+    ; ++i
+    (i:integer <- add i:integer 1:literal)
+    ; ++result-idx
+    (result-idx:integer <- add result-idx:integer 1:literal)
+    (loop)
+  }
+  (reply result:string-address))
+
+(init-fn find-next  ; string, character, index -> next index
+  (default-space:space-address <- new space:literal 30:literal)
+  (text:string-address <- next-input)
+  (pattern:character <- next-input)
+  (idx:integer <- next-input)
+  (len:integer <- length text:string-address/deref)
+  { begin
+    (eof?:boolean <- greater-or-equal idx:integer len:integer)
+    (break-if eof?:boolean)
+    (curr:byte <- index text:string-address/deref idx:integer)
+    (found?:boolean <- equal curr:byte pattern:character)
+    (break-if found?:boolean)
+    (idx:integer <- add idx:integer 1:literal)
+    (loop)
+  }
+  (reply idx:integer))
+
+(init-fn find-substring/variant:find-next
+  (default-space:space-address <- new space:literal 30:literal)
+  ; fairly dumb algorithm; used for parsing code and traces
+  (text:string-address <- next-input)
+  (pattern:string-address <- next-input)
+  (idx:integer <- next-input)
+  (first:character <- index pattern:string-address/deref 0:literal)
+  ; repeatedly check for match at current idx
+  (len:integer <- length text:string-address/deref)
+  { begin
+    ; does some unnecessary work checking for substrings even when there isn't enough of text left
+    (eof?:boolean <- greater-or-equal idx:integer len:integer)
+    (break-if eof?:boolean)
+    (found?:boolean <- match-at text:string-address pattern:string-address idx:integer)
+    (break-if found?:boolean)
+    (idx:integer <- add idx:integer 1:literal)
+    ; optimization: skip past indices that definitely won't match
+    (idx:integer <- find-next text:string-address first:character idx:integer)
+    (loop)
+  }
+  (reply idx:integer)
+)
+
+(init-fn match-at
+  (default-space:space-address <- new space:literal 30:literal)
+  ; fairly dumb algorithm; used for parsing code and traces
+  (text:string-address <- next-input)
+  (pattern:string-address <- next-input)
+  (idx:integer <- next-input)
+  (pattern-len:integer <- length pattern:string-address/deref)
+  ; check that there's space left for the pattern
+  { begin
+    (x:integer <- length text:string-address/deref)
+    (x:integer <- subtract x:integer pattern-len:integer)
+    (enough-room?:boolean <- lesser-or-equal idx:integer x:integer)
+    (break-if enough-room?:boolean)
+    (reply nil:literal)
+  }
+  ; check each character of pattern
+  (pattern-idx:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- greater-or-equal pattern-idx:integer pattern-len:integer)
+    (break-if done?:boolean)
+    (c:character <- index text:string-address/deref idx:integer)
+    (exp:character <- index pattern:string-address/deref pattern-idx:integer)
+    { begin
+      (match?:boolean <- equal c:character exp:character)
+      (break-if match?:boolean)
+      (reply nil:literal)
+    }
+    (idx:integer <- add idx:integer 1:literal)
+    (pattern-idx:integer <- add pattern-idx:integer 1:literal)
+    (loop)
+  }
+  (reply t:literal)
+)
+
+(init-fn split  ; string, character -> string-address-array-address
+  (default-space:space-address <- new space:literal 30:literal)
+  (s:string-address <- next-input)
+  (delim:character <- next-input)
+  ; empty string? return empty array
+  (len:integer <- length s:string-address/deref)
+  { begin
+    (empty?:boolean <- equal len:integer 0:literal)
+    (break-unless empty?:boolean)
+    (result:string-address-array-address <- new string-address-array:literal 0:literal)
+    (reply result:string-address-array-address)
+  }
+  ; count #pieces we need room for
+  (count:integer <- copy 1:literal)  ; n delimiters = n+1 pieces
+  (idx:integer <- copy 0:literal)
+  { begin
+    (idx:integer <- find-next s:string-address delim:character idx:integer)
+    (done?:boolean <- greater-or-equal idx:integer len:integer)
+    (break-if done?:boolean)
+    (idx:integer <- add idx:integer 1:literal)
+    (count:integer <- add count:integer 1:literal)
+    (loop)
+  }
+  ; allocate space
+;?   ($print (("alloc: " literal)))
+;?   ($print count:integer)
+;?   ($print (("\n" literal)))
+  (result:string-address-array-address <- new string-address-array:literal count:integer)
+  ; repeatedly copy slices (start..end) until delimiter into result[curr-result]
+  (curr-result:integer <- copy 0:literal)
+  (start:integer <- copy 0:literal)
+  { begin
+    ; while next delim exists
+    (done?:boolean <- greater-or-equal start:integer len:integer)
+    (break-if done?:boolean)
+    (end:integer <- find-next s:string-address delim:character start:integer)
+;?     ($print start:integer) ;? 1
+;?     ($print ((" " literal))) ;? 1
+;?     ($print end:integer) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    ; copy start..end into result[curr-result]
+    (dest:string-address-address <- index-address result:string-address-array-address/deref curr-result:integer)
+    (dest:string-address-address/deref <- string-copy s:string-address start:integer end:integer)
+    ; slide over to next slice
+    (start:integer <- add end:integer 1:literal)
+    (curr-result:integer <- add curr-result:integer 1:literal)
+    (loop)
+  }
+  (reply result:string-address-array-address)
+)
+
+(init-fn split-first-at-substring/variant:split-first  ; string text, string delim -> string first, string rest
+  (default-space:space-address <- new space:literal 30:literal)
+  (text:string-address <- next-input)
+  (delim:string-address <- next-input)
+  ; empty string? return empty strings
+  (len:integer <- length text:string-address/deref)
+  { begin
+    (empty?:boolean <- equal len:integer 0:literal)
+    (break-unless empty?:boolean)
+    (x:string-address <- new "")
+    (y:string-address <- new "")
+    (reply x:string-address y:string-address)
+  }
+  (idx:integer <- find-substring text:string-address delim:string-address 0:literal)
+  (x:string-address <- string-copy text:string-address 0:literal idx:integer)
+  (k:integer <- length delim:string-address/deref)
+  (idx:integer <- add idx:integer k:integer)
+  (y:string-address <- string-copy text:string-address idx:integer len:integer)
+  (reply x:string-address y:string-address)
+)
+
+(init-fn split-first  ; string text, character delim -> string first, string rest
+  (default-space:space-address <- new space:literal 30:literal)
+  (text:string-address <- next-input)
+  (delim:character <- next-input)
+  ; empty string? return empty strings
+  (len:integer <- length text:string-address/deref)
+  { begin
+    (empty?:boolean <- equal len:integer 0:literal)
+    (break-unless empty?:boolean)
+    (x:string-address <- new "")
+    (y:string-address <- new "")
+    (reply x:string-address y:string-address)
+  }
+  (idx:integer <- find-next text:string-address delim:character 0:literal)
+  (x:string-address <- string-copy text:string-address 0:literal idx:integer)
+  (idx:integer <- add idx:integer 1:literal)
+  (y:string-address <- string-copy text:string-address idx:integer len:integer)
+  (reply x:string-address y:string-address)
+)
+
+; todo: make this generic
+(init-fn string-copy  ; buf start end -> address of new array
+  (default-space:space-address <- new space:literal 30:literal)
+  (buf:string-address <- next-input)
+  (start:integer <- next-input)
+  (end:integer <- next-input)
+;?   ($print (("  copy: " literal))) ;? 1
+;?   ($print start:integer) ;? 1
+;?   ($print (("-" literal))) ;? 1
+;?   ($print end:integer) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  ; if end is out of bounds, trim it
+  (len:integer <- length buf:string-address/deref)
+  (end:integer <- min len:integer end:integer)
+  ; allocate space for result
+  (len:integer <- subtract end:integer start:integer)
+  (result:string-address <- new string:literal len:integer)
+  ; copy start..end into result[curr-result]
+  (src-idx:integer <- copy start:integer)
+  (dest-idx:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- greater-or-equal src-idx:integer end:integer)
+    (break-if done?:boolean)
+    (src:character <- index buf:string-address/deref src-idx:integer)
+;?     ($print (("  copying " literal))) ;? 1
+;?     ($print src:character) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    (dest:character-address <- index-address result:string-address/deref dest-idx:integer)
+    (dest:character-address/deref <- copy src:character)
+    (src-idx:integer <- add src-idx:integer 1:literal)
+    (dest-idx:integer <- add dest-idx:integer 1:literal)
+    (loop)
+  }
+  (reply result:string-address)
+)
+
+(init-fn min
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:integer <- next-input)
+  (y:integer <- next-input)
+  { begin
+    (return-x?:boolean <- less-than x:integer y:integer)
+    (break-if return-x?:boolean)
+    (reply y:integer)
+  }
+  (reply x:integer)
+)
+
+(init-fn max
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:integer <- next-input)
+  (y:integer <- next-input)
+  { begin
+    (return-x?:boolean <- greater-than x:integer y:integer)
+    (break-if return-x?:boolean)
+    (reply y:integer)
+  }
+  (reply x:integer)
+)
+
+(init-fn init-stream
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:string-address <- next-input)
+  (result:stream-address <- new stream:literal)
+  (x:integer-address <- get-address result:stream-address/deref pointer:offset)
+  (x:integer-address/deref <- copy 0:literal)
+  (y:string-address-address <- get-address result:stream-address/deref data:offset)
+  (y:string-address-address/deref <- copy in:string-address)
+  (reply result:stream-address)
+)
+
+(init-fn rewind-stream
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:stream-address <- next-input)
+  (x:integer-address <- get-address in:stream-address/deref pointer:offset)
+  (x:integer-address/deref <- copy 0:literal)
+  (reply in:stream-address/same-as-arg:0)
+)
+
+(init-fn read-line
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:stream-address <- next-input)
+  (idx:integer-address <- get-address in:stream-address/deref pointer:offset)
+  (s:string-address <- get in:stream-address/deref data:offset)
+;?   ($print (("idx before: " literal))) ;? 1
+;?   ($print idx:integer-address/deref) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (next-idx:integer <- find-next s:string-address ((#\newline literal)) idx:integer-address/deref)
+;?   ($print (("next-idx: " literal))) ;? 1
+;?   ($print next-idx:integer) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (result:string-address <- string-copy s:string-address idx:integer-address/deref next-idx:integer)
+  (idx:integer-address/deref <- add next-idx:integer 1:literal)  ; skip newline
+;?   ($print (("idx now: " literal))) ;? 1
+;?   ($print idx:integer-address/deref) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (reply result:string-address)
+)
+
+(init-fn read-character
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:stream-address <- next-input)
+  (idx:integer-address <- get-address in:stream-address/deref pointer:offset)
+  (s:string-address <- get in:stream-address/deref data:offset)
+  (c:character <- index s:string-address/deref idx:integer-address/deref)
+  (idx:integer-address/deref <- add idx:integer-address/deref 1:literal)
+  (reply c:character)
+)
+
+(init-fn end-of-stream?
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:stream-address <- next-input)
+  (idx:integer <- get in:stream-address/deref pointer:offset)
+  (s:string-address <- get in:stream-address/deref data:offset)
+  (len:integer <- length s:string-address/deref)
+;?   ($print (("eos: " literal))) ;? 1
+;?   ($print len:integer) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+;?   ($print (("idx: " literal))) ;? 1
+;?   ($print idx:integer) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (result:boolean <- greater-or-equal idx:integer len:integer)
+  (reply result:boolean)
+)
+
+(init-fn init-keyboard
+  (default-space:space-address <- new space:literal 30:literal)
+  (result:keyboard-address <- new keyboard:literal)
+  (buf:string-address-address <- get-address result:keyboard-address/deref data:offset)
+  (buf:string-address-address/deref <- next-input)
+  (idx:integer-address <- get-address result:keyboard-address/deref index:offset)
+  (idx:integer-address/deref <- copy 0:literal)
+  (reply result:keyboard-address)
+)
+
+(init-fn read-key
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:keyboard-address <- next-input)
+  (screen:terminal-address <- next-input)
+  { begin
+    (break-unless x:keyboard-address)
+    (idx:integer-address <- get-address x:keyboard-address/deref index:offset)
+    (buf:string-address <- get x:keyboard-address/deref data:offset)
+    (max:integer <- length buf:string-address/deref)
+    { begin
+      (done?:boolean <- greater-or-equal idx:integer-address/deref max:integer)
+      (break-unless done?:boolean)
+      (reply ((#\null literal)))
+    }
+    (c:character <- index buf:string-address/deref idx:integer-address/deref)
+    (idx:integer-address/deref <- add idx:integer-address/deref 1:literal)
+    (reply c:character)
+  }
+  ; real keyboard input is infrequent; avoid polling it too much
+  (sleep for-some-cycles:literal 1:literal)
+  (c:character <- read-key-from-host)
+  ; when we read from a real keyboard we print to screen as well
+  { begin
+    (break-unless c:character)
+    (silent?:boolean <- equal screen:terminal-address ((silent literal)))
+    (break-if silent?:boolean)
+;?     ($print (("aaaa\n" literal))) ;? 1
+    (print-character-to-host c:character)
+  }
+  (reply c:character)
+)
+
+(init-fn wait-for-key
+  (default-space:space-address <- new space:literal 30:literal)
+  (k:keyboard-address <- next-input)
+  (screen:terminal-address <- next-input)
+  { begin
+    (result:character <- read-key k:keyboard-address screen:terminal-address)
+    (loop-unless result:character)
+  }
+  (reply result:character)
+)
+
+(init-fn send-keys-to-stdin
+  (default-space:space-address <- new space:literal 30:literal)
+  (k:keyboard-address <- next-input)
+  (stdin:channel-address <- next-input)
+;?   (c:character <- copy ((#\a literal))) ;? 1
+;?   (curr:tagged-value <- save-type c:character) ;? 1
+;?   (stdin:channel-address/deref <- write stdin:channel-address curr:tagged-value) ;? 1
+;?   (c:character <- copy ((#\newline literal))) ;? 1
+;?   (curr:tagged-value <- save-type c:character) ;? 1
+;?   (stdin:channel-address/deref <- write stdin:channel-address curr:tagged-value) ;? 1
+  { begin
+    (c:character <- read-key k:keyboard-address)
+    (loop-unless c:character)
+    (curr:tagged-value <- save-type c:character)
+    (stdin:channel-address/deref <- write stdin:channel-address curr:tagged-value)
+    (eof?:boolean <- equal c:character ((#\null literal)))
+    (break-if eof?:boolean)
+    (loop)
+  }
+)
+
+; collect characters until newline before sending out
+(init-fn buffer-lines
+  (default-space:space-address <- new space:literal 30:literal)
+  (stdin:channel-address <- next-input)
+  (buffered-stdin:channel-address <- next-input)
+  ; repeat forever
+  { begin
+    (line:buffer-address <- init-buffer 30:literal)
+;?     ($dump-channel 1093:literal) ;? 1
+    ; read characters from stdin until newline, copy into line
+    { begin
+      (x:tagged-value stdin:channel-address/deref <- read stdin:channel-address)
+      (c:character <- maybe-coerce x:tagged-value character:literal)
+      (assert c:character)
+;?       ($print line:buffer-address) ;? 2
+;?       ($print (("\n" literal))) ;? 2
+;?       ($print c:character) ;? 2
+;?       ($print (("\n" literal))) ;? 2
+      ; handle backspace
+      { begin
+        (backspace?:boolean <- equal c:character ((#\backspace literal)))
+        (break-unless backspace?:boolean)
+        (len:integer-address <- get-address line:buffer-address/deref length:offset)
+        ; but only if we need to
+        { begin
+;?           ($print (("backspace: " literal))) ;? 1
+;?           ($print len:integer-address/deref) ;? 1
+;?           ($print (("\n" literal))) ;? 1
+          (zero?:boolean <- lesser-or-equal len:integer-address/deref 0:literal)
+          (break-if zero?:boolean)
+          (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
+        }
+        (loop 2:blocks)
+      }
+      (line:buffer-address <- append line:buffer-address c:character)
+      (line-done?:boolean <- equal c:character ((#\newline literal)))
+      (break-if line-done?:boolean)
+      (eof?:boolean <- equal c:character ((#\null literal)))
+      (break-if eof?:boolean 2:blocks)
+      (loop)
+    }
+    ; copy line into buffered-stdout
+    (i:integer <- copy 0:literal)
+    (line-contents:string-address <- get line:buffer-address/deref data:offset)
+    (max:integer <- get line:buffer-address/deref length:offset)
+;?     ($print (("len: " literal))) ;? 1
+;?     ($print max:integer) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    { begin
+      (done?:boolean <- greater-or-equal i:integer max:integer)
+      (break-if done?:boolean)
+      (c:character <- index line-contents:string-address/deref i:integer)
+      (curr:tagged-value <- save-type c:character)
+;?       ($dump-channel 1093:literal) ;? 1
+;?       ($start-tracing) ;? 1
+;?       ($print (("bufferout: " literal))) ;? 2
+;?       ($print c:character) ;? 1
+;?       (x:integer <- character-to-integer c:character) ;? 1
+;?       ($print x:integer) ;? 1
+;?       ($print (("\n" literal))) ;? 2
+      (buffered-stdin:channel-address/deref <- write buffered-stdin:channel-address curr:tagged-value)
+;?       ($stop-tracing) ;? 1
+;?       ($dump-channel 1093:literal) ;? 1
+;?       ($quit) ;? 1
+      (i:integer <- add i:integer 1:literal)
+      (loop)
+    }
+    (loop)
+  }
+)
+
+(init-fn clear-screen
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  { begin
+    (break-unless x:terminal-address)
+;?     ($print (("AAA" literal)))
+    (buf:string-address <- get x:terminal-address/deref data:offset)
+    (max:integer <- length buf:string-address/deref)
+    (i:integer <- copy 0:literal)
+    { begin
+      (done?:boolean <- greater-or-equal i:integer max:integer)
+      (break-if done?:boolean)
+      (x:byte-address <- index-address buf:string-address/deref i:integer)
+      (x:byte-address/deref <- copy ((#\space literal)))
+      (i:integer <- add i:integer 1:literal)
+      (loop)
+    }
+    (reply)
+  }
+  (clear-host-screen)
+)
+
+(init-fn cursor
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  (newrow:integer <- next-input)
+  (newcol:integer <- next-input)
+  { begin
+    (break-unless x:terminal-address)
+    (row:integer-address <- get-address x:terminal-address/deref cursor-row:offset)
+    (row:integer-address/deref <- copy newrow:integer)
+    (col:integer-address <- get-address x:terminal-address/deref cursor-col:offset)
+    (col:integer-address/deref <- copy newcol:integer)
+    (reply)
+  }
+  (cursor-on-host row:integer col:integer)
+)
+
+(init-fn cursor-to-next-line
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  { begin
+    (break-unless x:terminal-address)
+    (row:integer-address <- get-address x:terminal-address/deref cursor-row:offset)
+;?     ($print row:integer-address/deref)
+;?     ($print (("\n" literal)))
+    (row:integer-address/deref <- add row:integer-address/deref 1:literal)
+    (col:integer-address <- get-address x:terminal-address/deref cursor-col:offset)
+;?     ($print col:integer-address/deref)
+;?     ($print (("\n" literal)))
+    (col:integer-address/deref <- copy 0:literal)
+    (reply)
+  }
+  (cursor-on-host-to-next-line)
+)
+
+(init-fn cursor-down
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+;?   ($print ((#\# literal))) ;? 1
+  (height:integer-address <- get-address x:terminal-address/deref num-rows:offset)
+;?   ($print height:integer-address/deref) ;? 1
+  { begin
+    (break-unless x:terminal-address)
+;?     ($print ((#\% literal))) ;? 1
+    (row:integer-address <- get-address x:terminal-address/deref cursor-row:offset)
+;?     ($print (("cursor down: " literal))) ;? 1
+;?     ($print row:integer-address/deref) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    { begin
+      (bottom?:boolean <- greater-or-equal row:integer-address/deref height:integer-address/deref)
+      (break-if bottom?:boolean)
+      (row:integer-address/deref <- add row:integer-address/deref 1:literal)
+;?       ($print ((#\* literal))) ;? 1
+;?       ($print row:integer-address/deref) ;? 1
+    }
+    (reply)
+  }
+  (cursor-down-on-host)
+)
+
+(init-fn cursor-up
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  { begin
+    (break-unless x:terminal-address)
+    (row:integer-address <- get-address x:terminal-address/deref cursor-row:offset)
+;?     ($print (("cursor up: " literal))) ;? 1
+;?     ($print row:integer-address/deref) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    { begin
+      (top?:boolean <- lesser-or-equal row:integer-address/deref 0:literal)
+      (break-if top?:boolean)
+      (row:integer-address/deref <- subtract row:integer-address/deref 1:literal)
+    }
+    (reply)
+  }
+  (cursor-up-on-host)
+)
+
+(init-fn cursor-left
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  { begin
+    (break-unless x:terminal-address)
+    (col:integer-address <- get-address x:terminal-address/deref cursor-col:offset)
+    { begin
+      (edge?:boolean <- lesser-or-equal col:integer-address/deref 0:literal)
+      (break-if edge?:boolean)
+      (col:integer-address/deref <- subtract col:integer-address/deref 1:literal)
+    }
+    (reply)
+  }
+  (cursor-left-on-host)
+)
+
+(init-fn cursor-right
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  (width:integer-address <- get-address x:terminal-address/deref num-cols:offset)
+  { begin
+    (break-unless x:terminal-address)
+    (col:integer-address <- get-address x:terminal-address/deref cursor-col:offset)
+    { begin
+      (edge?:boolean <- lesser-or-equal col:integer-address/deref width:integer-address/deref)
+      (break-if edge?:boolean)
+      (col:integer-address/deref <- add col:integer-address/deref 1:literal)
+    }
+    (reply)
+  }
+  (cursor-right-on-host)
+)
+
+(init-fn replace-character
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  (c:character <- next-input)
+  (print-character x:terminal-address c:character)
+  (cursor-left x:terminal-address)
+)
+
+(init-fn clear-line
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  { begin
+    (break-unless x:terminal-address)
+    (n:integer <- get x:terminal-address/deref num-cols:offset)
+    (col:integer-address <- get-address x:terminal-address/deref cursor-col:offset)
+    (orig-col:integer <- copy col:integer-address/deref)
+    ; space over the entire line
+    { begin
+      (done?:boolean <- greater-or-equal col:integer-address/deref n:integer)
+      (break-if done?:boolean)
+      (print-character x:terminal-address ((#\space literal)))  ; implicitly updates 'col'
+      (loop)
+    }
+    ; now back to where the cursor was
+    (col:integer-address/deref <- copy orig-col:integer)
+    (reply)
+  }
+  (clear-line-on-host)
+)
+
+(init-fn print-character
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  (c:character <- next-input)
+  (fg:integer/color <- next-input)
+  (bg:integer/color <- next-input)
+;?   ($print (("printing character to screen " literal)))
+;?   ($print c:character)
+;?   (reply)
+;?   ($print (("\n" literal)))
+  { begin
+    (break-unless x:terminal-address)
+    (row:integer-address <- get-address x:terminal-address/deref cursor-row:offset)
+;?     ($print row:integer-address/deref) ;? 2
+;?     ($print ((", " literal))) ;? 1
+    (col:integer-address <- get-address x:terminal-address/deref cursor-col:offset)
+;?     ($print col:integer-address/deref) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    (width:integer <- get x:terminal-address/deref num-cols:offset)
+    (t1:integer <- multiply row:integer-address/deref width:integer)
+    (idx:integer <- add t1:integer col:integer-address/deref)
+    (buf:string-address <- get x:terminal-address/deref data:offset)
+    (cursor:byte-address <- index-address buf:string-address/deref idx:integer)
+    (cursor:byte-address/deref <- copy c:character)  ; todo: newline, etc.
+    (col:integer-address/deref <- add col:integer-address/deref 1:literal)
+    ; we don't rely on any auto-wrap functionality
+    ; maybe die if we go out of screen bounds?
+    (reply)
+  }
+  (print-character-to-host c:character fg:integer/color bg:integer/color)
+)
+
+(init-fn print-string
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  (s:string-address <- next-input)
+  (len:integer <- length s:string-address/deref)
+;?   ($print (("print/string: len: " literal)))
+;?   ($print len:integer)
+;?   ($print (("\n" literal)))
+  (i:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- greater-or-equal i:integer len:integer)
+    (break-if done?:boolean)
+    (c:character <- index s:string-address/deref i:integer)
+    (print-character x:terminal-address c:character)
+    (i:integer <- add i:integer 1:literal)
+    (loop)
+  }
+)
+
+(init-fn print-integer
+  (default-space:space-address <- new space:literal 30:literal)
+  (x:terminal-address <- next-input)
+  (n:integer <- next-input)
+  ; todo: other bases besides decimal
+;?   ($print (("AAA " literal)))
+;?   ($print n:integer)
+  (s:string-address <- integer-to-decimal-string n:integer)
+;?   ($print s:string-address)
+  (print-string x:terminal-address s:string-address)
+)
+
+(init-fn init-buffer
+  (default-space:space-address <- new space:literal 30:literal)
+  (result:buffer-address <- new buffer:literal)
+  (len:integer-address <- get-address result:buffer-address/deref length:offset)
+  (len:integer-address/deref <- copy 0:literal)
+  (s:string-address-address <- get-address result:buffer-address/deref data:offset)
+  (capacity:integer <- next-input)
+  (s:string-address-address/deref <- new string:literal capacity:integer)
+  (reply result:buffer-address)
+)
+
+(init-fn grow-buffer
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  ; double buffer size
+  (x:string-address-address <- get-address in:buffer-address/deref data:offset)
+  (oldlen:integer <- length x:string-address-address/deref/deref)
+;?   ($print oldlen:integer) ;? 1
+  (newlen:integer <- multiply oldlen:integer 2:literal)
+;?   ($print newlen:integer) ;? 1
+  (olddata:string-address <- copy x:string-address-address/deref)
+  (x:string-address-address/deref <- new string:literal newlen:integer)
+  ; copy old contents
+  (i:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- greater-or-equal i:integer oldlen:integer)
+    (break-if done?:boolean)
+    (src:byte <- index olddata:string-address/deref i:integer)
+    (dest:byte-address <- index-address x:string-address-address/deref/deref i:integer)
+    (dest:byte-address/deref <- copy src:byte)
+    (i:integer <- add i:integer 1:literal)
+    (loop)
+  }
+  (reply in:buffer-address)
+)
+
+(init-fn buffer-full?
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (len:integer <- get in:buffer-address/deref length:offset)
+  (s:string-address <- get in:buffer-address/deref data:offset)
+  (capacity:integer <- length s:string-address/deref)
+  (result:boolean <- greater-or-equal len:integer capacity:integer)
+  (reply result:boolean)
+)
+
+(init-fn buffer-index
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (idx:integer <- next-input)
+  { begin
+    (len:integer <- get in:buffer-address/deref length:offset)
+    (not-too-high?:boolean <- less-than idx:integer len:integer)
+    (not-too-low?:boolean <- greater-or-equal idx:integer 0:literal)
+    (in-bounds?:boolean <- and not-too-low?:boolean not-too-high?:boolean)
+    (break-if in-bounds?:boolean)
+    (assert nil:literal (("buffer-index out of bounds" literal)))
+  }
+  (s:string-address <- get in:buffer-address/deref data:offset)
+  (result:character <- index s:string-address/deref idx:integer)
+  (reply result:character)
+)
+
+(init-fn to-array  ; from buffer
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (len:integer <- get in:buffer-address/deref length:offset)
+  (s:string-address <- get in:buffer-address/deref data:offset)
+  { begin
+    ; test: ctrl-d -> s is nil -> to-array returns nil -> read-expression returns t -> exit repl
+    (break-if s:string-address)
+    (reply nil:literal)
+  }
+  ; we can't just return s because it is usually the wrong length
+  (result:string-address <- new string:literal len:integer)
+  (i:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- greater-or-equal i:integer len:integer)
+    (break-if done?:boolean)
+    (src:byte <- index s:string-address/deref i:integer)
+;?     (foo:integer <- character-to-integer src:byte) ;? 1
+;?     ($print (("a: " literal))) ;? 1
+;?     ($print foo:integer) ;? 1
+;?     ($print ((#\newline literal))) ;? 1
+    (dest:byte-address <- index-address result:string-address/deref i:integer)
+    (dest:byte-address/deref <- copy src:byte)
+    (i:integer <- add i:integer 1:literal)
+    (loop)
+  }
+  (reply result:string-address)
+)
+
+(init-fn append
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (c:character <- next-input)
+;?   ($print c:character) ;? 1
+  { begin
+    ; grow buffer if necessary
+    (full?:boolean <- buffer-full? in:buffer-address)
+;?     ($print (("aa\n" literal))) ;? 1
+    (break-unless full?:boolean)
+;?     ($print (("bb\n" literal))) ;? 1
+    (in:buffer-address <- grow-buffer in:buffer-address)
+;?     ($print (("cc\n" literal))) ;? 1
+  }
+  (len:integer-address <- get-address in:buffer-address/deref length:offset)
+  (s:string-address <- get in:buffer-address/deref data:offset)
+  (dest:byte-address <- index-address s:string-address/deref len:integer-address/deref)
+  (dest:byte-address/deref <- copy c:character)
+  (len:integer-address/deref <- add len:integer-address/deref 1:literal)
+  (reply in:buffer-address/same-as-arg:0)
+)
+
+(init-fn last
+  (default-space:space-address <- new space:literal 30:literal)
+  (in:buffer-address <- next-input)
+  (n:integer <- get in:buffer-address/deref length:offset)
+  { begin
+    ; if empty return nil
+    (empty?:boolean <- equal n:integer 0:literal)
+    (break-unless empty?:boolean)
+    (reply nil:literal)
+  }
+  (n:integer <- subtract n:integer 1:literal)
+  (s:string-address <- get in:buffer-address/deref data:offset)
+  (result:character <- index s:string-address/deref n:integer)
+  (reply result:character)
+)
+
+(init-fn integer-to-decimal-string
+  (default-space:space-address <- new space:literal 30:literal)
+  (n:integer <- next-input)
+  ; is it zero?
+  { begin
+    (zero?:boolean <- equal n:integer 0:literal)
+    (break-unless zero?:boolean)
+    (s:string-address <- new "0")
+    (reply s:string-address)
+  }
+  ; save sign
+  (negate-result:boolean <- copy nil:literal)
+  { begin
+    (negative?:boolean <- less-than n:integer 0:literal)
+    (break-unless negative?:boolean)
+;?     ($print (("is negative " literal)))
+    (negate-result:boolean <- copy t:literal)
+    (n:integer <- multiply n:integer -1:literal)
+  }
+  ; add digits from right to left into intermediate buffer
+  (tmp:buffer-address <- init-buffer 30:literal)
+  (zero:character <- copy ((#\0 literal)))
+  (digit-base:integer <- character-to-integer zero:character)
+  { begin
+    (done?:boolean <- equal n:integer 0:literal)
+    (break-if done?:boolean)
+    (n:integer digit:integer <- divide-with-remainder n:integer 10:literal)
+    (digit-codepoint:integer <- add digit-base:integer digit:integer)
+    (c:character <- integer-to-character digit-codepoint:integer)
+    (tmp:buffer-address <- append tmp:buffer-address c:character)
+    (loop)
+  }
+  ; add sign
+  { begin
+    (break-unless negate-result:boolean)
+    (tmp:buffer-address <- append tmp:buffer-address ((#\- literal)))
+  }
+  ; reverse buffer into string result
+  (len:integer <- get tmp:buffer-address/deref length:offset)
+  (buf:string-address <- get tmp:buffer-address/deref data:offset)
+  (result:string-address <- new string:literal len:integer)
+  (i:integer <- subtract len:integer 1:literal)
+  (j:integer <- copy 0:literal)
+  { begin
+    ; while (i >= 0)
+    (done?:boolean <- less-than i:integer 0:literal)
+    (break-if done?:boolean)
+    ; result[j] = tmp[i]
+    (src:byte <- index buf:string-address/deref i:integer)
+    (dest:byte-address <- index-address result:string-address/deref j:integer)
+    (dest:byte-address/deref <- copy src:byte)
+    ; ++i
+    (i:integer <- subtract i:integer 1:literal)
+    ; --j
+    (j:integer <- add j:integer 1:literal)
+    (loop)
+  }
+  (reply result:string-address)
+)
+
+(init-fn send-prints-to-stdout
+  (default-space:space-address <- new space:literal 30:literal)
+  (screen:terminal-address <- next-input)
+  (stdout:channel-address <- next-input)
+;?   (i:integer <- copy 0:literal) ;? 1
+  { begin
+    (x:tagged-value stdout:channel-address/deref <- read stdout:channel-address)
+    (c:character <- maybe-coerce x:tagged-value character:literal)
+    (done?:boolean <- equal c:character ((#\null literal)))
+    (break-if done?:boolean)
+;?     ($print (("printing " literal))) ;? 1
+;?     ($print i:integer) ;? 1
+;?     ($print ((" -- " literal))) ;? 1
+;?     (x:integer <- character-to-integer c:character) ;? 1
+;?     ($print x:integer) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+;?     (i:integer <- add i:integer 1:literal) ;? 1
+    (print-character screen:terminal-address c:character)
+    (loop)
+  }
+)
+
+; remember to call this before you clear the screen or at any other milestone
+; in an interactive program
+(init-fn flush-stdout
+  (default-space:boolean <- copy nil:literal)  ; silence warning, but die if locals used
+  (sleep for-some-cycles:literal 1:literal)
+)
+
+(init-fn init-fake-terminal
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (result:terminal-address <- new terminal:literal)
+  (width:integer-address <- get-address result:terminal-address/deref num-cols:offset)
+  (width:integer-address/deref <- next-input)
+  (height:integer-address <- get-address result:terminal-address/deref num-rows:offset)
+  (height:integer-address/deref <- next-input)
+  (row:integer-address <- get-address result:terminal-address/deref cursor-row:offset)
+  (row:integer-address/deref <- copy 0:literal)
+  (col:integer-address <- get-address result:terminal-address/deref cursor-col:offset)
+  (col:integer-address/deref <- copy 0:literal)
+  (bufsize:integer <- multiply width:integer-address/deref height:integer-address/deref)
+  (buf:string-address-address <- get-address result:terminal-address/deref data:offset)
+  (buf:string-address-address/deref <- new string:literal bufsize:integer)
+  (clear-screen result:terminal-address)
+  (reply result:terminal-address)
+)
+
+(init-fn divides?
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:integer <- next-input)
+  (y:integer <- next-input)
+  (_ remainder:integer <- divide-with-remainder x:integer y:integer)
+  (result:boolean <- equal remainder:integer 0:literal)
+  (reply result:boolean)
+)
+
+; after all system software is loaded:
+;? (= dump-trace* (obj whitelist '("cn0" "cn1")))
+(freeze system-function*)
+)  ; section 100 for system software
+
+;; initialization
+
+(reset)
+(awhen (pos "--" argv)
+  ; batch mode: load all provided files and start at 'main'
+  (map add-code:readfile (cut argv (+ it 1)))
+;?   (set dump-trace*)
+  (run 'main)
+  (if ($.current-charterm) ($.close-charterm))
+  (when ($.graphics-open?) ($.close-viewport Viewport) ($.close-graphics))
+;?   (pr "\nmemory: ")
+;?   (write int-canon.memory*)
+  (prn)
+  (each routine completed-routines*
+    (awhen rep.routine!error
+      (prn "error - " it)
+;?       (prn routine)
+      ))
+)
+
+; repl
+(def run-interactive (stmt)
+  ; careful to avoid re-processing functions and adding noise to traces
+  (= function*!interactive (convert-labels:convert-braces:tokenize-args (list stmt)))
+  (add-next-space-generator function*!interactive 'interactive)
+  (= location*!interactive (assign-names-to-location function*!interactive 'interactive location*!interactive))
+  (replace-names-with-location function*!interactive 'interactive)
+  (= traces* (queue))  ; skip preprocessing
+  (run-more 'interactive))
+
+(when (no cdr.argv)
+  (add-code:readfile "trace.mu")
+  (wipe function*!main)
+  (add-code:readfile "factorial.mu")
+;?   (add-code:readfile "chessboard.mu")  ; takes too long
+  (wipe function*!main)
+  (freeze function*)
+  (load-system-functions)
+  (wipe interactive-commands*)
+  (wipe interactive-traces*)
+  (= interactive-cmdidx* 0)
+  (= traces* (queue))
+;?   (set dump-trace*) ;? 2
+  ; interactive mode
+  (point break
+  (while t
+    (pr interactive-cmdidx*)(pr "> ")
+    (let expr (read)
+      (unless expr (break))
+      (push expr interactive-commands*)
+      (run-interactive expr))
+    (push traces* interactive-traces*)
+    (++ interactive-cmdidx*)
+    )))
+
+(if ($.current-charterm) ($.close-charterm))
+(reset)
+;? (print-times)
diff --git a/archive/1.vm.arc/mu.arc.t b/archive/1.vm.arc/mu.arc.t
new file mode 100644
index 00000000..6c0464f9
--- /dev/null
+++ b/archive/1.vm.arc/mu.arc.t
@@ -0,0 +1,5208 @@
+; Mu: An exploration on making the global structure of programs more accessible.
+;
+;   "Is it a language, or an operating system, or a virtual machine? Mu."
+;   (with apologies to Robert Pirsig: http://en.wikipedia.org/wiki/Mu_%28negative%29#In_popular_culture)
+;
+;; Motivation
+;
+; I want to live in a world where I can have an itch to tweak a program, clone
+; its open-source repository, orient myself on how it's organized, and make
+; the simple change I envisioned, all in an afternoon. This codebase tries to
+; make this possible for its readers. (More details: http://akkartik.name/about)
+;
+; What helps comprehend the global structure of programs? For starters, let's
+; enumerate what doesn't: idiomatic code, adherence to a style guide or naming
+; convention, consistent indentation, API documentation for each class, etc.
+; These conventional considerations improve matters in the small, but don't
+; help understand global organization. They help existing programmers manage
+; day-to-day operations, but they can't turn outsider programmers into
+; insiders. (Elaboration: http://akkartik.name/post/readable-bad)
+;
+; In my experience, two things have improved matters so far: version control
+; and automated tests. Version control lets me rewind back to earlier, simpler
+; times when the codebase was simpler, when its core skeleton was easier to
+; ascertain. Indeed, arguably what came first is by definition the skeleton of
+; a program, modulo major rewrites. Once you understand the skeleton, it
+; becomes tractable to 'play back' later major features one by one. (Previous
+; project that fleshed out this idea: http://akkartik.name/post/wart-layers)
+;
+; The second and biggest boost to comprehension comes from tests. Tests are
+; good for writers for well-understood reasons: they avoid regressions, and
+; they can influence code to be more decoupled and easier to change. In
+; addition, tests are also good for the outsider reader because they permit
+; active reading. If you can't build a program and run its tests it can't help
+; you understand it. It hangs limp at best, and might even be actively
+; misleading. If you can run its tests, however, it comes alive. You can step
+; through scenarios in a debugger. You can add logging and scan logs to make
+; sense of them. You can run what-if scenarios: "why is this line not written
+; like this?" Make a change, rerun tests: "Oh, that's why." (Elaboration:
+; http://akkartik.name/post/literate-programming)
+;
+; However, tests are only useful to the extent that they exist. Think back to
+; your most recent codebase. Do you feel comfortable releasing a new version
+; just because the tests pass? I'm not aware of any such project. There's just
+; too many situations envisaged by the authors that were never encoded in a
+; test. Even disciplined authors can't test for performance or race conditions
+; or fault tolerance. If a line is phrased just so because of some subtle
+; performance consideration, it's hard to communicate to newcomers.
+;
+; This isn't an arcane problem, and it isn't just a matter of altruism. As
+; more and more such implicit considerations proliferate, and as the original
+; authors are replaced by latecomers for day-to-day operations, knowledge is
+; actively forgotten and lost. The once-pristine codebase turns into legacy
+; code that is hard to modify without expensive and stress-inducing
+; regressions.
+;
+; How to write tests for performance, fault tolerance, race conditions, etc.?
+; How can we state and verify that a codepath doesn't ever perform memory
+; allocation, or write to disk? It requires better, more observable primitives
+; than we currently have. Modern operating systems have their roots in the
+; 70s. Their interfaces were not designed to be testable. They provide no way
+; to simulate a full disk, or a specific sequence of writes from different
+; threads. We need something better.
+;
+; This project tries to move, groping, towards that 'something better', a
+; platform that is both thoroughly tested and allows programs written for it
+; to be thoroughly tested. It tries to answer the question:
+;
+;   If Denis Ritchie and Ken Thompson were to set out today to co-design unix
+;   and C, knowing what we know about automated tests, what would they do
+;   differently?
+;
+; To try to impose *some* constraints on this gigantic yak-shave, we'll try to
+; keep both language and OS as simple as possible, focused entirely on
+; permitting more kinds of tests, on first *collecting* all the information
+; about implicit considerations in some form so that readers and tools can
+; have at least some hope of making sense of it.
+;
+; The initial language will be just assembly. We'll try to make it convenient
+; to program in with some simple localized rewrite rules inspired by lisp
+; macros and literate programming. Programmers will have to do their own
+; memory management and register allocation, but we'll provide libraries to
+; help with them.
+;
+; The initial OS will provide just memory management and concurrency
+; primitives. No users or permissions (we don't live on mainframes anymore),
+; no kernel- vs user-mode, no virtual memory or process abstraction, all
+; threads sharing a single address space (use VMs for security and
+; sandboxing). The only use case we care about is getting a test harness to
+; run some code, feed it data through blocking channels, stop it and observe
+; its internals. The code under test is expected to cooperate in such testing,
+; by logging important events for the test harness to observe. (More info:
+; http://akkartik.name/post/tracing-tests)
+;
+; The common thread here is elimination of abstractions, and it's not an
+; accident. Abstractions help insiders manage the evolution of a codebase, but
+; they actively hinder outsiders in understanding it from scratch. This
+; matters, because the funnel to turn outsiders into insiders is critical to
+; the long-term life of a codebase. Perhaps authors should raise their
+; estimation of the costs of abstraction, and go against their instincts for
+; introducing it. That's what I'll be trying to do: question every abstraction
+; before I introduce it. We'll see how it goes.
+
+; ---
+
+;; Getting started
+;
+; Mu is currently built atop Racket and Arc, but this is temporary and
+; contingent. We want to keep our options open, whether to port to a different
+; host language, and easy to rewrite to native code for any platform. So we'll
+; try to avoid 'cheating': relying on the host platform for advanced
+; functionality.
+;
+; Other than that, we'll say no more about the code, and focus in the rest of
+; this file on the scenarios the code cares about.
+
+(selective-load "mu.arc" section-level)
+(ero "running tests in mu.ar.c.t (takes ~30s)")
+;? (quit)
+
+(set allow-raw-addresses*)
+
+(section 20
+
+; Our language is assembly-like in that functions consist of series of
+; statements, and statements consist of an operation and its arguments (input
+; and output).
+;
+;   oarg1, oarg2, ... <- op arg1, arg2, ...
+;
+; Args must be atomic, like an integer or a memory address, they can't be
+; expressions doing arithmetic or function calls. But we can have any number
+; of them.
+;
+; Since we're building on lisp, our code samples won't look quite like the
+; idealized syntax above. For now they will look like this:
+;
+;   (function f [
+;     (oarg1 oarg2 ... <- op arg1 arg2 ...)
+;     ...
+;     ...
+;    ])
+;
+; Each arg/oarg can contain metadata separated by slashes and colons. In this
+; first example below, the only metadata is types: 'integer' for a memory
+; location containing an integer, and 'literal' for a value included directly
+; in code. (Assembly languages traditionally call them 'immediate' operands.)
+; In the future a simple tool will check that the types line up as expected in
+; each op. A different tool might add types where they aren't provided.
+; Instead of a monolithic compiler I want to build simple, lightweight tools
+; that can be combined in various ways, say for using different typecheckers
+; in different subsystems.
+;
+; In our tests we'll define such mu functions using a call to 'add-code', so
+; look for it when reading the code examples. Everything outside 'add-code' is
+; just test-harness details that can be skipped at first.
+
+(reset)
+;? (set dump-trace*)
+(new-trace "literal")
+(add-code
+  '((function main [
+      (1:integer <- copy 23:literal)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 23)
+  (prn "F - 'copy' writes its lone 'arg' after the instruction name to its lone 'oarg' or output arg before the arrow. After this test, the value 23 is stored in memory address 1."))
+;? (reset) ;? 2
+;? (quit) ;? 2
+
+; Our basic arithmetic ops can operate on memory locations or literals.
+; (Ignore hardware details like registers for now.)
+
+(reset)
+(new-trace "add")
+(add-code
+  '((function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (3:integer <- add 1:integer 2:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 1  2 3  3 4))
+  (prn "F - 'add' operates on two addresses"))
+;? (reset) ;? 1
+;? (quit) ;? 1
+
+(reset)
+(new-trace "add-literal")
+(add-code
+  '((function main [
+      (1:integer <- add 2:literal 3:literal)
+     ])))
+(run 'main)
+(when (~is memory*.1 5)
+  (prn "F - ops can take 'literal' operands (but not return them)"))
+
+(reset)
+(new-trace "sub-literal")
+(add-code
+  '((function main [
+      (1:integer <- subtract 1:literal 3:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 -2)
+  (prn "F - 'subtract'"))
+
+(reset)
+(new-trace "mul-literal")
+(add-code
+  '((function main [
+      (1:integer <- multiply 2:literal 3:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 6)
+  (prn "F - 'multiply'"))
+
+(reset)
+(new-trace "div-literal")
+(add-code
+  '((function main [
+      (1:integer <- divide 8:literal 3:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 (/ real.8 3))
+  (prn "F - 'divide'"))
+
+(reset)
+(new-trace "idiv-literal")
+(add-code
+  '((function main [
+      (1:integer 2:integer <- divide-with-remainder 23:literal 6:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 3  2 5))
+  (prn "F - 'divide-with-remainder' performs integer division"))
+
+(reset)
+(new-trace "dummy-oarg")
+;? (set dump-trace*)
+(add-code
+  '((function main [
+      (_ 2:integer <- divide-with-remainder 23:literal 6:literal)
+     ])))
+(run 'main)
+(when (~iso memory* (obj 2 5))
+  (prn "F - '_' oarg can ignore some results"))
+;? (quit)
+
+; Basic boolean operations: and, or, not
+; There are easy ways to encode booleans in binary, but we'll skip past those
+; details for now.
+
+(reset)
+(new-trace "and-literal")
+(add-code
+  '((function main [
+      (1:boolean <- and t:literal nil:literal)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 nil)
+  (prn "F - logical 'and' for booleans"))
+
+; Basic comparison operations
+
+(reset)
+(new-trace "lt-literal")
+(add-code
+  '((function main [
+      (1:boolean <- less-than 4:literal 3:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 nil)
+  (prn "F - 'less-than' inequality operator"))
+
+(reset)
+(new-trace "le-literal-false")
+(add-code
+  '((function main [
+      (1:boolean <- lesser-or-equal 4:literal 3:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 nil)
+  (prn "F - 'lesser-or-equal'"))
+
+(reset)
+(new-trace "le-literal-true")
+(add-code
+  '((function main [
+      (1:boolean <- lesser-or-equal 4:literal 4:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 t)
+  (prn "F - 'lesser-or-equal' returns true for equal operands"))
+
+(reset)
+(new-trace "le-literal-true-2")
+(add-code
+  '((function main [
+      (1:boolean <- lesser-or-equal 4:literal 5:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 t)
+  (prn "F - 'lesser-or-equal' - 2"))
+
+; Control flow operations: jump, jump-if, jump-unless
+; These introduce a new type -- 'offset' -- for literals that refer to memory
+; locations relative to the current location.
+
+(reset)
+(new-trace "jump-skip")
+(add-code
+  '((function main [
+      (1:integer <- copy 8:literal)
+      (jump 1:offset)
+      (2:integer <- copy 3:literal)  ; should be skipped
+      (reply)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 8))
+  (prn "F - 'jump' skips some instructions"))
+;? (quit)
+
+(reset)
+(new-trace "jump-target")
+(add-code
+  '((function main [
+      (1:integer <- copy 8:literal)
+      (jump 1:offset)
+      (2:integer <- copy 3:literal)  ; should be skipped
+      (reply)
+      (3:integer <- copy 34:literal)
+     ])))  ; never reached
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 8))
+  (prn "F - 'jump' doesn't skip too many instructions"))
+;? (quit)
+
+(reset)
+(new-trace "jump-if-skip")
+(add-code
+  '((function main [
+      (2:integer <- copy 1:literal)
+      (1:boolean <- equal 1:literal 2:integer)
+      (jump-if 1:boolean 1:offset)
+      (2:integer <- copy 3:literal)
+      (reply)
+      (3:integer <- copy 34:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 t  2 1))
+  (prn "F - 'jump-if' is a conditional 'jump'"))
+
+(reset)
+(new-trace "jump-if-fallthrough")
+(add-code
+  '((function main [
+      (1:boolean <- equal 1:literal 2:literal)
+      (jump-if 3:boolean 1:offset)
+      (2:integer <- copy 3:literal)
+      (reply)
+      (3:integer <- copy 34:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 nil  2 3))
+  (prn "F - if 'jump-if's first arg is false, it doesn't skip any instructions"))
+
+(reset)
+(new-trace "jump-if-backward")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 1:literal)
+      ; loop
+      (2:integer <- add 2:integer 2:integer)
+      (3:boolean <- equal 1:integer 2:integer)
+      (jump-if 3:boolean -3:offset)  ; to loop
+      (4:integer <- copy 3:literal)
+      (reply)
+      (3:integer <- copy 34:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 4  3 nil  4 3))
+  (prn "F - 'jump-if' can take a negative offset to make backward jumps"))
+
+(reset)
+(new-trace "jump-label")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 1:literal)
+      loop
+      (2:integer <- add 2:integer 2:integer)
+      (3:boolean <- equal 1:integer 2:integer)
+      (jump-if 3:boolean loop:offset)
+      (4:integer <- copy 3:literal)
+      (reply)
+      (3:integer <- copy 34:literal)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj whitelist '("-")))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 4  3 nil  4 3))
+  (prn "F - 'jump-if' can take a negative offset to make backward jumps"))
+;? (quit)
+
+; Data movement relies on addressing modes:
+;   'direct' - refers to a memory location; default for most types.
+;   'literal' - directly encoded in the code; implicit for some types like 'offset'.
+
+(reset)
+(new-trace "direct-addressing")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:integer <- copy 1:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 34))
+  (prn "F - 'copy' performs direct addressing"))
+
+; 'Indirect' addressing refers to an address stored in a memory location.
+; Indicated by the metadata '/deref'. Usually requires an address type.
+; In the test below, the memory location 1 contains '2', so an indirect read
+; of location 1 returns the value of location 2.
+
+(reset)
+(new-trace "indirect-addressing")
+(add-code
+  '((function main [
+      (1:integer-address <- copy 2:literal)  ; unsafe; can't do this in general
+      (2:integer <- copy 34:literal)
+      (3:integer <- copy 1:integer-address/deref)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 34  3 34))
+  (prn "F - 'copy' performs indirect addressing"))
+
+; Output args can use indirect addressing. In the test below the value is
+; stored at the location stored in location 1 (i.e. location 2).
+
+(reset)
+(new-trace "indirect-addressing-oarg")
+(add-code
+  '((function main [
+      (1:integer-address <- copy 2:literal)
+      (2:integer <- copy 34:literal)
+      (1:integer-address/deref <- add 2:integer 2:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 36))
+  (prn "F - instructions can perform indirect addressing on output arg"))
+
+;; Compound data types
+;
+; Until now we've dealt with scalar types like integers and booleans and
+; addresses, where mu looks like other assembly languages. In addition, mu
+; provides first-class support for compound types: arrays and and-records.
+;
+; 'get' accesses fields in and-records
+; 'index' accesses indices in arrays
+;
+; Both operations require knowledge about the types being worked on, so all
+; types used in mu programs are defined in a single global system-wide table
+; (see type* in mu.arc for the complete list of types; we'll add to it over
+; time).
+
+; first a sanity check that the table of types is consistent
+(reset)
+(each (typ typeinfo) type*
+  (when typeinfo!and-record
+    (assert (is typeinfo!size (len typeinfo!elems)))
+    (when typeinfo!fields
+      (assert (is typeinfo!size (len typeinfo!fields))))))
+
+(reset)
+(new-trace "get-record")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:boolean <- copy nil:literal)
+      (3:boolean <- get 1:integer-boolean-pair 1:offset)
+      (4:integer <- get 1:integer-boolean-pair 0:offset)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 nil  3 nil  4 34))
+  (prn "F - 'get' accesses fields of and-records"))
+;? (quit)
+
+(reset)
+(new-trace "get-indirect")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:boolean <- copy nil:literal)
+      (3:integer-boolean-pair-address <- copy 1:literal)
+      (4:boolean <- get 3:integer-boolean-pair-address/deref 1:offset)
+      (5:integer <- get 3:integer-boolean-pair-address/deref 0:offset)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 nil  3 1  4 nil  5 34))
+  (prn "F - 'get' accesses fields of and-record address"))
+
+(reset)
+(new-trace "get-indirect-repeated")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:integer <- copy 35:literal)
+      (3:integer <- copy 36:literal)
+      (4:integer-point-pair-address <- copy 1:literal)  ; unsafe
+      (5:integer-point-pair-address-address <- copy 4:literal)  ; unsafe
+      (6:integer-integer-pair <- get 5:integer-point-pair-address-address/deref/deref 1:offset)
+      (8:integer <- get 5:integer-point-pair-address-address/deref/deref 0:offset)
+     ])))
+(run 'main)
+(when (~memory-contains 6 '(35 36 34))
+  (prn "F - 'get' can deref multiple times"))
+;? (quit)
+
+(reset)
+(new-trace "get-compound-field")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:integer <- copy 35:literal)
+      (3:integer <- copy 36:literal)
+      (4:integer-integer-pair <- get 1:integer-point-pair 1:offset)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 35  3 36  4 35  5 36))
+  (prn "F - 'get' accesses fields spanning multiple locations"))
+
+(reset)
+(new-trace "get-address")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:boolean <- copy t:literal)
+      (3:boolean-address <- get-address 1:integer-boolean-pair 1:offset)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 t  3 2))
+  (prn "F - 'get-address' returns address of fields of and-records"))
+
+(reset)
+(new-trace "get-address-indirect")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:boolean <- copy t:literal)
+      (3:integer-boolean-pair-address <- copy 1:literal)
+      (4:boolean-address <- get-address 3:integer-boolean-pair-address/deref 1:offset)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 t  3 1  4 2))
+  (prn "F - 'get-address' accesses fields of and-record address"))
+
+(reset)
+(new-trace "index-literal")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer-boolean-pair <- index 1:integer-boolean-pair-array 1:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 23 3 nil  4 24 5 t  6 24 7 t))
+  (prn "F - 'index' accesses indices of arrays"))
+;? (quit)
+
+(reset)
+(new-trace "index-direct")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer <- copy 1:literal)
+      (7:integer-boolean-pair <- index 1:integer-boolean-pair-array 6:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 23 3 nil  4 24 5 t  6 1  7 24 8 t))
+  (prn "F - 'index' accesses indices of arrays"))
+;? (quit)
+
+(reset)
+(new-trace "index-indirect")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer <- copy 1:literal)
+      (7:integer-boolean-pair-array-address <- copy 1:literal)
+      (8:integer-boolean-pair <- index 7:integer-boolean-pair-array-address/deref 6:integer)
+     ])))
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "cvt0" "cvt1")))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 23 3 nil  4 24 5 t  6 1  7 1  8 24 9 t))
+  (prn "F - 'index' accesses indices of array address"))
+;? (quit)
+
+(reset)
+(new-trace "index-indirect-multiple")
+(add-code
+  '((function main [
+      (1:integer <- copy 4:literal)
+      (2:integer <- copy 23:literal)
+      (3:integer <- copy 24:literal)
+      (4:integer <- copy 25:literal)
+      (5:integer <- copy 26:literal)
+      (6:integer-array-address <- copy 1:literal)  ; unsafe
+      (7:integer-array-address-address <- copy 6:literal)  ; unsafe
+      (8:integer <- index 7:integer-array-address-address/deref/deref 1:literal)
+     ])))
+(run 'main)
+(when (~is memory*.8 24)
+  (prn "F - 'index' can deref multiple times"))
+
+(reset)
+(new-trace "index-address")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer <- copy 1:literal)
+      (7:integer-boolean-pair-address <- index-address 1:integer-boolean-pair-array 6:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 23 3 nil  4 24 5 t  6 1  7 4))
+  (prn "F - 'index-address' returns addresses of indices of arrays"))
+
+(reset)
+(new-trace "index-address-indirect")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer <- copy 1:literal)
+      (7:integer-boolean-pair-array-address <- copy 1:literal)
+      (8:integer-boolean-pair-address <- index-address 7:integer-boolean-pair-array-address/deref 6:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 2  2 23 3 nil  4 24 5 t  6 1  7 1  8 4))
+  (prn "F - 'index-address' returns addresses of indices of array addresses"))
+
+; Array values know their length. Record lengths are saved in the types table.
+
+(reset)
+(new-trace "len-array")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer <- length 1:integer-boolean-pair-array)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.6 2)
+  (prn "F - 'length' of array"))
+
+(reset)
+(new-trace "len-array-indirect")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer-address <- copy 1:literal)
+      (7:integer <- length 6:integer-boolean-pair-array-address/deref)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "cvt0" "cvt1")))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.7 2)
+  (prn "F - 'length' of array address"))
+
+; 'sizeof' is a helper to determine the amount of memory required by a type.
+; Only for non-arrays.
+
+(reset)
+(new-trace "sizeof-record")
+(add-code
+  '((function main [
+      (1:integer <- sizeof integer-boolean-pair:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.1 2)
+  (prn "F - 'sizeof' returns space required by arg"))
+
+(reset)
+(new-trace "sizeof-record-not-len")
+(add-code
+  '((function main [
+      (1:integer <- sizeof integer-point-pair:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (is memory*.1 2)
+  (prn "F - 'sizeof' is different from number of elems"))
+
+; Regardless of a type's length, you can move it around just like a primitive.
+
+(reset)
+(new-trace "copy-record")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:boolean <- copy nil:literal)
+      (4:boolean <- copy t:literal)
+      (3:integer-boolean-pair <- copy 1:integer-boolean-pair)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 nil  3 34  4 nil))
+  (prn "F - ops can operate on records spanning multiple locations"))
+
+(reset)
+(new-trace "copy-record2")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:integer <- copy 35:literal)
+      (3:integer <- copy 36:literal)
+      (4:integer <- copy 0:literal)
+      (5:integer <- copy 0:literal)
+      (6:integer <- copy 0:literal)
+      (4:integer-point-pair <- copy 1:integer-point-pair)
+     ])))
+;? (= dump-trace* (obj whitelist '("run" "sizeof")))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 35  3 36
+                         ; result
+                         4 34  5 35  6 36))
+  (prn "F - ops can operate on records with fields spanning multiple locations"))
+
+)  ; section 20
+
+(section 100
+
+; A special kind of record is the 'tagged type'. It lets us represent
+; dynamically typed values, which save type information in memory rather than
+; in the code to use them. This will let us do things like create heterogenous
+; lists containing both integers and strings. Tagged values admit two
+; operations:
+;
+;   'save-type' - turns a regular value into a tagged-value of the appropriate type
+;   'maybe-coerce' - turns a tagged value into a regular value if the type matches
+;
+; The payload of a tagged value must occupy just one location. Save pointers
+; to records.
+
+(reset)
+(new-trace "tagged-value")
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "cvt0" "cvt1")))
+(add-code
+  '((function main [
+      (1:type <- copy integer:literal)
+      (2:integer <- copy 34:literal)
+      (3:integer 4:boolean <- maybe-coerce 1:tagged-value integer:literal)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn completed-routines*)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn memory*)
+(when (or (~is memory*.3 34)
+          (~is memory*.4 t))
+  (prn "F - 'maybe-coerce' copies value only if type tag matches"))
+;? (quit)
+
+(reset)
+(new-trace "tagged-value-2")
+;? (set dump-trace*)
+(add-code
+  '((function main [
+      (1:type <- copy integer-address:literal)
+      (2:integer <- copy 34:literal)
+      (3:boolean 4:boolean <- maybe-coerce 1:tagged-value boolean:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (or (~is memory*.3 0)
+          (~is memory*.4 nil))
+  (prn "F - 'maybe-coerce' doesn't copy value when type tag doesn't match"))
+
+(reset)
+(new-trace "save-type")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:tagged-value <- save-type 1:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj  1 34  2 'integer  3 34))
+  (prn "F - 'save-type' saves the type of a value at runtime, turning it into a tagged-value"))
+
+(reset)
+(new-trace "init-tagged-value")
+(add-code
+  '((function main [
+      (1:integer <- copy 34:literal)
+      (2:tagged-value-address <- init-tagged-value integer:literal 1:integer)
+      (3:integer 4:boolean <- maybe-coerce 2:tagged-value-address/deref integer:literal)
+     ])))
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "cvt0" "cvt1" "sizeof")))
+(run 'main)
+;? (prn memory*)
+(when (or (~is memory*.3 34)
+          (~is memory*.4 t))
+  (prn "F - 'init-tagged-value' is the converse of 'maybe-coerce'"))
+;? (quit)
+
+; Now that we can package values together with their types, we can construct a
+; dynamically typed list.
+
+(reset)
+(new-trace "list")
+;? (set dump-trace*)
+(add-code
+  '((function main [
+      ; 1 points at first node: tagged-value (int 34)
+      (1:list-address <- new list:literal)
+      (2:tagged-value-address <- list-value-address 1:list-address)
+      (3:type-address <- get-address 2:tagged-value-address/deref type:offset)
+      (3:type-address/deref <- copy integer:literal)
+      (4:location <- get-address 2:tagged-value-address/deref payload:offset)
+      (4:location/deref <- copy 34:literal)
+      (5:list-address-address <- get-address 1:list-address/deref cdr:offset)
+      (5:list-address-address/deref <- new list:literal)
+      ; 6 points at second node: tagged-value (boolean t)
+      (6:list-address <- copy 5:list-address-address/deref)
+      (7:tagged-value-address <- list-value-address 6:list-address)
+      (8:type-address <- get-address 7:tagged-value-address/deref type:offset)
+      (8:type-address/deref <- copy boolean:literal)
+      (9:location <- get-address 7:tagged-value-address/deref payload:offset)
+      (9:location/deref <- copy t:literal)
+      (10:list-address <- get 6:list-address/deref 1:offset)
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let first rep.routine!alloc
+;?     (= dump-trace* (obj whitelist '("run")))
+;?     (set dump-trace*)
+    (run)
+;?     (prn memory*)
+    (each routine completed-routines*
+      (aif rep.routine!error (prn "error - " it)))
+    (when (or (~all first (map memory* '(1 2 3)))
+              (~is memory*.first  'integer)
+              (~is memory*.4 (+ first 1))
+              (~is (memory* (+ first 1))  34)
+              (~is memory*.5 (+ first 2))
+              (let second memory*.6
+                (or
+                  (~is (memory* (+ first 2)) second)
+                  (~all second (map memory* '(6 7 8)))
+                  (~is memory*.second 'boolean)
+                  (~is memory*.9 (+ second 1))
+                  (~is (memory* (+ second 1)) t)
+                  (~is memory*.10 nil))))
+      (prn "F - lists can contain elements of different types"))))
+(run-code test2
+  (10:list-address <- list-next 1:list-address))
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~is memory*.10 memory*.6)
+  (prn "F - 'list-next can move a list pointer to the next node"))
+;? (quit)
+
+; 'init-list' takes a variable number of args and constructs a list containing
+; them. Just integers for now.
+
+(reset)
+(new-trace "init-list")
+(add-code
+  '((function main [
+      (1:integer <- init-list 3:literal 4:literal 5:literal)
+     ])))
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "cvt0" "cvt1" "sizeof")))
+(run 'main)
+;? (prn memory*)
+(let first memory*.1
+;?   (prn first)
+  (when (or (~is memory*.first  'integer)
+            (~is (memory* (+ first 1))  3)
+            (let second (memory* (+ first 2))
+;?               (prn second)
+              (or (~is memory*.second 'integer)
+                  (~is (memory* (+ second 1)) 4)
+                  (let third (memory* (+ second 2))
+;?                     (prn third)
+                    (or (~is memory*.third 'integer)
+                        (~is (memory* (+ third 1)) 5)
+                        (~is (memory* (+ third 2) nil)))))))
+    (prn "F - 'init-list' can construct a list of integers")))
+
+)  ; section 100
+
+(section 20
+
+;; Functions
+;
+; Just like the table of types is centralized, functions are conceptualized as
+; a centralized table of operations just like the "primitives" we've seen so
+; far. If you create a function you can call it like any other op.
+
+(reset)
+(new-trace "new-fn")
+(add-code
+  '((function test1 [
+      (3:integer <- add 1:integer 2:integer)
+     ])
+    (function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (test1)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 1  2 3  3 4))
+  (prn "F - calling a user-defined function runs its instructions"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-once")
+(add-code
+  '((function test1 [
+      (1:integer <- copy 1:literal)
+     ])
+    (function main [
+      (test1)
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(when (~is 2 curr-cycle*)
+  (prn "F - calling a user-defined function runs its instructions exactly once " curr-cycle*))
+;? (quit)
+
+; User-defined functions communicate with their callers through two
+; primitives:
+;
+;   'arg' - to access inputs
+;   'reply' - to return outputs
+
+(reset)
+(new-trace "new-fn-reply")
+(add-code
+  '((function test1 [
+      (3:integer <- add 1:integer 2:integer)
+      (reply)
+      (4:integer <- copy 34:literal)
+     ])
+    (function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (test1)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 1  2 3  3 4))
+  (prn "F - 'reply' stops executing the current function"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-reply-nested")
+(add-code
+  '((function test1 [
+      (3:integer <- test2)
+     ])
+    (function test2 [
+      (reply 2:integer)
+     ])
+    (function main [
+      (2:integer <- copy 34:literal)
+      (test1)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 2 34  3 34))
+  (prn "F - 'reply' stops executing any callers as necessary"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-reply-once")
+(add-code
+  '((function test1 [
+      (3:integer <- add 1:integer 2:integer)
+      (reply)
+      (4:integer <- copy 34:literal)
+     ])
+    (function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (test1)
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(when (~is 5 curr-cycle*)
+  (prn "F - 'reply' executes instructions exactly once " curr-cycle*))
+;? (quit)
+
+(reset)
+(new-trace "reply-increments-caller-pc")
+(add-code
+  '((function callee [
+      (reply)
+     ])
+    (function caller [
+      (1:integer <- copy 0:literal)
+      (2:integer <- copy 0:literal)
+     ])))
+(freeze function*)
+(= routine* (make-routine 'caller))
+(assert (is 0 pc.routine*))
+(push-stack routine* 'callee)  ; pretend call was at first instruction of caller
+(run-for-time-slice 1)
+(when (~is 1 pc.routine*)
+  (prn "F - 'reply' increments pc in caller (to move past calling instruction)"))
+
+(reset)
+(new-trace "new-fn-arg-sequential")
+(add-code
+  '((function test1 [
+      (4:integer <- next-input)
+      (5:integer <- next-input)
+      (3:integer <- add 4:integer 5:integer)
+      (reply)
+      (4:integer <- copy 34:literal)
+     ])
+    (function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (test1 1:integer 2:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 1  2 3  3 4
+                         ; test1's temporaries
+                         4 1  5 3))
+  (prn "F - 'arg' accesses in order the operands of the most recent function call (the caller)"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-arg-random-access")
+;? (set dump-trace*)
+(add-code
+  '((function test1 [
+      (5:integer <- input 1:literal)
+      (4:integer <- input 0:literal)
+      (3:integer <- add 4:integer 5:integer)
+      (reply)
+      (4:integer <- copy 34:literal)  ; should never run
+     ])
+    (function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (test1 1:integer 2:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 1  2 3  3 4
+                         ; test's temporaries
+                         4 1  5 3))
+  (prn "F - 'arg' with index can access function call arguments out of order"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-arg-random-then-sequential")
+;? (set dump-trace*)
+(add-code
+  '((function test1 [
+      (_ <- input 1:literal)
+      (1:integer <- next-input)  ; takes next arg after index 1
+     ])  ; should never run
+    (function main [
+      (test1 1:literal 2:literal 3:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 3))
+  (prn "F - 'arg' with index resets index for later calls"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-arg-status")
+(add-code
+  '((function test1 [
+      (4:integer 5:boolean <- next-input)
+     ])
+    (function main [
+      (test1 1:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 4 1  5 t))
+  (prn "F - 'arg' sets a second oarg when arg exists"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-arg-missing")
+(add-code
+  '((function test1 [
+      (4:integer <- next-input)
+      (5:integer <- next-input)
+     ])
+    (function main [
+      (test1 1:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 4 1))
+  (prn "F - missing 'arg' doesn't cause error"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-arg-missing-2")
+(add-code
+  '((function test1 [
+      (4:integer <- next-input)
+      (5:integer 6:boolean <- next-input)
+     ])
+    (function main [
+      (test1 1:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 4 1  6 nil))
+  (prn "F - missing 'arg' wipes second oarg when provided"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-arg-missing-3")
+(add-code
+  '((function test1 [
+      (4:integer <- next-input)
+      (5:integer <- copy 34:literal)
+      (5:integer 6:boolean <- next-input)
+    ])
+    (function main [
+      (test1 1:literal)
+    ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 4 1  6 nil))
+  (prn "F - missing 'arg' consistently wipes its oarg"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-arg-missing-4")
+(add-code
+  '((function test1 [
+      ; if given two args, adds them; if given one arg, increments
+      (4:integer <- next-input)
+      (5:integer 6:boolean <- next-input)
+      { begin
+        (break-if 6:boolean)
+        (5:integer <- copy 1:literal)
+      }
+      (7:integer <- add 4:integer 5:integer)
+     ])
+    (function main [
+      (test1 34:literal)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 4 34  5 1  6 nil  7 35))
+  (prn "F - function with optional second arg"))
+;? (quit)
+
+(reset)
+(new-trace "new-fn-arg-by-value")
+(add-code
+  '((function test1 [
+      (1:integer <- copy 0:literal)  ; overwrite caller memory
+      (2:integer <- next-input)
+     ])  ; arg not clobbered
+    (function main [
+      (1:integer <- copy 34:literal)
+      (test1 1:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 0  2 34))
+  (prn "F - 'arg' passes by value"))
+
+(reset)
+(new-trace "arg-record")
+(add-code
+  '((function test1 [
+      (4:integer-boolean-pair <- next-input)
+     ])
+    (function main [
+      (1:integer <- copy 34:literal)
+      (2:boolean <- copy nil:literal)
+      (test1 1:integer-boolean-pair)
+     ])))
+(run 'main)
+(when (~iso memory* (obj 1 34  2 nil  4 34  5 nil))
+  (prn "F - 'arg' can copy records spanning multiple locations"))
+
+(reset)
+(new-trace "arg-record-indirect")
+;? (set dump-trace*)
+(add-code
+  '((function test1 [
+      (4:integer-boolean-pair <- next-input)
+     ])
+    (function main [
+      (1:integer <- copy 34:literal)
+      (2:boolean <- copy nil:literal)
+      (3:integer-boolean-pair-address <- copy 1:literal)
+      (test1 3:integer-boolean-pair-address/deref)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 34  2 nil  3 1  4 34  5 nil))
+  (prn "F - 'arg' can copy records spanning multiple locations in indirect mode"))
+
+(reset)
+(new-trace "new-fn-reply-oarg")
+(add-code
+  '((function test1 [
+      (4:integer <- next-input)
+      (5:integer <- next-input)
+      (6:integer <- add 4:integer 5:integer)
+      (reply 6:integer)
+      (4:integer <- copy 34:literal)
+     ])
+    (function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (3:integer <- test1 1:integer 2:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 1  2 3  3 4
+                         ; test1's temporaries
+                         4 1  5 3  6 4))
+  (prn "F - 'reply' can take aguments that are returned, or written back into output args of caller"))
+
+(reset)
+(new-trace "new-fn-reply-oarg-multiple")
+(add-code
+  '((function test1 [
+      (4:integer <- next-input)
+      (5:integer <- next-input)
+      (6:integer <- add 4:integer 5:integer)
+      (reply 6:integer 5:integer)
+      (4:integer <- copy 34:literal)
+     ])
+    (function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (3:integer 7:integer <- test1 1:integer 2:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 1  2 3  3 4    7 3
+                         ; test1's temporaries
+                         4 1  5 3  6 4))
+  (prn "F - 'reply' permits a function to return multiple values at once"))
+
+; 'prepare-reply' is useful for doing cleanup before exiting a function
+(reset)
+(new-trace "new-fn-prepare-reply")
+(add-code
+  '((function test1 [
+      (4:integer <- next-input)
+      (5:integer <- next-input)
+      (6:integer <- add 4:integer 5:integer)
+      (prepare-reply 6:integer 5:integer)
+      (reply)
+      (4:integer <- copy 34:literal)
+     ])
+    (function main [
+      (1:integer <- copy 1:literal)
+      (2:integer <- copy 3:literal)
+      (3:integer 7:integer <- test1 1:integer 2:integer)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory* (obj 1 1  2 3  3 4    7 3
+                         ; test1's temporaries
+                         4 1  5 3  6 4))
+  (prn "F - without args, 'reply' returns values from previous 'prepare-reply'."))
+
+; When you have arguments that are both read from and written to, include them
+; redundantly in both ingredients and results. That'll help tools track what
+; changed.
+
+; To enforce that the result and ingredient must always match, use the
+; 'same-as-arg' property. Results with 'same-as-arg' properties should only be
+; copied to a caller output arg identical to the specified caller arg.
+(reset)
+(new-trace "new-fn-same-as-arg")
+(add-code
+  '((function test1 [
+      ; increment the contents of an address
+      (default-space:space-address <- new space:literal 2:literal)
+      (x:integer-address <- next-input)
+      (x:integer-address/deref <- add x:integer-address/deref 1:literal)
+      (reply x:integer-address/same-as-arg:0)
+    ])
+    (function main [
+      (2:integer-address <- new integer:literal)
+      (2:integer-address/deref <- copy 0:literal)
+      (3:integer-address <- test1 2:integer-address)
+    ])))
+(run 'main)
+(let routine (car completed-routines*)
+;?   (prn rep.routine!error) ;? 1
+  (when (no rep.routine!error)
+    (prn "F - 'same-as-arg' results must be identical to a given input")))
+;? (quit) ;? 2
+
+)  ; section 20
+
+(section 11
+
+;; Structured programming
+;
+; Our jump operators are quite inconvenient to use, so mu provides a
+; lightweight tool called 'convert-braces' to work in a slightly more
+; convenient format with nested braces:
+;
+;   {
+;     some instructions
+;     {
+;       more instructions
+;     }
+;   }
+;
+; Braces are like labels in assembly language, they require no special
+; parsing. The operations 'loop' and 'break' jump to just after the enclosing
+; '{' and '}' respectively.
+;
+; Conditional and unconditional 'loop' and 'break' should give us 80% of the
+; benefits of the control-flow primitives we're used to in other languages,
+; like 'if', 'while', 'for', etc.
+;
+; Compare 'unquoted blocks' using {} with 'quoted blocks' using [] that we've
+; gotten used to seeing. Quoted blocks are used by top-level instructions to
+; provide code without running it.
+
+(reset)
+(new-trace "convert-braces")
+(= traces* (queue))
+;? (= dump-trace* (obj whitelist '("c{0" "c{1")))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                (((2 integer)) <- ((copy)) ((0 literal)))
+                (((3 integer)) <- ((copy)) ((0 literal)))
+                { begin  ; 'begin' is just a hack because racket turns braces into parens
+                  (((4 boolean)) <- ((not-equal)) ((1 integer)) ((3 integer)))
+                  (((break-if)) ((4 boolean)))
+                  (((5 integer)) <- ((copy)) ((0 literal)))
+                }
+                (((reply)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((4 boolean)) <- ((not-equal)) ((1 integer)) ((3 integer)))
+              (((jump-if)) ((4 boolean)) ((1 offset)))
+              (((5 integer)) <- ((copy)) ((0 literal)))
+              (((reply)))))
+  (prn "F - convert-braces replaces break-if with a jump-if to after the next close-brace"))
+;? (quit)
+
+(reset)
+(new-trace "convert-braces-empty-block")
+(= traces* (queue))
+;? (= dump-trace* (obj whitelist '("c{0" "c{1")))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                (((2 integer)) <- ((copy)) ((0 literal)))
+                (((3 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((break)))
+                }
+                (((reply)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((0 offset)))
+              (((reply)))))
+  (prn "F - convert-braces works for degenerate blocks"))
+;? (quit)
+
+(reset)
+(new-trace "convert-braces-nested-break")
+(= traces* (queue))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                (((2 integer)) <- ((copy)) ((0 literal)))
+                (((3 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((4 boolean)) <- ((not-equal)) ((1 integer)) ((3 integer)))
+                  (((break-if)) ((4 boolean)))
+                  { begin
+                    (((5 integer)) <- ((copy)) ((0 literal)))
+                  }
+                }
+                (((reply)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((4 boolean)) <- ((not-equal)) ((1 integer)) ((3 integer)))
+              (((jump-if)) ((4 boolean)) ((1 offset)))
+              (((5 integer)) <- ((copy)) ((0 literal)))
+              (((reply)))))
+  (prn "F - convert-braces balances braces when converting break"))
+
+(reset)
+(new-trace "convert-braces-repeated-jump")
+(= traces* (queue))
+;? (= dump-trace* (obj whitelist '("c{0" "c{1")))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((break)))
+                  (((2 integer)) <- ((copy)) ((0 literal)))
+                }
+                { begin
+                  (((break)))
+                  (((3 integer)) <- ((copy)) ((0 literal)))
+                }
+                (((4 integer)) <- ((copy)) ((0 literal)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((1 offset)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((1 offset)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((4 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-braces handles jumps on jumps"))
+;? (quit)
+
+(reset)
+(new-trace "convert-braces-nested-loop")
+(= traces* (queue))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                (((2 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((3 integer)) <- ((copy)) ((0 literal)))
+                  { begin
+                    (((4 boolean)) <- ((not-equal)) ((1 integer)) ((3 integer)))
+                  }
+                  (((loop-if)) ((4 boolean)))
+                  (((5 integer)) <- ((copy)) ((0 literal)))
+                }
+                (((reply)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((4 boolean)) <- ((not-equal)) ((1 integer)) ((3 integer)))
+              (((jump-if)) ((4 boolean)) ((-3 offset)))
+              (((5 integer)) <- ((copy)) ((0 literal)))
+              (((reply)))))
+  (prn "F - convert-braces balances braces when converting 'loop'"))
+
+(reset)
+(new-trace "convert-braces-label")
+(= traces* (queue))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                foo
+                (((2 integer)) <- ((copy)) ((0 literal)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              foo
+              (((2 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-braces skips past labels"))
+;? (quit)
+
+(reset)
+(new-trace "convert-braces-label-increments-offset")
+(= traces* (queue))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((break)))
+                  foo
+                }
+                (((2 integer)) <- ((copy)) ((0 literal)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((1 offset)))
+              foo
+              (((2 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-braces treats labels as instructions"))
+;? (quit)
+
+(reset)
+(new-trace "convert-braces-label-increments-offset2")
+(= traces* (queue))
+;? (= dump-trace* (obj whitelist '("c{0" "c{1")))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((break)))
+                  foo
+                }
+                (((2 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((break)))
+                  (((3 integer)) <- ((copy)) ((0 literal)))
+                }
+                (((4 integer)) <- ((copy)) ((0 literal)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((1 offset)))
+              foo
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((1 offset)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((4 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-braces treats labels as instructions - 2"))
+;? (quit)
+
+(reset)
+(new-trace "break-multiple")
+(= traces* (queue))
+;? (= dump-trace* (obj whitelist '("-")))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  { begin
+                    (((break)) ((2 blocks)))
+                  }
+                  (((2 integer)) <- ((copy)) ((0 literal)))
+                  (((3 integer)) <- ((copy)) ((0 literal)))
+                  (((4 integer)) <- ((copy)) ((0 literal)))
+                  (((5 integer)) <- ((copy)) ((0 literal)))
+                }))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((4 offset)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((4 integer)) <- ((copy)) ((0 literal)))
+              (((5 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - 'break' can take an extra arg with number of nested blocks to exit"))
+;? (quit)
+
+(reset)
+(new-trace "loop")
+;? (set dump-trace*)
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                (((2 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((3 integer)) <- ((copy)) ((0 literal)))
+                  (((loop)))
+                }))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((-2 offset)))))
+  (prn "F - 'loop' jumps to start of containing block"))
+;? (quit)
+
+; todo: fuzz-test invariant: convert-braces offsets should be robust to any
+; number of inner blocks inside but not around the loop block.
+
+(reset)
+(new-trace "loop-nested")
+;? (set dump-trace*)
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                (((2 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((3 integer)) <- ((copy)) ((0 literal)))
+                  { begin
+                    (((4 integer)) <- ((copy)) ((0 literal)))
+                  }
+                  (((loop)))
+                }))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((4 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((-3 offset)))))
+  (prn "F - 'loop' correctly jumps back past nested braces"))
+
+(reset)
+(new-trace "loop-multiple")
+(= traces* (queue))
+;? (= dump-trace* (obj whitelist '("-")))
+(when (~iso (convert-braces
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                { begin
+                  (((2 integer)) <- ((copy)) ((0 literal)))
+                  (((3 integer)) <- ((copy)) ((0 literal)))
+                  { begin
+                    (((loop)) ((2 blocks)))
+                  }
+                }))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))
+              (((jump)) ((-3 offset)))))
+  (prn "F - 'loop' can take an extra arg with number of nested blocks to exit"))
+;? (quit)
+
+(reset)
+(new-trace "convert-labels")
+(= traces* (queue))
+(when (~iso (convert-labels
+              '(loop
+                (((jump)) ((loop offset)))))
+            '(loop
+              (((jump)) ((-2 offset)))))
+  (prn "F - 'convert-labels' rewrites jumps to labels"))
+
+;; Variables
+;
+; A big convenience high-level languages provide is the ability to name memory
+; locations. In mu, a lightweight tool called 'convert-names' provides this
+; convenience.
+
+(reset)
+(new-trace "convert-names")
+(= traces* (queue))
+;? (set dump-trace*)
+(when (~iso (convert-names
+              '((((x integer)) <- ((copy)) ((0 literal)))
+                (((y integer)) <- ((copy)) ((0 literal)))
+                (((z integer)) <- ((copy)) ((0 literal)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-names renames symbolic names to integer locations"))
+
+(reset)
+(new-trace "convert-names-compound")
+(= traces* (queue))
+(when (~iso (convert-names
+              ; copying 0 into pair is meaningless; just for testing
+              '((((x integer-boolean-pair)) <- ((copy)) ((0 literal)))
+                (((y integer)) <- ((copy)) ((0 literal)))))
+            '((((1 integer-boolean-pair)) <- ((copy)) ((0 literal)))
+              (((3 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-names increments integer locations by the size of the type of the previous var"))
+
+(reset)
+(new-trace "convert-names-nil")
+(= traces* (queue))
+;? (set dump-trace*)
+(when (~iso (convert-names
+              '((((x integer)) <- ((copy)) ((0 literal)))
+                (((y integer)) <- ((copy)) ((0 literal)))
+                ; nil location is meaningless; just for testing
+                (((nil integer)) <- ((copy)) ((0 literal)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((nil integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-names never renames nil"))
+
+(reset)
+(new-trace "convert-names-string")
+;? (set dump-trace*)
+(when (~iso (convert-names
+              '((((1 integer-address)) <- ((new)) "foo")))
+            '((((1 integer-address)) <- ((new)) "foo")))
+  (prn "convert-names passes through raw strings (just a convenience arg for 'new')"))
+
+(reset)
+(new-trace "convert-names-raw")
+(= traces* (queue))
+(when (~iso (convert-names
+              '((((x integer)) <- ((copy)) ((0 literal)))
+                (((y integer) (raw)) <- ((copy)) ((0 literal)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((y integer) (raw)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-names never renames raw operands"))
+
+(reset)
+(new-trace "convert-names-literal")
+(= traces* (queue))
+(when (~iso (convert-names
+              ; meaningless; just for testing
+              '((((x literal)) <- ((copy)) ((0 literal)))))
+            '((((x literal)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-names never renames literals"))
+
+(reset)
+(new-trace "convert-names-literal-2")
+(= traces* (queue))
+(when (~iso (convert-names
+              '((((x boolean)) <- ((copy)) ((x literal)))))
+            '((((1 boolean)) <- ((copy)) ((x literal)))))
+  (prn "F - convert-names never renames literals, even when the name matches a variable"))
+
+; kludgy support for 'fork' below
+(reset)
+(new-trace "convert-names-functions")
+(= traces* (queue))
+(when (~iso (convert-names
+              '((((x integer)) <- ((copy)) ((0 literal)))
+                (((y integer)) <- ((copy)) ((0 literal)))
+                ; meaningless; just for testing
+                (((z fn)) <- ((copy)) ((0 literal)))))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              (((2 integer)) <- ((copy)) ((0 literal)))
+              (((z fn)) <- ((copy)) ((0 literal)))))
+  (prn "F - convert-names never renames fns"))
+
+(reset)
+(new-trace "convert-names-record-fields")
+(= traces* (queue))
+;? (= dump-trace* (obj whitelist '("cn0")))
+(when (~iso (convert-names
+              '((((x integer)) <- ((get)) ((34 integer-boolean-pair)) ((bool offset)))))
+            '((((1 integer)) <- ((get)) ((34 integer-boolean-pair)) ((1 offset)))))
+  (prn "F - convert-names replaces record field offsets"))
+
+(reset)
+(new-trace "convert-names-record-fields-ambiguous")
+(= traces* (queue))
+(when (errsafe (convert-names
+                 '((((bool boolean)) <- ((copy)) ((t literal)))
+                   (((x integer)) <- ((get)) ((34 integer-boolean-pair)) ((bool offset))))))
+  (prn "F - convert-names doesn't allow offsets and variables with the same name in a function"))
+
+(reset)
+(new-trace "convert-names-record-fields-ambiguous-2")
+(= traces* (queue))
+(when (errsafe (convert-names
+                 '((((x integer)) <- ((get)) ((34 integer-boolean-pair)) ((bool offset)))
+                   (((bool boolean)) <- ((copy)) ((t literal))))))
+  (prn "F - convert-names doesn't allow offsets and variables with the same name in a function - 2"))
+
+(reset)
+(new-trace "convert-names-record-fields-indirect")
+(= traces* (queue))
+;? (= dump-trace* (obj whitelist '("cn0")))
+(when (~iso (convert-names
+              '((((x integer)) <- ((get)) ((34 integer-boolean-pair-address) (deref)) ((bool offset)))))
+            '((((1 integer)) <- ((get)) ((34 integer-boolean-pair-address) (deref)) ((1 offset)))))
+  (prn "F - convert-names replaces field offsets for record addresses"))
+;? (quit)
+
+(reset)
+(new-trace "convert-names-record-fields-multiple")
+(= traces* (queue))
+(when (~iso (convert-names
+              '((((2 boolean)) <- ((get)) ((1 integer-boolean-pair)) ((bool offset)))
+                (((3 boolean)) <- ((get)) ((1 integer-boolean-pair)) ((bool offset)))))
+            '((((2 boolean)) <- ((get)) ((1 integer-boolean-pair)) ((1 offset)))
+              (((3 boolean)) <- ((get)) ((1 integer-boolean-pair)) ((1 offset)))))
+  (prn "F - convert-names replaces field offsets with multiple mentions"))
+;? (quit)
+
+(reset)
+(new-trace "convert-names-label")
+(= traces* (queue))
+(when (~iso (convert-names
+              '((((1 integer)) <- ((copy)) ((0 literal)))
+                foo))
+            '((((1 integer)) <- ((copy)) ((0 literal)))
+              foo))
+  (prn "F - convert-names skips past labels"))
+;? (quit)
+
+)  ; section 11
+
+(section 20
+
+; A rudimentary memory allocator. Eventually we want to write this in mu.
+;
+; No deallocation yet; let's see how much code we can build in mu before we
+; feel the need for it.
+
+(reset)
+(new-trace "new-primitive")
+(add-code
+  '((function main [
+      (1:integer-address <- new integer:literal)
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let before rep.routine!alloc
+;?     (set dump-trace*)
+    (run)
+;?     (prn memory*)
+    (when (~iso memory*.1 before)
+      (prn "F - 'new' returns current high-water mark"))
+    (when (~iso rep.routine!alloc (+ before 1))
+      (prn "F - 'new' on primitive types increments high-water mark by their size"))))
+;? (quit)
+
+(reset)
+(new-trace "new-array-literal")
+(add-code
+  '((function main [
+      (1:type-array-address <- new type-array:literal 5:literal)
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let before rep.routine!alloc
+    (run)
+;?     (prn memory*)
+    (when (~iso memory*.1 before)
+      (prn "F - 'new' on array with literal size returns current high-water mark"))
+    (when (~iso rep.routine!alloc (+ before 6))
+      (prn "F - 'new' on primitive arrays increments high-water mark by their size"))))
+
+(reset)
+(new-trace "new-array-direct")
+(add-code
+  '((function main [
+      (1:integer <- copy 5:literal)
+      (2:type-array-address <- new type-array:literal 1:integer)
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let before rep.routine!alloc
+    (run)
+;?     (prn memory*)
+    (when (~iso memory*.2 before)
+      (prn "F - 'new' on array with variable size returns current high-water mark"))
+    (when (~iso rep.routine!alloc (+ before 6))
+      (prn "F - 'new' on primitive arrays increments high-water mark by their (variable) size"))))
+
+(reset)
+(new-trace "new-allocation-chunk")
+(add-code
+  '((function main [
+      (1:integer-address <- new integer:literal)
+     ])))
+; start allocating from address 30, in chunks of 10 locations each
+(= Memory-allocated-until 30
+   Allocation-chunk 10)
+(let routine make-routine!main
+  (assert:is rep.routine!alloc 30)
+  (assert:is rep.routine!alloc-max 40)
+  ; pretend the current chunk is full
+  (= rep.routine!alloc 40)
+  (enq routine running-routines*)
+  (run)
+  (each routine completed-routines*
+    (aif rep.routine!error (prn "error - " it)))
+  (when (~is rep.routine!alloc 41)
+    (prn "F - 'new' can allocate past initial routine memory"))
+  (when (~is rep.routine!alloc-max 50)
+    (prn "F - 'new' updates upper bound for routine memory @rep.routine!alloc-max")))
+
+(reset)
+(new-trace "new-skip")
+(add-code
+  '((function main [
+      (1:integer-boolean-pair-address <- new integer-boolean-pair:literal)
+     ])))
+; start allocating from address 30, in chunks of 10 locations each
+(= Memory-allocated-until 30
+   Allocation-chunk 10)
+(let routine make-routine!main
+  (assert:is rep.routine!alloc 30)
+  (assert:is rep.routine!alloc-max 40)
+  ; pretend the current chunk has just one location left
+  (= rep.routine!alloc 39)
+  (enq routine running-routines*)
+  ; request 2 locations
+  (run)
+  (each routine completed-routines*
+    (aif rep.routine!error (prn "error - " it)))
+  (when (or (~is memory*.1 40)
+            (~is rep.routine!alloc 42)
+            (~is rep.routine!alloc-max 50)
+            (~is Memory-allocated-until 50))
+    (prn "F - 'new' skips past current chunk if insufficient space")))
+
+(reset)
+(new-trace "new-skip-noncontiguous")
+(add-code
+  '((function main [
+      (1:integer-boolean-pair-address <- new integer-boolean-pair:literal)
+     ])))
+; start allocating from address 30, in chunks of 10 locations each
+(= Memory-allocated-until 30
+   Allocation-chunk 10)
+(let routine make-routine!main
+  (assert:is rep.routine!alloc 30)
+  (assert:is rep.routine!alloc-max 40)
+  ; pretend the current chunk has just one location left
+  (= rep.routine!alloc 39)
+  ; pretend we allocated more memory since we created the routine
+  (= Memory-allocated-until 90)
+  (enq routine running-routines*)
+  ; request 2 locations
+  (run)
+  (each routine completed-routines*
+    (aif rep.routine!error (prn "error - " it)))
+  (when (or (~is memory*.1 90)
+            (~is rep.routine!alloc 92)
+            (~is rep.routine!alloc-max 100)
+            (~is Memory-allocated-until 100))
+    (prn "F - 'new' allocates a new chunk if insufficient space")))
+
+(reset)
+(new-trace "new-array-skip-noncontiguous")
+(add-code
+  '((function main [
+      (1:integer-array-address <- new integer-array:literal 4:literal)
+     ])))
+; start allocating from address 30, in chunks of 10 locations each
+(= Memory-allocated-until 30
+   Allocation-chunk 10)
+(let routine make-routine!main
+  (assert:is rep.routine!alloc 30)
+  (assert:is rep.routine!alloc-max 40)
+  ; pretend the current chunk has just one location left
+  (= rep.routine!alloc 39)
+  ; pretend we allocated more memory since we created the routine
+  (= Memory-allocated-until 90)
+  (enq routine running-routines*)
+  ; request 4 locations
+  (run)
+  (each routine completed-routines*
+    (aif rep.routine!error (prn "error - " it)))
+;?   (prn memory*.1) ;? 1
+;?   (prn rep.routine) ;? 1
+;?   (prn Memory-allocated-until) ;? 1
+  (when (or (~is memory*.1 90)
+            (~is rep.routine!alloc 95)
+            (~is rep.routine!alloc-max 100)
+            (~is Memory-allocated-until 100))
+    (prn "F - 'new-array' allocates a new chunk if insufficient space")))
+
+;? (quit) ;? 1
+
+; Even though our memory locations can now have names, the names are all
+; globals, accessible from any function. To isolate functions from their
+; callers we need local variables, and mu provides them using a special
+; variable called default-space. When you initialize such a variable (likely
+; with a call to our just-defined memory allocator) mu interprets memory
+; locations as offsets from its value. If default-space is set to 1000, for
+; example, reads and writes to memory location 1 will really go to 1001.
+;
+; 'default-space' is itself hard-coded to be function-local; it's nil in a new
+; function, and it's restored when functions return to their callers. But the
+; actual space allocation is independent. So you can define closures, or do
+; even more funky things like share locals between two coroutines.
+
+(reset)
+(new-trace "set-default-space")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 2:literal)
+      (1:integer <- copy 23:literal)
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let before rep.routine!alloc
+;?     (set dump-trace*)
+    (run)
+;?     (prn memory*)
+    (when (~and (~is 23 memory*.1)
+                (is 23 (memory* (+ before 2))))
+      (prn "F - default-space implicitly modifies variable locations"))))
+;? (quit)
+
+(reset)
+(new-trace "set-default-space-skips-offset")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 2:literal)
+      (1:integer <- copy 23:offset)
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let before rep.routine!alloc
+;?     (set dump-trace*)
+    (run)
+;?     (prn memory*)
+    (when (~and (~is 23 memory*.1)
+                (is 23 (memory* (+ before 2))))
+      (prn "F - default-space skips 'offset' types just like literals"))))
+
+(reset)
+(new-trace "default-space-bounds-check")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 2:literal)
+      (2:integer <- copy 23:literal)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(let routine (car completed-routines*)
+  (when (no rep.routine!error)
+    (prn "F - default-space checks bounds")))
+
+(reset)
+(new-trace "default-space-and-get-indirect")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 5:literal)
+      (1:integer-boolean-pair-address <- new integer-boolean-pair:literal)
+      (2:integer-address <- get-address 1:integer-boolean-pair-address/deref 0:offset)
+      (2:integer-address/deref <- copy 34:literal)
+      (3:integer/raw <- get 1:integer-boolean-pair-address/deref 0:offset)
+     ])))
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "cvt0" "cvt1")))
+(run 'main)
+;? (prn memory*)
+;? (prn completed-routines*)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~is 34 memory*.3)
+  (prn "F - indirect 'get' works in the presence of default-space"))
+;? (quit)
+
+(reset)
+(new-trace "default-space-and-index-indirect")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 5:literal)
+      (1:integer-array-address <- new integer-array:literal 4:literal)
+      (2:integer-address <- index-address 1:integer-array-address/deref 2:offset)
+      (2:integer-address/deref <- copy 34:literal)
+      (3:integer/raw <- index 1:integer-array-address/deref 2:offset)
+     ])))
+;? (= dump-trace* (obj whitelist '("run" "array-info")))
+(run 'main)
+;? (prn memory*)
+;? (prn completed-routines*)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~is 34 memory*.3)
+  (prn "F - indirect 'index' works in the presence of default-space"))
+;? (quit)
+
+(reset)
+(new-trace "convert-names-default-space")
+(= traces* (queue))
+(when (~iso (convert-names
+              '((((x integer)) <- ((copy)) ((4 literal)))
+                (((y integer)) <- ((copy)) ((2 literal)))
+                ; unsafe in general; don't write random values to 'default-space'
+                (((default-space integer)) <- ((add)) ((x integer)) ((y integer)))))
+            '((((1 integer)) <- ((copy)) ((4 literal)))
+              (((2 integer)) <- ((copy)) ((2 literal)))
+              (((default-space integer)) <- ((add)) ((1 integer)) ((2 integer)))))
+  (prn "F - convert-names never renames default-space"))
+
+(reset)
+(new-trace "suppress-default-space")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 2:literal)
+      (1:integer/raw <- copy 23:literal)
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let before rep.routine!alloc
+;?     (set dump-trace*)
+    (run)
+;?     (prn memory*)
+    (when (~and (is 23 memory*.1)
+                (~is 23 (memory* (+ before 1))))
+      (prn "F - default-space skipped for locations with metadata 'raw'"))))
+;? (quit)
+
+(reset)
+(new-trace "array-copy-indirect-scoped")
+(add-code
+  '((function main [
+      (10:integer <- copy 30:literal)  ; pretend allocation
+      (default-space:space-address <- copy 10:literal)  ; unsafe
+      (1:integer <- copy 2:literal)  ; raw location 12
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer-boolean-pair-array-address <- copy 12:literal)  ; unsafe
+      (7:integer-boolean-pair-array <- copy 6:integer-boolean-pair-array-address/deref)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj whitelist '("run" "mem" "sizeof")))
+(run 'main)
+;? (prn memory*)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~iso memory*.18 2)  ; variable 7
+  (prn "F - indirect array copy in the presence of 'default-space'"))
+;? (quit)
+
+(reset)
+(new-trace "len-array-indirect-scoped")
+(add-code
+  '((function main [
+      (10:integer <- copy 30:literal)  ; pretend allocation
+      (default-space:space-address <- copy 10:literal)  ; unsafe
+      (1:integer <- copy 2:literal)  ; raw location 12
+      (2:integer <- copy 23:literal)
+      (3:boolean <- copy nil:literal)
+      (4:integer <- copy 24:literal)
+      (5:boolean <- copy t:literal)
+      (6:integer-address <- copy 12:literal)  ; unsafe
+      (7:integer <- length 6:integer-boolean-pair-array-address/deref)
+     ])))
+;? (= dump-trace* (obj whitelist '("run" "addr" "sz" "array-len")))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory*.18 2)
+  (prn "F - 'len' accesses length of array address"))
+;? (quit)
+
+(reset)
+(new-trace "default-space-shared")
+(add-code
+  '((function init-counter [
+      (default-space:space-address <- new space:literal 30:literal)
+      (1:integer <- copy 3:literal)  ; initialize to 3
+      (reply default-space:space-address)
+     ])
+    (function increment-counter [
+      (default-space:space-address <- next-input)
+      (1:integer <- add 1:integer 1:literal)  ; increment
+      (reply 1:integer)
+     ])
+    (function main [
+      (1:space-address <- init-counter)
+      (2:integer <- increment-counter 1:space-address)
+      (3:integer <- increment-counter 1:space-address)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn memory*)
+(when (or (~is memory*.2 4)
+          (~is memory*.3 5))
+  (prn "F - multiple calls to a function can share locals"))
+;? (quit)
+
+(reset)
+(new-trace "default-space-closure")
+(add-code
+  '((function init-counter [
+      (default-space:space-address <- new space:literal 30:literal)
+      (1:integer <- copy 3:literal)  ; initialize to 3
+      (reply default-space:space-address)
+     ])
+    (function increment-counter [
+      (default-space:space-address <- new space:literal 30:literal)
+      (0:space-address <- next-input)  ; share outer space
+      (1:integer/space:1 <- add 1:integer/space:1 1:literal)  ; increment
+      (1:integer <- copy 34:literal)  ; dummy
+      (reply 1:integer/space:1)
+     ])
+    (function main [
+      (1:space-address <- init-counter)
+      (2:integer <- increment-counter 1:space-address)
+      (3:integer <- increment-counter 1:space-address)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn memory*)
+(when (or (~is memory*.2 4)
+          (~is memory*.3 5))
+  (prn "F - closures using /space metadata"))
+;? (quit)
+
+(reset)
+(new-trace "default-space-closure-with-names")
+(add-code
+  '((function init-counter [
+      (default-space:space-address <- new space:literal 30:literal)
+      (x:integer <- copy 23:literal)
+      (y:integer <- copy 3:literal)  ; correct copy of y
+      (reply default-space:space-address)
+     ])
+    (function increment-counter [
+      (default-space:space-address <- new space:literal 30:literal)
+      (0:space-address/names:init-counter <- next-input)  ; outer space must be created by 'init-counter' above
+      (y:integer/space:1 <- add y:integer/space:1 1:literal)  ; increment
+      (y:integer <- copy 34:literal)  ; dummy
+      (reply y:integer/space:1)
+     ])
+    (function main [
+      (1:space-address/names:init-counter <- init-counter)
+      (2:integer <- increment-counter 1:space-address/names:init-counter)
+      (3:integer <- increment-counter 1:space-address/names:init-counter)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn memory*)
+(when (or (~is memory*.2 4)
+          (~is memory*.3 5))
+  (prn "F - /names to name variables in outer spaces"))
+;? (quit)
+
+(reset)
+(new-trace "default-space-shared-with-names")
+(add-code
+  '((function f [
+      (default-space:space-address <- new space:literal 30:literal)
+      (x:integer <- copy 3:literal)
+      (y:integer <- copy 4:literal)
+      (reply default-space:space-address)
+     ])
+    (function g [
+      (default-space:space-address/names:f <- next-input)
+      (y:integer <- add y:integer 1:literal)
+      (x:integer <- add x:integer 2:literal)
+      (reply x:integer y:integer)
+     ])
+    (function main [
+      (1:space-address <- f)
+      (2:integer 3:integer <- g 1:space-address)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (or (~is memory*.2 5)
+          (~is memory*.3 5))
+  (prn "F - override names for the default space"))
+
+(reset)
+(new-trace "default-space-shared-with-extra-names")
+(add-code
+  '((function f [
+      (default-space:space-address <- new space:literal 30:literal)
+      (x:integer <- copy 3:literal)
+      (y:integer <- copy 4:literal)
+      (reply default-space:space-address)
+     ])
+    (function g [
+      (default-space:space-address/names:f <- next-input)
+      (y:integer <- add y:integer 1:literal)
+      (x:integer <- add x:integer 2:literal)
+      (z:integer <- add x:integer y:integer)
+      (reply z:integer)
+     ])
+    (function main [
+      (1:space-address <- f)
+      (2:integer <- g 1:space-address)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~is memory*.2 10)
+  (prn "F - shared spaces can add new names"))
+
+(reset)
+(new-trace "default-space-shared-extra-names-dont-overlap-bindings")
+(add-code
+  '((function f [
+      (default-space:space-address <- new space:literal 30:literal)
+      (x:integer <- copy 3:literal)
+      (y:integer <- copy 4:literal)
+      (reply default-space:space-address)
+     ])
+    (function g [
+      (default-space:space-address/names:f <- next-input)
+      (y:integer <- add y:integer 1:literal)
+      (x:integer <- add x:integer 2:literal)
+      (z:integer <- copy 2:literal)
+      (reply x:integer y:integer)
+     ])
+    (function main [
+      (1:space-address <- f)
+      (2:integer 3:integer <- g 1:space-address)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn memory*) ;? 1
+(when (or (~is memory*.2 5)
+          (~is memory*.3 5))
+  (prn "F - new names in shared spaces don't override old ones"))
+;? (quit) ;? 1
+
+)  ; section 20
+
+(section 100
+
+;; Dynamic dispatch
+;
+; Putting it all together, here's how you define generic functions that run
+; different code based on the types of their args.
+
+(reset)
+(new-trace "dispatch-clause")
+;? (set dump-trace*)
+(add-code
+  '((function test1 [
+      ; doesn't matter too much how many locals you allocate space for (here 20)
+      ; if it's slightly too many -- memory is plentiful
+      ; if it's too few -- mu will raise an error
+      (default-space:space-address <- new space:literal 20:literal)
+      (first-arg-box:tagged-value-address <- next-input)
+      ; if given integers, add them
+      { begin
+        (first-arg:integer match?:boolean <- maybe-coerce first-arg-box:tagged-value-address/deref integer:literal)
+        (break-unless match?:boolean)
+        (second-arg-box:tagged-value-address <- next-input)
+        (second-arg:integer <- maybe-coerce second-arg-box:tagged-value-address/deref integer:literal)
+        (result:integer <- add first-arg:integer second-arg:integer)
+        (reply result:integer)
+      }
+      (reply nil:literal)
+     ])
+    (function main [
+      (1:tagged-value-address <- init-tagged-value integer:literal 34:literal)
+      (2:tagged-value-address <- init-tagged-value integer:literal 3:literal)
+      (3:integer <- test1 1:tagged-value-address 2:tagged-value-address)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~is memory*.3 37)
+  (prn "F - an example function that checks that its oarg is an integer"))
+;? (quit)
+
+(reset)
+(new-trace "dispatch-multiple-clauses")
+;? (set dump-trace*)
+(add-code
+  '((function test1 [
+      (default-space:space-address <- new space:literal 20:literal)
+      (first-arg-box:tagged-value-address <- next-input)
+      ; if given integers, add them
+      { begin
+        (first-arg:integer match?:boolean <- maybe-coerce first-arg-box:tagged-value-address/deref integer:literal)
+        (break-unless match?:boolean)
+        (second-arg-box:tagged-value-address <- next-input)
+        (second-arg:integer <- maybe-coerce second-arg-box:tagged-value-address/deref integer:literal)
+        (result:integer <- add first-arg:integer second-arg:integer)
+        (reply result:integer)
+      }
+      ; if given booleans, or them (it's a silly kind of generic function)
+      { begin
+        (first-arg:boolean match?:boolean <- maybe-coerce first-arg-box:tagged-value-address/deref boolean:literal)
+        (break-unless match?:boolean)
+        (second-arg-box:tagged-value-address <- next-input)
+        (second-arg:boolean <- maybe-coerce second-arg-box:tagged-value-address/deref boolean:literal)
+        (result:boolean <- or first-arg:boolean second-arg:boolean)
+        (reply result:integer)
+      }
+      (reply nil:literal)
+     ])
+    (function main [
+      (1:tagged-value-address <- init-tagged-value boolean:literal t:literal)
+      (2:tagged-value-address <- init-tagged-value boolean:literal nil:literal)
+      (3:boolean <- test1 1:tagged-value-address 2:tagged-value-address)
+     ])))
+;? (each stmt function*!test-fn
+;?   (prn "  " stmt))
+(run 'main)
+;? (wipe dump-trace*)
+;? (prn memory*)
+(when (~is memory*.3 t)
+  (prn "F - an example function that can do different things (dispatch) based on the type of its args or oargs"))
+;? (quit)
+
+(reset)
+(new-trace "dispatch-multiple-calls")
+(add-code
+  '((function test1 [
+      (default-space:space-address <- new space:literal 20:literal)
+      (first-arg-box:tagged-value-address <- next-input)
+      ; if given integers, add them
+      { begin
+        (first-arg:integer match?:boolean <- maybe-coerce first-arg-box:tagged-value-address/deref integer:literal)
+        (break-unless match?:boolean)
+        (second-arg-box:tagged-value-address <- next-input)
+        (second-arg:integer <- maybe-coerce second-arg-box:tagged-value-address/deref integer:literal)
+        (result:integer <- add first-arg:integer second-arg:integer)
+        (reply result:integer)
+      }
+      ; if given booleans, or them (it's a silly kind of generic function)
+      { begin
+        (first-arg:boolean match?:boolean <- maybe-coerce first-arg-box:tagged-value-address/deref boolean:literal)
+        (break-unless match?:boolean)
+        (second-arg-box:tagged-value-address <- next-input)
+        (second-arg:boolean <- maybe-coerce second-arg-box:tagged-value-address/deref boolean:literal)
+        (result:boolean <- or first-arg:boolean second-arg:boolean)
+        (reply result:integer)
+      }
+      (reply nil:literal)
+     ])
+    (function main [
+      (1:tagged-value-address <- init-tagged-value boolean:literal t:literal)
+      (2:tagged-value-address <- init-tagged-value boolean:literal nil:literal)
+      (3:boolean <- test1 1:tagged-value-address 2:tagged-value-address)
+      (10:tagged-value-address <- init-tagged-value integer:literal 34:literal)
+      (11:tagged-value-address <- init-tagged-value integer:literal 3:literal)
+      (12:integer <- test1 10:tagged-value-address 11:tagged-value-address)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~and (is memory*.3 t) (is memory*.12 37))
+  (prn "F - different calls can exercise different clauses of the same function"))
+
+; We can also dispatch based on the type of the operands or results at the
+; caller.
+
+(reset)
+(new-trace "dispatch-otype")
+(add-code
+  '((function test1 [
+      (4:type <- otype 0:offset)
+      { begin
+        (5:boolean <- equal 4:type integer:literal)
+        (break-unless 5:boolean)
+        (6:integer <- next-input)
+        (7:integer <- next-input)
+        (8:integer <- add 6:integer 7:integer)
+      }
+      (reply 8:integer)
+     ])
+    (function main [
+      (1:integer <- test1 1:literal 3:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~iso memory*.1 4)
+  (prn "F - an example function that checks that its oarg is an integer"))
+;? (quit)
+
+(reset)
+(new-trace "dispatch-otype-multiple-clauses")
+;? (set dump-trace*)
+(add-code
+  '((function test1 [
+      (4:type <- otype 0:offset)
+      { begin
+        ; integer needed? add args
+        (5:boolean <- equal 4:type integer:literal)
+        (break-unless 5:boolean)
+        (6:integer <- next-input)
+        (7:integer <- next-input)
+        (8:integer <- add 6:integer 7:integer)
+        (reply 8:integer)
+      }
+      { begin
+        ; boolean needed? 'or' args
+        (5:boolean <- equal 4:type boolean:literal)
+        (break-unless 5:boolean 4:offset)
+        (6:boolean <- next-input)
+        (7:boolean <- next-input)
+        (8:boolean <- or 6:boolean 7:boolean)
+        (reply 8:boolean)
+      }])
+    (function main [
+      (1:boolean <- test1 t:literal t:literal)
+     ])))
+;? (each stmt function*!test1
+;?   (prn "  " stmt))
+(run 'main)
+;? (wipe dump-trace*)
+;? (prn memory*)
+(when (~is memory*.1 t)
+  (prn "F - an example function that can do different things (dispatch) based on the type of its args or oargs"))
+;? (quit)
+
+(reset)
+(new-trace "dispatch-otype-multiple-calls")
+(add-code
+  '((function test1 [
+      (4:type <- otype 0:offset)
+      { begin
+        (5:boolean <- equal 4:type integer:literal)
+        (break-unless 5:boolean)
+        (6:integer <- next-input)
+        (7:integer <- next-input)
+        (8:integer <- add 6:integer 7:integer)
+        (reply 8:integer)
+      }
+      { begin
+        (5:boolean <- equal 4:type boolean:literal)
+        (break-unless 5:boolean)
+        (6:boolean <- next-input)
+        (7:boolean <- next-input)
+        (8:boolean <- or 6:boolean 7:boolean)
+        (reply 8:boolean)
+      }])
+    (function main [
+      (1:boolean <- test1 t:literal t:literal)
+      (2:integer <- test1 3:literal 4:literal)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (~and (is memory*.1 t) (is memory*.2 7))
+  (prn "F - different calls can exercise different clauses of the same function"))
+
+)  ; section 100
+
+(section 20
+
+;; Concurrency
+;
+; A rudimentary process scheduler. You can 'run' multiple functions at once,
+; and they share the virtual processor.
+;
+; There's also a 'fork' primitive to let functions create new threads of
+; execution (we call them routines).
+;
+; Eventually we want to allow callers to influence how much of their CPU they
+; give to their 'children', or to rescind a child's running privileges.
+
+(reset)
+(new-trace "scheduler")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 3:literal)
+     ])
+    (function f2 [
+      (2:integer <- copy 4:literal)
+     ])))
+(run 'f1 'f2)
+(when (~iso 2 curr-cycle*)
+  (prn "F - scheduler didn't run the right number of instructions: " curr-cycle*))
+(when (~iso memory* (obj 1 3  2 4))
+  (prn "F - scheduler runs multiple functions: " memory*))
+(check-trace-contents "scheduler orders functions correctly"
+  '(("schedule" "f1")
+    ("schedule" "f2")
+  ))
+(check-trace-contents "scheduler orders schedule and run events correctly"
+  '(("schedule" "f1")
+    ("run" "f1 0")
+    ("schedule" "f2")
+    ("run" "f2 0")
+  ))
+
+(reset)
+(new-trace "scheduler-alternate")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+      (1:integer <- copy 0:literal)
+     ])
+    (function f2 [
+      (2:integer <- copy 0:literal)
+      (2:integer <- copy 0:literal)
+     ])))
+;? (= dump-trace* (obj whitelist '("schedule")))
+(= scheduling-interval* 1)
+(run 'f1 'f2)
+(check-trace-contents "scheduler alternates between routines"
+  '(("run" "f1 0")
+    ("run" "f2 0")
+    ("run" "f1 1")
+    ("run" "f2 1")
+  ))
+
+(reset)
+(new-trace "scheduler-sleep")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])
+    (function f2 [
+      (2:integer <- copy 0:literal)
+     ])))
+; add one baseline routine to run (empty running-routines* handled below)
+(enq make-routine!f1 running-routines*)
+(assert (is 1 len.running-routines*))
+; sleeping routine
+(let routine make-routine!f2
+  (= rep.routine!sleep '(until 23))
+  (set sleeping-routines*.routine))
+; not yet time for it to wake up
+(= curr-cycle* 23)
+;? (set dump-trace*)
+;? (= dump-trace* (obj whitelist '("run" "schedule")))
+(update-scheduler-state)
+(when (~is 1 len.running-routines*)
+  (prn "F - scheduler lets routines sleep"))
+
+(reset)
+(new-trace "scheduler-wakeup")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])
+    (function f2 [
+      (2:integer <- copy 0:literal)
+     ])))
+; add one baseline routine to run (empty running-routines* handled below)
+(enq make-routine!f1 running-routines*)
+(assert (is 1 len.running-routines*))
+; sleeping routine
+(let routine make-routine!f2
+  (= rep.routine!sleep '(until 23))
+  (set sleeping-routines*.routine))
+; time for it to wake up
+(= curr-cycle* 24)
+(update-scheduler-state)
+(when (~is 2 len.running-routines*)
+  (prn "F - scheduler wakes up sleeping routines at the right time"))
+
+(reset)
+(new-trace "scheduler-sleep-location")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])
+    (function f2 [
+      (2:integer <- copy 0:literal)
+     ])))
+; add one baseline routine to run (empty running-routines* handled below)
+(enq make-routine!f1 running-routines*)
+(assert (is 1 len.running-routines*))
+; blocked routine waiting for location 23 to change
+(let routine make-routine!f2
+  (= rep.routine!sleep '(until-location-changes 23 0))
+  (set sleeping-routines*.routine))
+; leave memory location 23 unchanged
+(= memory*.23 0)
+;? (prn memory*)
+;? (prn running-routines*)
+;? (prn sleeping-routines*)
+;? (set dump-trace*)
+;? (= dump-trace* (obj whitelist '("run" "schedule")))
+(update-scheduler-state)
+;? (prn running-routines*)
+;? (prn sleeping-routines*)
+; routine remains blocked
+(when (~is 1 len.running-routines*)
+  (prn "F - scheduler lets routines block on locations"))
+;? (quit)
+
+(reset)
+(new-trace "scheduler-wakeup-location")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])
+    (function f2 [
+      (2:integer <- copy 0:literal)
+     ])))
+; add one baseline routine to run (empty running-routines* handled below)
+(enq make-routine!f1 running-routines*)
+(assert (is 1 len.running-routines*))
+; blocked routine waiting for location 23 to change
+(let routine make-routine!f2
+  (= rep.routine!sleep '(until-location-changes 23 0))
+  (set sleeping-routines*.routine))
+; change memory location 23
+(= memory*.23 1)
+(update-scheduler-state)
+; routine unblocked
+(when (~is 2 len.running-routines*)
+  (prn "F - scheduler unblocks routines blocked on locations"))
+
+(reset)
+(new-trace "scheduler-skip")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])))
+; running-routines* is empty
+(assert (empty running-routines*))
+; sleeping routine
+(let routine make-routine!f1
+  (= rep.routine!sleep '(until 34))
+  (set sleeping-routines*.routine))
+; long time left for it to wake up
+(= curr-cycle* 0)
+(update-scheduler-state)
+;? (prn curr-cycle*)
+(assert (is curr-cycle* 35))
+(when (~is 1 len.running-routines*)
+  (prn "F - scheduler skips ahead to earliest sleeping routines when nothing to run"))
+
+(reset)
+(new-trace "scheduler-deadlock")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])))
+(assert (empty running-routines*))
+(assert (empty completed-routines*))
+; blocked routine
+(let routine make-routine!f1
+  (= rep.routine!sleep '(until-location-changes 23 0))
+  (set sleeping-routines*.routine))
+; location it's waiting on is 'unchanged'
+(= memory*.23 0)
+(update-scheduler-state)
+(assert (~empty completed-routines*))
+;? (prn completed-routines*)
+(let routine completed-routines*.0
+  (when (~posmatch "deadlock" rep.routine!error)
+    (prn "F - scheduler detects deadlock")))
+;? (quit)
+
+(reset)
+(new-trace "scheduler-deadlock2")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])))
+; running-routines* is empty
+(assert (empty running-routines*))
+; blocked routine
+(let routine make-routine!f1
+  (= rep.routine!sleep '(until-location-changes 23 0))
+  (set sleeping-routines*.routine))
+; but is about to become ready
+(= memory*.23 1)
+(update-scheduler-state)
+(when (~empty completed-routines*)
+  (prn "F - scheduler ignores sleeping but ready threads when detecting deadlock"))
+
+; Helper routines are just to sidestep the deadlock test; they stop running
+; when there's no non-helper routines left to run.
+;
+; Be careful not to overuse them. In particular, the component under test
+; should never run in a helper routine; that'll make interrupting and
+; restarting it very brittle.
+(reset)
+(new-trace "scheduler-helper")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])))
+; just a helper routine
+(= routine* make-routine!f1)
+(set rep.routine*!helper)
+;? (= dump-trace* (obj whitelist '("schedule")))
+(update-scheduler-state)
+(when (or (~empty running-routines*) (~empty sleeping-routines*))
+  (prn "F - scheduler stops when there's only helper routines left"))
+
+(reset)
+(new-trace "scheduler-helper-sleeping")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])))
+; just a helper routine
+(let routine make-routine!f1
+  (set rep.routine!helper)
+  (= rep.routine!sleep '(until-location-changes 23 nil))
+  (set sleeping-routines*.routine))
+;? (= dump-trace* (obj whitelist '("schedule")))
+;? (prn "1 " running-routines*)
+;? (prn sleeping-routines*)
+(update-scheduler-state)
+;? (prn "2 " running-routines*)
+;? (prn sleeping-routines*)
+(when (or (~empty running-routines*) (~empty sleeping-routines*))
+  (prn "F - scheduler stops when there's only sleeping helper routines left"))
+
+(reset)
+(new-trace "scheduler-termination")
+(= traces* (queue))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])))
+; all routines done
+(update-scheduler-state)
+(check-trace-doesnt-contain "scheduler helper check shouldn't trigger unless necessary"
+  '(("schedule" "just helpers left")))
+
+; both running and sleeping helpers
+; running helper and sleeping non-helper
+; sleeping helper and running non-helper
+
+(reset)
+(new-trace "scheduler-account-slice")
+; function running an infinite loop
+(add-code
+  '((function f1 [
+      { begin
+        (1:integer <- copy 0:literal)
+        (loop)
+      }
+     ])))
+(let routine make-routine!f1
+  (= rep.routine!limit 10)
+  (enq routine running-routines*))
+(= scheduling-interval* 20)
+(run)
+(when (or (empty completed-routines*)
+          (~is -10 ((rep completed-routines*.0) 'limit)))
+  (prn "F - when given a low cycle limit, a routine runs to end of time slice"))
+
+(reset)
+(new-trace "scheduler-account-slice-multiple")
+; function running an infinite loop
+(add-code
+  '((function f1 [
+      { begin
+        (1:integer <- copy 0:literal)
+        (loop)
+      }
+     ])))
+(let routine make-routine!f1
+  (= rep.routine!limit 100)
+  (enq routine running-routines*))
+(= scheduling-interval* 20)
+(run)
+(when (or (empty completed-routines*)
+          (~is -0 ((rep completed-routines*.0) 'limit)))
+  (prn "F - when given a high limit, a routine successfully stops after multiple time slices"))
+
+(reset)
+(new-trace "scheduler-account-run-while-asleep")
+(add-code
+    ; f1 needs 4 cycles of sleep time, 4 cycles of work
+  '((function f1 [
+      (sleep for-some-cycles:literal 4:literal)
+      (i:integer <- copy 0:literal)
+      (i:integer <- copy 0:literal)
+      (i:integer <- copy 0:literal)
+      (i:integer <- copy 0:literal)
+     ])))
+(let routine make-routine!f1
+  (= rep.routine!limit 6)  ; enough time excluding sleep
+  (enq routine running-routines*))
+(= scheduling-interval* 1)
+;? (= dump-trace* (obj whitelist '("schedule")))
+(run)
+; if time slept counts against limit, routine doesn't have time to complete
+(when (ran-to-completion 'f1)
+  (prn "F - time slept counts against a routine's cycle limit"))
+;? (quit)
+
+(reset)
+(new-trace "scheduler-account-stop-on-preempt")
+(add-code
+  '((function baseline [
+      (i:integer <- copy 0:literal)
+      { begin
+        (done?:boolean <- greater-or-equal i:integer 10:literal)
+        (break-if done?:boolean)
+        (1:integer <- add i:integer 1:literal)
+        (loop)
+      }
+     ])
+    (function f1 [
+      (i:integer <- copy 0:literal)
+      { begin
+        (done?:boolean <- greater-or-equal i:integer 6:literal)
+        (break-if done?:boolean)
+        (1:integer <- add i:integer 1:literal)
+        (loop)
+      }
+     ])))
+(let routine make-routine!baseline
+  (enq routine running-routines*))
+; now add the routine we care about
+(let routine make-routine!f1
+  (= rep.routine!limit 40)  ; less than 2x time f1 needs to complete
+  (enq routine running-routines*))
+(= scheduling-interval* 1)
+; if baseline's time were to count against f1's limit, it wouldn't be able to
+; complete.
+(when (~ran-to-completion 'f1)
+  (prn "F - preempted time doesn't count against a routine's limit"))
+;? (quit)
+
+(reset)
+(new-trace "scheduler-sleep-timeout")
+(add-code
+  '((function baseline [
+      (i:integer <- copy 0:literal)
+      { begin
+        (done?:boolean <- greater-or-equal i:integer 10:literal)
+        (break-if done?:boolean)
+        (1:integer <- add i:integer 1:literal)
+        (loop)
+      }
+     ])
+    (function f1 [
+      (sleep for-some-cycles:literal 10:literal)  ; less time than baseline would take to run
+     ])))
+; add baseline routine to prevent cycle-skipping
+(let routine make-routine!baseline
+  (enq routine running-routines*))
+; now add the routine we care about
+(let routine make-routine!f1
+  (= rep.routine!limit 4)  ; less time than f1 would take to run
+  (enq routine running-routines*))
+(= scheduling-interval* 1)
+;? (= dump-trace* (obj whitelist '("schedule")))
+(run)
+(when (ran-to-completion 'f1)
+  (prn "F - sleeping routines can time out"))
+;? (quit)
+
+(reset)
+(new-trace "sleep")
+(add-code
+  '((function f1 [
+      (sleep for-some-cycles:literal 1:literal)
+      (1:integer <- copy 0:literal)
+      (1:integer <- copy 0:literal)
+     ])
+    (function f2 [
+      (2:integer <- copy 0:literal)
+      (2:integer <- copy 0:literal)
+     ])))
+;? (= dump-trace* (obj whitelist '("run" "schedule")))
+(run 'f1 'f2)
+(check-trace-contents "scheduler handles sleeping routines"
+  '(("run" "f1 0")
+    ("run" "sleeping until 2")
+    ("schedule" "pushing f1 to sleep queue")
+    ("run" "f2 0")
+    ("run" "f2 1")
+    ("schedule" "waking up f1")
+    ("run" "f1 1")
+    ("run" "f1 2")
+  ))
+
+(reset)
+(new-trace "sleep-long")
+(add-code
+  '((function f1 [
+      (sleep for-some-cycles:literal 20:literal)
+      (1:integer <- copy 0:literal)
+      (1:integer <- copy 0:literal)
+     ])
+    (function f2 [
+      (2:integer <- copy 0:literal)
+      (2:integer <- copy 0:literal)
+     ])))
+;? (= dump-trace* (obj whitelist '("run" "schedule")))
+(run 'f1 'f2)
+(check-trace-contents "scheduler progresses sleeping routines when there are no routines left to run"
+  '(("run" "f1 0")
+    ("run" "sleeping until 21")
+    ("schedule" "pushing f1 to sleep queue")
+    ("run" "f2 0")
+    ("run" "f2 1")
+    ("schedule" "waking up f1")
+    ("run" "f1 1")
+    ("run" "f1 2")
+  ))
+
+(reset)
+(new-trace "sleep-location")
+(add-code
+  '((function f1 [
+      ; waits for memory location 1 to be set, before computing its successor
+      (1:integer <- copy 0:literal)
+      (sleep until-location-changes:literal 1:integer)
+      (2:integer <- add 1:integer 1:literal)
+     ])
+    (function f2 [
+      (sleep for-some-cycles:literal 30:literal)
+      (1:integer <- copy 3:literal)  ; set to value
+     ])))
+;? (= dump-trace* (obj whitelist '("run" "schedule")))
+;? (set dump-trace*)
+(run 'f1 'f2)
+;? (prn int-canon.memory*)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~is memory*.2 4)  ; successor of value
+  (prn "F - sleep can block on a memory location"))
+;? (quit)
+
+(reset)
+(new-trace "sleep-scoped-location")
+(add-code
+  '((function f1 [
+      ; waits for memory location 1 to be changed, before computing its successor
+      (10:integer <- copy 5:literal)  ; array of locals
+      (default-space:space-address <- copy 10:literal)
+      (1:integer <- copy 23:literal)  ; really location 12
+      (sleep until-location-changes:literal 1:integer)
+      (2:integer <- add 1:integer 1:literal)
+     ])
+    (function f2 [
+      (sleep for-some-cycles:literal 30:literal)
+      (12:integer <- copy 3:literal)  ; set to value
+     ])))
+;? (= dump-trace* (obj whitelist '("run" "schedule")))
+(run 'f1 'f2)
+(when (~is memory*.13 4)  ; successor of value
+  (prn "F - sleep can block on a scoped memory location"))
+;? (quit)
+
+(reset)
+(new-trace "fork")
+(add-code
+  '((function f1 [
+      (1:integer <- copy 4:literal)
+     ])
+    (function main [
+      (fork f1:fn)
+     ])))
+(run 'main)
+(when (~iso memory*.1 4)
+  (prn "F - fork works"))
+
+(reset)
+(new-trace "fork-returns-id")
+(add-code
+  '((function f1 [
+      (1:integer <- copy 4:literal)
+     ])
+    (function main [
+      (2:integer <- fork f1:fn)
+     ])))
+(run 'main)
+;? (prn memory*)
+(when (no memory*.2)
+  (prn "F - fork returns a pid for the new routine"))
+
+(reset)
+(new-trace "fork-returns-unique-id")
+(add-code
+  '((function f1 [
+      (1:integer <- copy 4:literal)
+     ])
+    (function main [
+      (2:integer <- fork f1:fn)
+      (3:integer <- fork f1:fn)
+     ])))
+(run 'main)
+(when (or (no memory*.2)
+          (no memory*.3)
+          (is memory*.2 memory*.3))
+  (prn "F - fork returns a unique pid everytime"))
+
+(reset)
+(new-trace "fork-with-args")
+(add-code
+  '((function f1 [
+      (2:integer <- next-input)
+     ])
+    (function main [
+      (fork f1:fn nil:literal/globals nil:literal/limit 4:literal)
+     ])))
+(run 'main)
+(when (~iso memory*.2 4)
+  (prn "F - fork can pass args"))
+
+(reset)
+(new-trace "fork-copies-args")
+(add-code
+  '((function f1 [
+      (2:integer <- next-input)
+     ])
+    (function main [
+      (default-space:space-address <- new space:literal 5:literal)
+      (x:integer <- copy 4:literal)
+      (fork f1:fn nil:literal/globals nil:literal/limit x:integer)
+      (x:integer <- copy 0:literal)  ; should be ignored
+     ])))
+(run 'main)
+(when (~iso memory*.2 4)
+  (prn "F - fork passes args by value"))
+
+(reset)
+(new-trace "fork-global")
+(add-code
+  '((function f1 [
+      (1:integer/raw <- copy 2:integer/space:global)
+     ])
+    (function main [
+      (default-space:space-address <- new space:literal 5:literal)
+      (2:integer <- copy 4:literal)
+      (fork f1:fn default-space:space-address/globals nil:literal/limit)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (awhen rep.routine!error (prn "error - " it)))
+(when (~iso memory*.1 4)
+  (prn "F - fork can take a space of global variables to access"))
+
+(reset)
+(new-trace "fork-limit")
+(add-code
+  '((function f1 [
+      { begin
+        (loop)
+      }
+     ])
+    (function main [
+      (fork f1:fn nil:literal/globals 30:literal/limit)
+     ])))
+(= scheduling-interval* 5)
+(run 'main)
+(each routine completed-routines*
+  (awhen rep.routine!error (prn "error - " it)))
+(when (ran-to-completion 'f1)
+  (prn "F - fork can specify a maximum cycle limit"))
+
+(reset)
+(new-trace "fork-then-wait")
+(add-code
+  '((function f1 [
+      { begin
+        (loop)
+      }
+     ])
+    (function main [
+      (1:integer/routine-id <- fork f1:fn nil:literal/globals 30:literal/limit)
+      (sleep until-routine-done:literal 1:integer/routine-id)
+      (2:integer <- copy 34:literal)
+     ])))
+(= scheduling-interval* 5)
+;? (= dump-trace* (obj whitelist '("schedule")))
+(run 'main)
+(each routine completed-routines*
+  (awhen rep.routine!error (prn "error - " it)))
+(check-trace-contents "scheduler orders functions correctly"
+  '(("schedule" "pushing main to sleep queue")
+    ("schedule" "scheduling f1")
+    ("schedule" "ran out of time")
+    ("schedule" "waking up main")
+  ))
+;? (quit)
+
+; todo: Haven't yet written several tests
+;   that restarting a routine works
+;     when it died
+;     when it timed out
+;     when it completed
+;   running multiple routines in tandem
+; first example using these features: read-move-incomplete in chessboard-cursor.arc.t
+
+; The scheduler needs to keep track of the call stack for each routine.
+; Eventually we'll want to save this information in mu's address space itself,
+; along with the types array, the magic buffers for args and oargs, and so on.
+;
+; Eventually we want the right stack-management primitives to build delimited
+; continuations in mu.
+
+; Routines can throw errors.
+(reset)
+(new-trace "array-bounds-check")
+(add-code
+  '((function main [
+      (1:integer <- copy 2:literal)
+      (2:integer <- copy 23:literal)
+      (3:integer <- copy 24:literal)
+      (4:integer <- index 1:integer-array 2:literal)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(let routine (car completed-routines*)
+  (when (no rep.routine!error)
+    (prn "F - 'index' throws an error if out of bounds")))
+
+)  ; section 20
+
+(section 100
+
+;; Synchronization
+;
+; Mu synchronizes using channels rather than locks, like Erlang and Go.
+;
+; The two ends of a channel will usually belong to different routines, but
+; each end should only be used by a single one. Don't try to read from or
+; write to it from multiple routines at once.
+;
+; To avoid locking, writer and reader will never write to the same location.
+; So channels will include fields in pairs, one for the writer and one for the
+; reader.
+
+; The core circular buffer contains values at index 'first-full' up to (but
+; not including) index 'first-empty'. The reader always modifies it at
+; first-full, while the writer always modifies it at first-empty.
+(reset)
+(new-trace "channel-new")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 3:literal)
+      (2:integer <- get 1:channel-address/deref first-full:offset)
+      (3:integer <- get 1:channel-address/deref first-free:offset)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (or (~is 0 memory*.2)
+          (~is 0 memory*.3))
+  (prn "F - 'init-channel' initializes 'first-full and 'first-free to 0"))
+
+(reset)
+(new-trace "channel-write")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 3:literal)
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      (5:integer <- get 1:channel-address/deref first-full:offset)
+      (6:integer <- get 1:channel-address/deref first-free:offset)
+     ])))
+;? (prn function*!write)
+;? (set dump-trace*)
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "array-len" "cvt0" "cvt1")))
+;? (= dump-trace* (obj whitelist '("jump")))
+;? (= dump-trace* (obj whitelist '("run" "reply")))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn canon.memory*)
+(when (or (~is 0 memory*.5)
+          (~is 1 memory*.6))
+  (prn "F - 'write' enqueues item to channel"))
+;? (quit)
+
+(reset)
+(new-trace "channel-read")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 3:literal)
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      (5:tagged-value 1:channel-address/deref <- read 1:channel-address)
+      (7:integer <- maybe-coerce 5:tagged-value integer:literal)
+      (8:integer <- get 1:channel-address/deref first-full:offset)
+      (9:integer <- get 1:channel-address/deref first-free:offset)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "array-len" "cvt0" "cvt1")))
+(run 'main)
+;? (prn int-canon.memory*)
+(when (~is memory*.7 34)
+  (prn "F - 'read' returns written value"))
+(when (or (~is 1 memory*.8)
+          (~is 1 memory*.9))
+  (prn "F - 'read' dequeues item from channel"))
+
+(reset)
+(new-trace "channel-write-wrap")
+(add-code
+  '((function main [
+      ; channel with 1 slot
+      (1:channel-address <- init-channel 1:literal)
+      ; write a value
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      ; first-free will now be 1
+      (5:integer <- get 1:channel-address/deref first-free:offset)
+      ; read one value
+      (_ 1:channel-address/deref <- read 1:channel-address)
+      ; write a second value; verify that first-free wraps around to 0.
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      (6:integer <- get 1:channel-address/deref first-free:offset)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "array-len" "cvt0" "cvt1")))
+(run 'main)
+;? (prn canon.memory*)
+(when (or (~is 1 memory*.5)
+          (~is 0 memory*.6))
+  (prn "F - 'write' can wrap pointer back to start"))
+
+(reset)
+(new-trace "channel-read-wrap")
+(add-code
+  '((function main [
+      ; channel with 1 slot
+      (1:channel-address <- init-channel 1:literal)
+      ; write a value
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      ; read one value
+      (_ 1:channel-address/deref <- read 1:channel-address)
+      ; first-full will now be 1
+      (5:integer <- get 1:channel-address/deref first-full:offset)
+      ; write a second value
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      ; read second value; verify that first-full wraps around to 0.
+      (_ 1:channel-address/deref <- read 1:channel-address)
+      (6:integer <- get 1:channel-address/deref first-full:offset)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj blacklist '("sz" "mem" "addr" "array-len" "cvt0" "cvt1")))
+(run 'main)
+;? (prn canon.memory*)
+(when (or (~is 1 memory*.5)
+          (~is 0 memory*.6))
+  (prn "F - 'read' can wrap pointer back to start"))
+
+(reset)
+(new-trace "channel-new-empty-not-full")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 3:literal)
+      (2:boolean <- empty? 1:channel-address/deref)
+      (3:boolean <- full? 1:channel-address/deref)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (or (~is t memory*.2)
+          (~is nil memory*.3))
+  (prn "F - a new channel is always empty, never full"))
+
+(reset)
+(new-trace "channel-write-not-empty")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 3:literal)
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      (5:boolean <- empty? 1:channel-address/deref)
+      (6:boolean <- full? 1:channel-address/deref)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (or (~is nil memory*.5)
+          (~is nil memory*.6))
+  (prn "F - a channel after writing is never empty"))
+
+(reset)
+(new-trace "channel-write-full")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 1:literal)
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      (5:boolean <- empty? 1:channel-address/deref)
+      (6:boolean <- full? 1:channel-address/deref)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (or (~is nil memory*.5)
+          (~is t memory*.6))
+  (prn "F - a channel after writing may be full"))
+
+(reset)
+(new-trace "channel-read-not-full")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 3:literal)
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      (_ 1:channel-address/deref <- read 1:channel-address)
+      (5:boolean <- empty? 1:channel-address/deref)
+      (6:boolean <- full? 1:channel-address/deref)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (or (~is nil memory*.5)
+          (~is nil memory*.6))
+  (prn "F - a channel after reading is never full"))
+
+(reset)
+(new-trace "channel-read-empty")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 3:literal)
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      (_ 1:channel-address/deref <- read 1:channel-address)
+      (5:boolean <- empty? 1:channel-address/deref)
+      (6:boolean <- full? 1:channel-address/deref)
+     ])))
+;? (set dump-trace*)
+(run 'main)
+;? (prn memory*)
+(when (or (~is t memory*.5)
+          (~is nil memory*.6))
+  (prn "F - a channel after reading may be empty"))
+
+; The key property of channels; writing to a full channel blocks the current
+; routine until it creates space. Ditto reading from an empty channel.
+
+(reset)
+(new-trace "channel-read-block")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 3:literal)
+      ; channel is empty, but receives a read
+      (2:tagged-value 1:channel-address/deref <- read 1:channel-address)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj whitelist '("run" "schedule")))
+(run 'main)
+;? (prn int-canon.memory*)
+;? (prn sleeping-routines*)
+;? (prn completed-routines*)
+; read should cause the routine to sleep, and
+; the sole sleeping routine should trigger the deadlock detector
+(let routine (car completed-routines*)
+  (when (or (no routine)
+            (no rep.routine!error)
+            (~posmatch "deadlock" rep.routine!error))
+    (prn "F - 'read' on empty channel blocks (puts the routine to sleep until the channel gets data)")))
+;? (quit)
+
+(reset)
+(new-trace "channel-write-block")
+(add-code
+  '((function main [
+      (1:channel-address <- init-channel 1:literal)
+      (2:integer <- copy 34:literal)
+      (3:tagged-value <- save-type 2:integer)
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+      ; channel has capacity 1, but receives a second write
+      (1:channel-address/deref <- write 1:channel-address 3:tagged-value)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj whitelist '("run" "schedule" "addr")))
+(run 'main)
+;? (prn int-canon.memory*)
+;? (prn running-routines*)
+;? (prn sleeping-routines*)
+;? (prn completed-routines*)
+; second write should cause the routine to sleep, and
+; the sole sleeping routine should trigger the deadlock detector
+(let routine (car completed-routines*)
+  (when (or (no routine)
+            (no rep.routine!error)
+            (~posmatch "deadlock" rep.routine!error))
+    (prn "F - 'write' on full channel blocks (puts the routine to sleep until the channel gets data)")))
+;? (quit)
+
+(reset)
+(new-trace "channel-handoff")
+(add-code
+  '((function consumer [
+      (default-space:space-address <- new space:literal 30:literal)
+      (chan:channel-address <- init-channel 3:literal)  ; create a channel
+      (fork producer:fn nil:literal/globals nil:literal/limit chan:channel-address)  ; fork a routine to produce a value in it
+      (1:tagged-value/raw <- read chan:channel-address)  ; wait for input on channel
+     ])
+    (function producer [
+      (default-space:space-address <- new space:literal 30:literal)
+      (n:integer <- copy 24:literal)
+      (ochan:channel-address <- next-input)
+      (x:tagged-value <- save-type n:integer)
+      (ochan:channel-address/deref <- write ochan:channel-address x:tagged-value)
+     ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj whitelist '("schedule" "run" "addr")))
+;? (= dump-trace* (obj whitelist '("-")))
+(run 'consumer)
+;? (prn memory*)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~is 24 memory*.2)  ; location 1 contains tagged-value x above
+  (prn "F - channels are meant to be shared between routines"))
+;? (quit)
+
+(reset)
+(new-trace "channel-handoff-routine")
+(add-code
+  '((function consumer [
+      (default-space:space-address <- new space:literal 30:literal)
+      (1:channel-address <- init-channel 3:literal)  ; create a channel
+      (fork producer:fn default-space:space-address/globals nil:literal/limit)  ; pass it as a global to another routine
+      (1:tagged-value/raw <- read 1:channel-address)  ; wait for input on channel
+     ])
+    (function producer [
+      (default-space:space-address <- new space:literal 30:literal)
+      (n:integer <- copy 24:literal)
+      (x:tagged-value <- save-type n:integer)
+      (1:channel-address/space:global/deref <- write 1:channel-address/space:global x:tagged-value)
+     ])))
+(run 'consumer)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~is 24 memory*.2)  ; location 1 contains tagged-value x above
+  (prn "F - channels are meant to be shared between routines"))
+
+)  ; section 100
+
+(section 10
+
+;; Separating concerns
+;
+; Lightweight tools can also operate on quoted lists of statements surrounded
+; by square brackets. In the example below, we mimic Go's 'defer' keyword
+; using 'convert-quotes'. It lets us write code anywhere in a function, but
+; have it run just before the function exits. Great for keeping code to
+; reclaim memory or other resources close to the code to allocate it. (C++
+; programmers know this as RAII.) We'll use 'defer' when we build a memory
+; deallocation routine like C's 'free'.
+;
+; More powerful reorderings are also possible like in Literate Programming or
+; Aspect-Oriented Programming; one advantage of prohibiting arbitrarily nested
+; code is that we can naturally name 'join points' wherever we want.
+
+(reset)
+(new-trace "convert-quotes-defer")
+(= traces* (queue))
+(when (~iso (convert-quotes
+              '((1:integer <- copy 4:literal)
+                (defer [
+                         (3:integer <- copy 6:literal)
+                       ])
+                (2:integer <- copy 5:literal)))
+            '((1:integer <- copy 4:literal)
+              (2:integer <- copy 5:literal)
+              (3:integer <- copy 6:literal)))
+  (prn "F - convert-quotes can handle 'defer'"))
+
+(reset)
+(new-trace "convert-quotes-defer-reply")
+(= traces* (queue))
+(when (~iso (convert-quotes
+              '((1:integer <- copy 0:literal)
+                (defer [
+                         (5:integer <- copy 0:literal)
+                       ])
+                (2:integer <- copy 0:literal)
+                (reply)
+                (3:integer <- copy 0:literal)
+                (4:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              (2:integer <- copy 0:literal)
+              (5:integer <- copy 0:literal)
+              (reply)
+              (3:integer <- copy 0:literal)
+              (4:integer <- copy 0:literal)
+              (5:integer <- copy 0:literal)))
+  (prn "F - convert-quotes inserts code at early exits"))
+
+(reset)
+(new-trace "convert-quotes-defer-reply-arg")
+(= traces* (queue))
+(when (~iso (convert-quotes
+              '((1:integer <- copy 0:literal)
+                (defer [
+                         (5:integer <- copy 0:literal)
+                       ])
+                (2:integer <- copy 0:literal)
+                (reply 2:literal)
+                (3:integer <- copy 0:literal)
+                (4:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              (2:integer <- copy 0:literal)
+              (prepare-reply 2:literal)
+              (5:integer <- copy 0:literal)
+              (reply)
+              (3:integer <- copy 0:literal)
+              (4:integer <- copy 0:literal)
+              (5:integer <- copy 0:literal)))
+  (prn "F - convert-quotes inserts code at early exits"))
+
+(reset)
+(new-trace "convert-quotes-label")
+(= traces* (queue))
+(when (~iso (convert-quotes
+              '((1:integer <- copy 4:literal)
+                foo
+                (2:integer <- copy 5:literal)))
+            '((1:integer <- copy 4:literal)
+              foo
+              (2:integer <- copy 5:literal)))
+  (prn "F - convert-quotes can handle labels"))
+
+(reset)
+(new-trace "before")
+(= traces* (queue))
+(add-code
+  '((before label1 [
+     (2:integer <- copy 0:literal)
+    ])))
+(when (~iso (as cons before*!label1)
+            '(; fragment
+              (
+                (2:integer <- copy 0:literal))))
+  (prn "F - 'before' records fragments of code to insert before labels"))
+
+(when (~iso (insert-code
+              '((1:integer <- copy 0:literal)
+                label1
+                (3:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              (2:integer <- copy 0:literal)
+              label1
+              (3:integer <- copy 0:literal)))
+  (prn "F - 'insert-code' can insert fragments before labels"))
+
+(reset)
+(new-trace "before-multiple")
+(= traces* (queue))
+(add-code
+  '((before label1 [
+      (2:integer <- copy 0:literal)
+     ])
+    (before label1 [
+      (3:integer <- copy 0:literal)
+     ])))
+(when (~iso (as cons before*!label1)
+            '(; fragment
+              (
+                (2:integer <- copy 0:literal))
+              (
+                (3:integer <- copy 0:literal))))
+  (prn "F - 'before' records fragments in order"))
+
+(when (~iso (insert-code
+              '((1:integer <- copy 0:literal)
+                label1
+                (4:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              (2:integer <- copy 0:literal)
+              (3:integer <- copy 0:literal)
+              label1
+              (4:integer <- copy 0:literal)))
+  (prn "F - 'insert-code' can insert multiple fragments in order before label"))
+
+(reset)
+(new-trace "before-scoped")
+(= traces* (queue))
+(add-code
+  '((before f/label1 [  ; label1 only inside function f
+     (2:integer <- copy 0:literal)
+    ])))
+(when (~iso (insert-code
+              '((1:integer <- copy 0:literal)
+                label1
+                (3:integer <- copy 0:literal))
+              'f)
+            '((1:integer <- copy 0:literal)
+              (2:integer <- copy 0:literal)
+              label1
+              (3:integer <- copy 0:literal)))
+  (prn "F - 'insert-code' can insert fragments before labels just in specified functions"))
+
+(reset)
+(new-trace "before-scoped2")
+(= traces* (queue))
+(add-code
+  '((before f/label1 [  ; label1 only inside function f
+      (2:integer <- copy 0:literal)
+     ])))
+(when (~iso (insert-code
+              '((1:integer <- copy 0:literal)
+                label1
+                (3:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              label1
+              (3:integer <- copy 0:literal)))
+  (prn "F - 'insert-code' ignores labels not in specified functions"))
+
+(reset)
+(new-trace "after")
+(= traces* (queue))
+(add-code
+  '((after label1 [
+      (2:integer <- copy 0:literal)
+     ])))
+(when (~iso (as cons after*!label1)
+            '(; fragment
+              (
+                (2:integer <- copy 0:literal))))
+  (prn "F - 'after' records fragments of code to insert after labels"))
+
+(when (~iso (insert-code
+              '((1:integer <- copy 0:literal)
+                label1
+                (3:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              label1
+              (2:integer <- copy 0:literal)
+              (3:integer <- copy 0:literal)))
+  (prn "F - 'insert-code' can insert fragments after labels"))
+
+(reset)
+(new-trace "after-multiple")
+(= traces* (queue))
+(add-code
+  '((after label1 [
+      (2:integer <- copy 0:literal)
+     ])
+    (after label1 [
+      (3:integer <- copy 0:literal)
+     ])))
+(when (~iso (as cons after*!label1)
+            '(; fragment
+              (
+                (3:integer <- copy 0:literal))
+              (
+                (2:integer <- copy 0:literal))))
+  (prn "F - 'after' records fragments in *reverse* order"))
+
+(when (~iso (insert-code
+              '((1:integer <- copy 0:literal)
+                label1
+                (4:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              label1
+              (3:integer <- copy 0:literal)
+              (2:integer <- copy 0:literal)
+              (4:integer <- copy 0:literal)))
+  (prn "F - 'insert-code' can insert multiple fragments in order after label"))
+
+(reset)
+(new-trace "before-after")
+(= traces* (queue))
+(add-code
+  '((before label1 [
+      (2:integer <- copy 0:literal)
+     ])
+    (after label1 [
+      (3:integer <- copy 0:literal)
+     ])))
+(when (and (~iso (as cons before*!label1)
+                 '(; fragment
+                   (
+                     (2:integer <- copy 0:literal))))
+           (~iso (as cons after*!label1)
+                 '(; fragment
+                   (
+                     (3:integer <- copy 0:literal)))))
+  (prn "F - 'before' and 'after' fragments work together"))
+
+(when (~iso (insert-code
+              '((1:integer <- copy 0:literal)
+                label1
+                (4:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              (2:integer <- copy 0:literal)
+              label1
+              (3:integer <- copy 0:literal)
+              (4:integer <- copy 0:literal)))
+  (prn "F - 'insert-code' can insert multiple fragments around label"))
+
+(reset)
+(new-trace "before-after-multiple")
+(= traces* (queue))
+(add-code
+  '((before label1 [
+      (2:integer <- copy 0:literal)
+      (3:integer <- copy 0:literal)
+     ])
+    (after label1 [
+      (4:integer <- copy 0:literal)
+     ])
+    (before label1 [
+      (5:integer <- copy 0:literal)
+     ])
+    (after label1 [
+      (6:integer <- copy 0:literal)
+      (7:integer <- copy 0:literal)
+     ])))
+(when (or (~iso (as cons before*!label1)
+                '(; fragment
+                  (
+                    (2:integer <- copy 0:literal)
+                    (3:integer <- copy 0:literal))
+                  (
+                    (5:integer <- copy 0:literal))))
+          (~iso (as cons after*!label1)
+                '(; fragment
+                  (
+                    (6:integer <- copy 0:literal)
+                    (7:integer <- copy 0:literal))
+                  (
+                    (4:integer <- copy 0:literal)))))
+  (prn "F - multiple 'before' and 'after' fragments at once"))
+
+(when (~iso (insert-code
+              '((1:integer <- copy 0:literal)
+                label1
+                (8:integer <- copy 0:literal)))
+            '((1:integer <- copy 0:literal)
+              (2:integer <- copy 0:literal)
+              (3:integer <- copy 0:literal)
+              (5:integer <- copy 0:literal)
+              label1
+              (6:integer <- copy 0:literal)
+              (7:integer <- copy 0:literal)
+              (4:integer <- copy 0:literal)
+              (8:integer <- copy 0:literal)))
+  (prn "F - 'insert-code' can insert multiple fragments around label - 2"))
+
+(reset)
+(new-trace "before-after-independent")
+(= traces* (queue))
+(when (~iso (do
+              (reset)
+              (add-code
+                '((before label1 [
+                    (2:integer <- copy 0:literal)
+                   ])
+                  (after label1 [
+                    (3:integer <- copy 0:literal)
+                   ])
+                  (before label1 [
+                    (4:integer <- copy 0:literal)
+                   ])
+                  (after label1 [
+                    (5:integer <- copy 0:literal)
+                   ])))
+              (list before*!label1 after*!label1))
+            (do
+              (reset)
+              (add-code
+                '((before label1 [
+                    (2:integer <- copy 0:literal)
+                   ])
+                  (before label1 [
+                    (4:integer <- copy 0:literal)
+                   ])
+                  (after label1 [
+                    (3:integer <- copy 0:literal)
+                   ])
+                  (after label1 [
+                    (5:integer <- copy 0:literal)
+                   ])))
+              (list before*!label1 after*!label1)))
+  (prn "F - order matters between 'before' and between 'after' fragments, but not *across* 'before' and 'after' fragments"))
+
+(reset)
+(new-trace "before-after-braces")
+(= traces* (queue))
+(= function* (table))
+(add-code
+  '((after label1 [
+      (1:integer <- copy 0:literal)
+     ])
+    (function f1 [
+      { begin
+        label1
+      }
+     ])))
+;? (= dump-trace* (obj whitelist '("cn0")))
+(freeze function*)
+(when (~iso function*!f1
+            '(label1
+              (((1 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - before/after works inside blocks"))
+
+(reset)
+(new-trace "before-after-any-order")
+(= traces* (queue))
+(= function* (table))
+(add-code
+  '((function f1 [
+      { begin
+        label1
+      }
+     ])
+    (after label1 [
+       (1:integer <- copy 0:literal)
+     ])))
+(freeze function*)
+(when (~iso function*!f1
+            '(label1
+              (((1 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - before/after can come after the function they need to modify"))
+;? (quit)
+
+(reset)
+(new-trace "multiple-defs")
+(= traces* (queue))
+(= function* (table))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])
+    (function f1 [
+      (2:integer <- copy 0:literal)
+     ])))
+(freeze function*)
+(when (~iso function*!f1
+            '((((2 integer)) <- ((copy)) ((0 literal)))
+              (((1 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - multiple 'def' of the same function add clauses"))
+
+(reset)
+(new-trace "def!")
+(= traces* (queue))
+(= function* (table))
+(add-code
+  '((function f1 [
+      (1:integer <- copy 0:literal)
+     ])
+    (function! f1 [
+      (2:integer <- copy 0:literal)
+     ])))
+(freeze function*)
+(when (~iso function*!f1
+            '((((2 integer)) <- ((copy)) ((0 literal)))))
+  (prn "F - 'def!' clears all previous clauses"))
+
+)  ; section 10
+
+;; ---
+
+(section 100
+
+; String utilities
+
+(reset)
+(new-trace "string-new")
+(add-code
+  '((function main [
+      (1:string-address <- new string:literal 5:literal)
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let before rep.routine!alloc
+    (run)
+    (when (~iso rep.routine!alloc (+ before 5 1))
+      (prn "F - 'new' allocates arrays of bytes for strings"))))
+
+; Convenience: initialize strings using string literals
+(reset)
+(new-trace "string-literal")
+(add-code
+  '((function main [
+      (1:string-address <- new "hello")
+     ])))
+(let routine make-routine!main
+  (enq routine running-routines*)
+  (let before rep.routine!alloc
+;?     (set dump-trace*)
+;?     (= dump-trace* (obj whitelist '("schedule" "run" "addr")))
+    (run)
+    (when (~iso rep.routine!alloc (+ before 5 1))
+      (prn "F - 'new' allocates arrays of bytes for string literals"))
+    (when (~memory-contains-array before "hello")
+      (prn "F - 'new' initializes allocated memory to string literal"))))
+
+(reset)
+(new-trace "string-equal")
+(add-code
+  '((function main [
+      (1:string-address <- new "hello")
+      (2:string-address <- new "hello")
+      (3:boolean <- string-equal 1:string-address 2:string-address)
+     ])))
+(run 'main)
+(when (~iso memory*.3 t)
+  (prn "F - 'string-equal'"))
+
+(reset)
+(new-trace "string-equal-empty")
+(add-code
+  '((function main [
+      (1:string-address <- new "")
+      (2:string-address <- new "")
+      (3:boolean <- string-equal 1:string-address 2:string-address)
+     ])))
+(run 'main)
+(when (~iso memory*.3 t)
+  (prn "F - 'string-equal' works on empty strings"))
+
+(reset)
+(new-trace "string-equal-compare-with-empty")
+(add-code
+  '((function main [
+      (1:string-address <- new "a")
+      (2:string-address <- new "")
+      (3:boolean <- string-equal 1:string-address 2:string-address)
+     ])))
+(run 'main)
+(when (~iso memory*.3 nil)
+  (prn "F - 'string-equal' compares correctly with empty strings"))
+
+(reset)
+(new-trace "string-equal-compares-length")
+(add-code
+  '((function main [
+      (1:string-address <- new "a")
+      (2:string-address <- new "ab")
+      (3:boolean <- string-equal 1:string-address 2:string-address)
+     ])))
+(run 'main)
+(when (~iso memory*.3 nil)
+  (prn "F - 'string-equal' handles differing lengths"))
+
+(reset)
+(new-trace "string-equal-compares-initial-element")
+(add-code
+  '((function main [
+      (1:string-address <- new "aa")
+      (2:string-address <- new "ba")
+      (3:boolean <- string-equal 1:string-address 2:string-address)
+     ])))
+(run 'main)
+(when (~iso memory*.3 nil)
+  (prn "F - 'string-equal' handles inequal final byte"))
+
+(reset)
+(new-trace "string-equal-compares-final-element")
+(add-code
+  '((function main [
+      (1:string-address <- new "ab")
+      (2:string-address <- new "aa")
+      (3:boolean <- string-equal 1:string-address 2:string-address)
+     ])))
+(run 'main)
+(when (~iso memory*.3 nil)
+  (prn "F - 'string-equal' handles inequal final byte"))
+
+(reset)
+(new-trace "string-equal-reflexive")
+(add-code
+  '((function main [
+      (1:string-address <- new "ab")
+      (3:boolean <- string-equal 1:string-address 1:string-address)
+     ])))
+(run 'main)
+(when (~iso memory*.3 t)
+  (prn "F - 'string-equal' handles identical pointer"))
+
+(reset)
+(new-trace "strcat")
+(add-code
+  '((function main [
+      (1:string-address <- new "hello,")
+      (2:string-address <- new " world!")
+      (3:string-address <- strcat 1:string-address 2:string-address)
+     ])))
+;? (= dump-trace* (obj whitelist '("run"))) ;? 1
+(run 'main)
+(when (~memory-contains-array memory*.3 "hello, world!")
+  (prn "F - 'strcat' concatenates strings"))
+;? (quit) ;? 1
+
+(reset)
+(new-trace "interpolate")
+(add-code
+  '((function main [
+      (1:string-address <- new "hello, _!")
+      (2:string-address <- new "abc")
+      (3:string-address <- interpolate 1:string-address 2:string-address)
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(when (~memory-contains-array memory*.3 "hello, abc!")
+  (prn "F - 'interpolate' splices strings"))
+
+(reset)
+(new-trace "interpolate-empty")
+(add-code
+  '((function main [
+      (1:string-address <- new "hello!")
+      (2:string-address <- new "abc")
+      (3:string-address <- interpolate 1:string-address 2:string-address)
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(when (~memory-contains-array memory*.3 "hello!")
+  (prn "F - 'interpolate' without underscore returns template"))
+
+(reset)
+(new-trace "interpolate-at-start")
+(add-code
+  '((function main [
+      (1:string-address <- new "_, hello!")
+      (2:string-address <- new "abc")
+      (3:string-address <- interpolate 1:string-address 2:string-address)
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(when (~memory-contains-array memory*.3 "abc, hello")
+  (prn "F - 'interpolate' splices strings at start"))
+
+(reset)
+(new-trace "interpolate-at-end")
+(add-code
+  '((function main [
+      (1:string-address <- new "hello, _")
+      (2:string-address <- new "abc")
+      (3:string-address <- interpolate 1:string-address 2:string-address)
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(when (~memory-contains-array memory*.3 "hello, abc")
+  (prn "F - 'interpolate' splices strings at start"))
+
+(reset)
+(new-trace "interpolate-varargs")
+(add-code
+  '((function main [
+      (1:string-address <- new "hello, _, _, and _!")
+      (2:string-address <- new "abc")
+      (3:string-address <- new "def")
+      (4:string-address <- new "ghi")
+      (5:string-address <- interpolate 1:string-address 2:string-address 3:string-address 4:string-address)
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+;? (= dump-trace* (obj whitelist '("run" "array-info")))
+;? (set dump-trace*)
+(run 'main)
+;? (quit)
+;? (up i 1 (+ 1 (memory* memory*.5))
+;?   (prn (memory* (+ memory*.5 i))))
+(when (~memory-contains-array memory*.5 "hello, abc, def, and ghi!")
+  (prn "F - 'interpolate' splices in any number of strings"))
+
+(reset)
+(new-trace "string-find-next")
+(add-code
+  '((function main [
+      (1:string-address <- new "a/b")
+      (2:integer <- find-next 1:string-address ((#\/ literal)) 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.2 1)
+  (prn "F - 'find-next' finds first location of a character"))
+
+(reset)
+(new-trace "string-find-next-empty")
+(add-code
+  '((function main [
+      (1:string-address <- new "")
+      (2:integer <- find-next 1:string-address ((#\/ literal)) 0:literal)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~is memory*.2 0)
+  (prn "F - 'find-next' finds first location of a character"))
+
+(reset)
+(new-trace "string-find-next-initial")
+(add-code
+  '((function main [
+      (1:string-address <- new "/abc")
+      (2:integer <- find-next 1:string-address ((#\/ literal)) 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.2 0)
+  (prn "F - 'find-next' handles prefix match"))
+
+(reset)
+(new-trace "string-find-next-final")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc/")
+      (2:integer <- find-next 1:string-address ((#\/ literal)) 0:literal)
+     ])))
+(run 'main)
+;? (prn memory*.2)
+(when (~is memory*.2 3)
+  (prn "F - 'find-next' handles suffix match"))
+
+(reset)
+(new-trace "string-find-next-missing")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:integer <- find-next 1:string-address ((#\/ literal)) 0:literal)
+     ])))
+(run 'main)
+;? (prn memory*.2)
+(when (~is memory*.2 3)
+  (prn "F - 'find-next' handles no match"))
+
+(reset)
+(new-trace "string-find-next-invalid-index")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:integer <- find-next 1:string-address ((#\/ literal)) 4:literal)
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn memory*.2)
+(when (~is memory*.2 4)
+  (prn "F - 'find-next' skips invalid index (past end of string)"))
+
+(reset)
+(new-trace "string-find-next-first")
+(add-code
+  '((function main [
+      (1:string-address <- new "ab/c/")
+      (2:integer <- find-next 1:string-address ((#\/ literal)) 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.2 2)
+  (prn "F - 'find-next' finds first of multiple options"))
+
+(reset)
+(new-trace "string-find-next-second")
+(add-code
+  '((function main [
+      (1:string-address <- new "ab/c/")
+      (2:integer <- find-next 1:string-address ((#\/ literal)) 3:literal)
+     ])))
+(run 'main)
+(when (~is memory*.2 4)
+  (prn "F - 'find-next' finds second of multiple options"))
+
+(reset)
+(new-trace "match-at")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- new "ab")
+      (3:boolean <- match-at 1:string-address 2:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 t)
+  (prn "F - 'match-at' matches substring at given index"))
+
+(reset)
+(new-trace "match-at-reflexive")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (3:boolean <- match-at 1:string-address 1:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 t)
+  (prn "F - 'match-at' always matches a string at itself at index 0"))
+
+(reset)
+(new-trace "match-at-outside-bounds")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- new "a")
+      (3:boolean <- match-at 1:string-address 2:string-address 4:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 nil)
+  (prn "F - 'match-at' always fails to match outside the bounds of the text"))
+
+(reset)
+(new-trace "match-at-empty-pattern")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- new "")
+      (3:boolean <- match-at 1:string-address 2:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 t)
+  (prn "F - 'match-at' always matches empty pattern"))
+
+(reset)
+(new-trace "match-at-empty-pattern-outside-bounds")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- new "")
+      (3:boolean <- match-at 1:string-address 2:string-address 4:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 nil)
+  (prn "F - 'match-at' matches empty pattern -- unless index is out of bounds"))
+
+(reset)
+(new-trace "match-at-empty-text")
+(add-code
+  '((function main [
+      (1:string-address <- new "")
+      (2:string-address <- new "abc")
+      (3:boolean <- match-at 1:string-address 2:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 nil)
+  (prn "F - 'match-at' never matches empty text"))
+
+(reset)
+(new-trace "match-at-empty-against-empty")
+(add-code
+  '((function main [
+      (1:string-address <- new "")
+      (3:boolean <- match-at 1:string-address 1:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 t)
+  (prn "F - 'match-at' never matches empty text -- unless pattern is also empty"))
+
+(reset)
+(new-trace "match-at-inside-bounds")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- new "bc")
+      (3:boolean <- match-at 1:string-address 2:string-address 1:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 t)
+  (prn "F - 'match-at' matches inner substring"))
+
+(reset)
+(new-trace "match-at-inside-bounds-2")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- new "bc")
+      (3:boolean <- match-at 1:string-address 2:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 nil)
+  (prn "F - 'match-at' matches inner substring - 2"))
+
+(reset)
+(new-trace "find-substring")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- new "bc")
+      (3:integer <- find-substring 1:string-address 2:string-address 0:literal)
+     ])))
+(run 'main)
+;? (prn memory*.3) ;? 1
+(when (~is memory*.3 1)
+  (prn "F - 'find-substring' returns index of match"))
+
+(reset)
+(new-trace "find-substring-2")
+(add-code
+  '((function main [
+      (1:string-address <- new "abcd")
+      (2:string-address <- new "bc")
+      (3:integer <- find-substring 1:string-address 2:string-address 1:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 1)
+  (prn "F - 'find-substring' returns provided index if it matches"))
+
+(reset)
+(new-trace "find-substring-no-match")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- new "bd")
+      (3:integer <- find-substring 1:string-address 2:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 3)
+  (prn "F - 'find-substring' returns out-of-bounds index on no-match"))
+
+(reset)
+(new-trace "find-substring-suffix-match")
+(add-code
+  '((function main [
+      (1:string-address <- new "abcd")
+      (2:string-address <- new "cd")
+      (3:integer <- find-substring 1:string-address 2:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 2)
+  (prn "F - 'find-substring' returns provided index if it matches"))
+
+(reset)
+(new-trace "find-substring-suffix-match-2")
+(add-code
+  '((function main [
+      (1:string-address <- new "abcd")
+      (2:string-address <- new "cde")
+      (3:integer <- find-substring 1:string-address 2:string-address 0:literal)
+     ])))
+(run 'main)
+(when (~is memory*.3 4)
+  (prn "F - 'find-substring' returns provided index if it matches"))
+
+;? (quit) ;? 1
+
+(reset)
+(new-trace "string-split")
+(add-code
+  '((function main [
+      (1:string-address <- new "a/b")
+      (2:string-address-array-address <- split 1:string-address ((#\/ literal)))
+     ])))
+;? (set dump-trace*)
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(let base memory*.2
+;?   (prn base " " memory*.base)
+  (when (or (~is memory*.base 2)
+;?             (do1 nil prn.111)
+            (~memory-contains-array (memory* (+ base 1)) "a")
+;?             (do1 nil prn.111)
+            (~memory-contains-array (memory* (+ base 2)) "b"))
+    (prn "F - 'split' cuts string at delimiter")))
+
+(reset)
+(new-trace "string-split2")
+(add-code
+  '((function main [
+      (1:string-address <- new "a/b/c")
+      (2:string-address-array-address <- split 1:string-address ((#\/ literal)))
+     ])))
+;? (set dump-trace*)
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(let base memory*.2
+;?   (prn base " " memory*.base)
+  (when (or (~is memory*.base 3)
+;?             (do1 nil prn.111)
+            (~memory-contains-array (memory* (+ base 1)) "a")
+;?             (do1 nil prn.111)
+            (~memory-contains-array (memory* (+ base 2)) "b")
+;?             (do1 nil prn.111)
+            (~memory-contains-array (memory* (+ base 3)) "c"))
+    (prn "F - 'split' cuts string at two delimiters")))
+
+(reset)
+(new-trace "string-split-missing")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address-array-address <- split 1:string-address ((#\/ literal)))
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(let base memory*.2
+  (when (or (~is memory*.base 1)
+            (~memory-contains-array (memory* (+ base 1)) "abc"))
+    (prn "F - 'split' handles missing delimiter")))
+
+(reset)
+(new-trace "string-split-empty")
+(add-code
+  '((function main [
+      (1:string-address <- new "")
+      (2:string-address-array-address <- split 1:string-address ((#\/ literal)))
+     ])))
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(let base memory*.2
+;?   (prn base " " memory*.base)
+  (when (~is memory*.base 0)
+    (prn "F - 'split' handles empty string")))
+
+(reset)
+(new-trace "string-split-empty-piece")
+(add-code
+  '((function main [
+      (1:string-address <- new "a/b//c")
+      (2:string-address-array-address <- split 1:string-address ((#\/ literal)))
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(let base memory*.2
+  (when (or (~is memory*.base 4)
+            (~memory-contains-array (memory* (+ base 1)) "a")
+            (~memory-contains-array (memory* (+ base 2)) "b")
+            (~memory-contains-array (memory* (+ base 3)) "")
+            (~memory-contains-array (memory* (+ base 4)) "c"))
+    (prn "F - 'split' cuts string at two delimiters")))
+;? (quit) ;? 1
+
+(reset)
+(new-trace "string-split-first")
+(add-code
+  '((function main [
+      (1:string-address <- new "a/b")
+      (2:string-address 3:string-address <- split-first 1:string-address ((#\/ literal)))
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (or (~memory-contains-array memory*.2 "a")
+          (~memory-contains-array memory*.3 "b"))
+  (prn "F - 'split-first' cuts string at first occurrence of delimiter"))
+
+(reset)
+(new-trace "string-split-first-at-substring")
+(add-code
+  '((function main [
+      (1:string-address <- new "a//b")
+      (2:string-address <- new "//")
+      (3:string-address 4:string-address <- split-first-at-substring 1:string-address 2:string-address)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn int-canon.memory*) ;? 1
+(when (or (~memory-contains-array memory*.3 "a")
+          (~memory-contains-array memory*.4 "b"))
+  (prn "F - 'split-first-at-substring' is like split-first but with a string delimiter"))
+
+(reset)
+(new-trace "string-copy")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- string-copy 1:string-address 1:literal 3:literal)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~memory-contains-array memory*.2 "bc")
+  (prn "F - 'string-copy' returns a copy of a substring"))
+
+(reset)
+(new-trace "string-copy-out-of-bounds")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- string-copy 1:string-address 2:literal 4:literal)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~memory-contains-array memory*.2 "c")
+  (prn "F - 'string-copy' stops at bounds"))
+
+(reset)
+(new-trace "string-copy-out-of-bounds-2")
+(add-code
+  '((function main [
+      (1:string-address <- new "abc")
+      (2:string-address <- string-copy 1:string-address 3:literal 3:literal)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+(when (~memory-contains-array memory*.2 "")
+  (prn "F - 'string-copy' returns empty string when range is out of bounds"))
+
+(reset)
+(new-trace "min")
+(add-code
+  '((function main [
+      (1:integer <- min 3:literal 4:literal)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (aif rep.routine!error (prn "error - " it)))
+;? (prn int-canon.memory*) ;? 1
+(when (~is memory*.1 3)
+  (prn "F - 'min' returns smaller of two numbers"))
+
+;? (quit) ;? 2
+
+(reset)
+(new-trace "integer-to-decimal-string")
+(add-code
+  '((function main [
+      (1:string-address/raw <- integer-to-decimal-string 34:literal)
+    ])))
+;? (set dump-trace*)
+;? (= dump-trace* (obj whitelist '("run")))
+(run 'main)
+(let base memory*.1
+  (when (~memory-contains-array base "34")
+    (prn "F - converting integer to decimal string")))
+
+(reset)
+(new-trace "integer-to-decimal-string-zero")
+(add-code
+  '((function main [
+      (1:string-address/raw <- integer-to-decimal-string 0:literal)
+    ])))
+(run 'main)
+(let base memory*.1
+  (when (~memory-contains-array base "0")
+    (prn "F - converting zero to decimal string")))
+
+(reset)
+(new-trace "integer-to-decimal-string-negative")
+(add-code
+  '((function main [
+      (1:string-address/raw <- integer-to-decimal-string -237:literal)
+    ])))
+(run 'main)
+(let base memory*.1
+  (when (~memory-contains-array base "-237")
+    (prn "F - converting negative integer to decimal string")))
+
+; fake screen for tests; prints go to a string
+(reset)
+(new-trace "fake-screen-empty")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 30:literal/capacity)
+      (screen:terminal-address <- init-fake-terminal 20:literal 10:literal)
+      (5:string-address/raw <- get screen:terminal-address/deref data:offset)
+     ])))
+(run 'main)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~memory-contains-array memory*.5
+          (+ "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "))
+  (prn "F - fake screen starts out with all spaces"))
+
+; fake keyboard for tests; must initialize keys in advance
+(reset)
+(new-trace "fake-keyboard")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 30:literal)
+      (s:string-address <- new "foo")
+      (x:keyboard-address <- init-keyboard s:string-address)
+      (1:character-address/raw <- read-key x:keyboard-address)
+     ])))
+(run 'main)
+(when (~is memory*.1 #\f)
+  (prn "F - 'read-key' reads character from provided 'fake keyboard' string"))
+
+; fake keyboard for tests; must initialize keys in advance
+(reset)
+(new-trace "fake-keyboard2")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 30:literal)
+      (s:string-address <- new "foo")
+      (x:keyboard-address <- init-keyboard s:string-address)
+      (1:character-address/raw <- read-key x:keyboard-address)
+      (1:character-address/raw <- read-key x:keyboard-address)
+     ])))
+(run 'main)
+(when (~is memory*.1 #\o)
+  (prn "F - 'read-key' advances cursor in provided string"))
+
+; to receive input line by line, run send-keys-buffered-to-stdin
+(reset)
+(new-trace "buffer-stdin-until-newline")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 30:literal)
+      (s:string-address <- new "foo")
+      (k:keyboard-address <- init-keyboard s:string-address)
+      (stdin:channel-address <- init-channel 1:literal)
+      (fork send-keys-to-stdin:fn nil:literal/globals nil:literal/limit k:keyboard-address stdin:channel-address)
+      (buffered-stdin:channel-address <- init-channel 1:literal)
+      (r:integer/routine <- fork buffer-lines:fn nil:literal/globals nil:literal/limit stdin:channel-address buffered-stdin:channel-address)
+      (screen:terminal-address <- init-fake-terminal 20:literal 10:literal)
+      (5:string-address/raw <- get screen:terminal-address/deref data:offset)
+      (fork-helper send-prints-to-stdout:fn nil:literal/globals nil:literal/limit screen:terminal-address buffered-stdin:channel-address)
+      (sleep until-routine-done:literal r:integer/routine)
+    ])))
+;? (set dump-trace*) ;? 3
+;? (= dump-trace* (obj whitelist '("schedule" "run"))) ;? 0
+(run 'main)
+;? (prn int-canon.memory*) ;? 0
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~memory-contains-array memory*.5
+          (+ "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "))
+  (prn "F - 'buffer-lines' prints nothing until newline is encountered"))
+;? (quit) ;? 3
+
+(reset)
+(new-trace "print-buffered-contents-on-newline")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 30:literal)
+      (s:string-address <- new "foo\nline2")
+      (k:keyboard-address <- init-keyboard s:string-address)
+      (stdin:channel-address <- init-channel 1:literal)
+      (fork send-keys-to-stdin:fn nil:literal/globals nil:literal/limit k:keyboard-address stdin:channel-address)
+      (buffered-stdin:channel-address <- init-channel 1:literal)
+      (r:integer/routine <- fork buffer-lines:fn nil:literal/globals nil:literal/limit stdin:channel-address buffered-stdin:channel-address)
+      (screen:terminal-address <- init-fake-terminal 20:literal 10:literal)
+      (5:string-address/raw <- get screen:terminal-address/deref data:offset)
+      (fork-helper send-prints-to-stdout:fn nil:literal/globals nil:literal/limit screen:terminal-address buffered-stdin:channel-address)
+      (sleep until-routine-done:literal r:integer/routine)
+    ])))
+;? (= dump-trace* (obj whitelist '("schedule" "run"))) ;? 1
+(run 'main)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~memory-contains-array memory*.5
+          (+ "foo\n                "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "))
+  (prn "F - 'buffer-lines' prints lines to screen"))
+
+(reset)
+(new-trace "print-buffered-contents-right-at-newline")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 30:literal)
+      (s:string-address <- new "foo\n")
+      (k:keyboard-address <- init-keyboard s:string-address)
+      (stdin:channel-address <- init-channel 1:literal)
+      (fork send-keys-to-stdin:fn nil:literal/globals nil:literal/limit k:keyboard-address stdin:channel-address)
+      (buffered-stdin:channel-address <- init-channel 1:literal)
+      (r:integer/routine <- fork buffer-lines:fn nil:literal/globals nil:literal/limit stdin:channel-address buffered-stdin:channel-address)
+      (screen:terminal-address <- init-fake-terminal 20:literal 10:literal)
+      (5:string-address/raw <- get screen:terminal-address/deref data:offset)
+      (fork-helper send-prints-to-stdout:fn nil:literal/globals nil:literal/limit screen:terminal-address buffered-stdin:channel-address)
+      (sleep until-routine-done:literal r:integer/routine)
+      ; hack: give helper some time to finish printing
+      (sleep for-some-cycles:literal 500:literal)
+    ])))
+;? (= dump-trace* (obj whitelist '("schedule" "run"))) ;? 1
+(run 'main)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~memory-contains-array memory*.5
+          (+ "foo\n                "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "))
+  (prn "F - 'buffer-lines' prints lines to screen immediately on newline"))
+
+(reset)
+(new-trace "buffered-contents-skip-backspace")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 30:literal)
+      (s:string-address <- new "fooa\b\nline2")
+      (k:keyboard-address <- init-keyboard s:string-address)
+      (stdin:channel-address <- init-channel 1:literal)
+      (fork send-keys-to-stdin:fn nil:literal/globals nil:literal/limit k:keyboard-address stdin:channel-address)
+      (buffered-stdin:channel-address <- init-channel 1:literal)
+      (r:integer/routine <- fork buffer-lines:fn nil:literal/globals nil:literal/limit stdin:channel-address buffered-stdin:channel-address)
+      (screen:terminal-address <- init-fake-terminal 20:literal 10:literal)
+      (5:string-address/raw <- get screen:terminal-address/deref data:offset)
+      (fork-helper send-prints-to-stdout:fn nil:literal/globals nil:literal/limit screen:terminal-address buffered-stdin:channel-address)
+      (sleep until-routine-done:literal r:integer/routine)
+    ])))
+;? (= dump-trace* (obj whitelist '("schedule" "run"))) ;? 1
+(run 'main)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~memory-contains-array memory*.5
+          (+ "foo\n                "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "))
+  (prn "F - 'buffer-lines' handles backspace"))
+
+(reset)
+(new-trace "buffered-contents-ignore-excess-backspace")
+(add-code
+  '((function main [
+      (default-space:space-address <- new space:literal 30:literal)
+      (s:string-address <- new "a\b\bfoo\n")
+      (k:keyboard-address <- init-keyboard s:string-address)
+      (stdin:channel-address <- init-channel 1:literal)
+      (fork send-keys-to-stdin:fn nil:literal/globals nil:literal/limit k:keyboard-address stdin:channel-address)
+      (buffered-stdin:channel-address <- init-channel 1:literal)
+      (r:integer/routine <- fork buffer-lines:fn nil:literal/globals nil:literal/limit stdin:channel-address buffered-stdin:channel-address)
+      (screen:terminal-address <- init-fake-terminal 20:literal 10:literal)
+      (5:string-address/raw <- get screen:terminal-address/deref data:offset)
+      (fork-helper send-prints-to-stdout:fn nil:literal/globals nil:literal/limit screen:terminal-address buffered-stdin:channel-address)
+      (sleep until-routine-done:literal r:integer/routine)
+      ; hack: give helper some time to finish printing
+      (sleep for-some-cycles:literal 500:literal)
+    ])))
+;? (= dump-trace* (obj whitelist '("schedule" "run"))) ;? 1
+(run 'main)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+;? (prn memory*.5) ;? 1
+(when (~memory-contains-array memory*.5
+          (+ "foo\n                "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "
+             "                    "))
+  (prn "F - 'buffer-lines' ignores backspace when there's nothing to backspace over"))
+
+)  ; section 100
+
+(reset)
+(new-trace "parse-and-record")
+(add-code
+  '((and-record foo [
+      x:string
+      y:integer
+      z:boolean
+     ])))
+(when (~iso type*!foo (obj size 3  and-record t  elems '((string) (integer) (boolean))  fields '(x y z)))
+  (prn "F - 'add-code' can add new and-records"))
+
+;; unit tests for various helpers
+
+; tokenize-args
+(prn "== tokenize-args")
+(assert:iso '((a b) (c d))
+            (tokenize-arg 'a:b/c:d))
+; numbers are not symbols
+(assert:iso '((a b) (1 d))
+            (tokenize-arg 'a:b/1:d))
+; special symbols are skipped
+(assert:iso '<-
+            (tokenize-arg '<-))
+(assert:iso '_
+            (tokenize-arg '_))
+
+; idempotent
+(assert:iso (tokenize-arg:tokenize-arg 'a:b/c:d)
+            (tokenize-arg              'a:b/c:d))
+
+; support labels
+(assert:iso '((((default-space space-address)) <- ((new)) ((space literal)) ((30 literal)))
+              foo)
+            (tokenize-args
+              '((default-space:space-address <- new space:literal 30:literal)
+                foo)))
+
+; support braces
+(assert:iso '((((default-space space-address)) <- ((new)) ((space literal)) ((30 literal)))
+              foo
+              { begin
+                bar
+                (((a b)) <- ((op)) ((c d)) ((e f)))
+              })
+            (tokenize-args
+              '((default-space:space-address <- new space:literal 30:literal)
+                foo
+                { begin
+                  bar
+                  (a:b <- op c:d e:f)
+                })))
+
+; space
+(prn "== space")
+(reset)
+(when (~iso 0 (space '((4 integer))))
+  (prn "F - 'space' is 0 by default"))
+(when (~iso 1 (space '((4 integer) (space 1))))
+  (prn "F - 'space' picks up space when available"))
+(when (~iso 'global (space '((4 integer) (space global))))
+  (prn "F - 'space' understands routine-global space"))
+
+; absolutize
+(prn "== absolutize")
+(reset)
+(when (~iso '((4 integer)) (absolutize '((4 integer))))
+  (prn "F - 'absolutize' works without routine"))
+(= routine* make-routine!foo)
+(when (~iso '((4 integer)) (absolutize '((4 integer))))
+  (prn "F - 'absolutize' works without default-space"))
+(= rep.routine*!call-stack.0!default-space 10)
+(= memory*.10 5)  ; bounds check for default-space
+(when (~iso '((15 integer) (raw))
+            (absolutize '((4 integer))))
+  (prn "F - 'absolutize' works with default-space"))
+(absolutize '((5 integer)))
+(when (~posmatch "no room" rep.routine*!error)
+  (prn "F - 'absolutize' checks against default-space bounds"))
+(when (~iso '((_ integer)) (absolutize '((_ integer))))
+  (prn "F - 'absolutize' passes dummy args right through"))
+(when (~iso '((default-space integer)) (absolutize '((default-space integer))))
+  (prn "F - 'absolutize' passes 'default-space' right through"))
+
+(= memory*.20 5)  ; pretend array
+(= rep.routine*!globals 20)  ; provide it to routine global
+(when (~iso '((22 integer) (raw))
+            (absolutize '((1 integer) (space global))))
+  (prn "F - 'absolutize' handles variables in the global space"))
+
+; deref
+(prn "== deref")
+(reset)
+(= memory*.3 4)
+(when (~iso '((4 integer))
+            (deref '((3 integer-address)
+                     (deref))))
+  (prn "F - 'deref' handles simple addresses"))
+(when (~iso '((4 integer) (deref))
+            (deref '((3 integer-address)
+                     (deref)
+                     (deref))))
+  (prn "F - 'deref' deletes just one deref"))
+(= memory*.4 5)
+(when (~iso '((5 integer))
+            (deref:deref '((3 integer-address-address)
+                           (deref)
+                           (deref))))
+  (prn "F - 'deref' can be chained"))
+(when (~iso '((5 integer) (foo))
+            (deref:deref '((3 integer-address-address)
+                           (deref)
+                           (foo)
+                           (deref))))
+  (prn "F - 'deref' skips junk"))
+
+; addr
+(prn "== addr")
+(reset)
+(= routine* nil)
+;? (prn 111)
+(when (~is 4 (addr '((4 integer))))
+  (prn "F - directly addressed operands are their own address"))
+;? (quit)
+(when (~is 4 (addr '((4 integer-address))))
+  (prn "F - directly addressed operands are their own address - 2"))
+(when (~is 4 (addr '((4 literal))))
+  (prn "F - 'addr' doesn't understand literals"))
+;? (prn 201)
+(= memory*.4 23)
+;? (prn 202)
+(when (~is 23 (addr '((4 integer-address) (deref))))
+  (prn "F - 'addr' works with indirectly-addressed 'deref'"))
+;? (quit)
+(= memory*.3 4)
+(when (~is 23 (addr '((3 integer-address-address) (deref) (deref))))
+  (prn "F - 'addr' works with multiple 'deref'"))
+
+(= routine* make-routine!foo)
+(when (~is 4 (addr '((4 integer))))
+  (prn "F - directly addressed operands are their own address inside routines"))
+(when (~is 4 (addr '((4 integer-address))))
+  (prn "F - directly addressed operands are their own address inside routines - 2"))
+(when (~is 4 (addr '((4 literal))))
+  (prn "F - 'addr' doesn't understand literals inside routines"))
+(= memory*.4 23)
+(when (~is 23 (addr '((4 integer-address) (deref))))
+  (prn "F - 'addr' works with indirectly-addressed 'deref' inside routines"))
+
+;? (prn 301)
+(= rep.routine*!call-stack.0!default-space 10)
+;? (prn 302)
+(= memory*.10 5)  ; bounds check for default-space
+;? (prn 303)
+(when (~is 15 (addr '((4 integer))))
+  (prn "F - directly addressed operands in routines add default-space"))
+;? (quit)
+(when (~is 15 (addr '((4 integer-address))))
+  (prn "F - directly addressed operands in routines add default-space - 2"))
+(when (~is 15 (addr '((4 literal))))
+  (prn "F - 'addr' doesn't understand literals"))
+(= memory*.15 23)
+(when (~is 23 (addr '((4 integer-address) (deref))))
+  (prn "F - 'addr' adds default-space before 'deref', not after"))
+;? (quit)
+
+; array-len
+(prn "== array-len")
+(reset)
+(= memory*.35 4)
+(when (~is 4 (array-len '((35 integer-boolean-pair-array))))
+  (prn "F - 'array-len'"))
+(= memory*.34 35)
+(when (~is 4 (array-len '((34 integer-boolean-pair-array-address) (deref))))
+  (prn "F - 'array-len'"))
+;? (quit)
+
+; sizeof
+(prn "== sizeof")
+(reset)
+;? (set dump-trace*)
+;? (prn 401)
+(when (~is 1 (sizeof '((_ integer))))
+  (prn "F - 'sizeof' works on primitives"))
+(when (~is 1 (sizeof '((_ integer-address))))
+  (prn "F - 'sizeof' works on addresses"))
+(when (~is 2 (sizeof '((_ integer-boolean-pair))))
+  (prn "F - 'sizeof' works on and-records"))
+(when (~is 3 (sizeof '((_ integer-point-pair))))
+  (prn "F - 'sizeof' works on and-records with and-record fields"))
+
+;? (prn 410)
+(when (~is 1 (sizeof '((34 integer))))
+  (prn "F - 'sizeof' works on primitive operands"))
+(when (~is 1 (sizeof '((34 integer-address))))
+  (prn "F - 'sizeof' works on address operands"))
+(when (~is 2 (sizeof '((34 integer-boolean-pair))))
+  (prn "F - 'sizeof' works on and-record operands"))
+(when (~is 3 (sizeof '((34 integer-point-pair))))
+  (prn "F - 'sizeof' works on and-record operands with and-record fields"))
+(when (~is 2 (sizeof '((34 integer-boolean-pair-address) (deref))))
+  (prn "F - 'sizeof' works on pointers to and-records"))
+(= memory*.35 4)  ; size of array
+(= memory*.34 35)
+;? (= dump-trace* (obj whitelist '("sizeof" "array-len")))
+(when (~is 9 (sizeof '((34 integer-boolean-pair-array-address) (deref))))
+  (prn "F - 'sizeof' works on pointers to arrays"))
+;? (quit)
+
+;? (prn 420)
+(= memory*.4 23)
+(when (~is 24 (sizeof '((4 integer-array))))
+  (prn "F - 'sizeof' reads array lengths from memory"))
+(= memory*.3 4)
+(when (~is 24 (sizeof '((3 integer-array-address) (deref))))
+  (prn "F - 'sizeof' handles pointers to arrays"))
+(= memory*.15 34)
+(= routine* make-routine!foo)
+(when (~is 24 (sizeof '((4 integer-array))))
+  (prn "F - 'sizeof' reads array lengths from memory inside routines"))
+(= rep.routine*!call-stack.0!default-space 10)
+(= memory*.10 5)  ; bounds check for default-space
+(when (~is 35 (sizeof '((4 integer-array))))
+  (prn "F - 'sizeof' reads array lengths from memory using default-space"))
+(= memory*.35 4)  ; size of array
+(= memory*.15 35)
+;? (= dump-trace* (obj whitelist '("sizeof")))
+(aif rep.routine*!error (prn "error - " it))
+(when (~is 9 (sizeof '((4 integer-boolean-pair-array-address) (deref))))
+  (prn "F - 'sizeof' works on pointers to arrays using default-space"))
+;? (quit)
+
+; m
+(prn "== m")
+(reset)
+(when (~is 4 (m '((4 literal))))
+  (prn "F - 'm' avoids reading memory for literals"))
+(when (~is 4 (m '((4 offset))))
+  (prn "F - 'm' avoids reading memory for offsets"))
+(= memory*.4 34)
+(when (~is 34 (m '((4 integer))))
+  (prn "F - 'm' reads memory for simple types"))
+(= memory*.3 4)
+(when (~is 34 (m '((3 integer-address) (deref))))
+  (prn "F - 'm' redirects addresses"))
+(= memory*.2 3)
+(when (~is 34 (m '((2 integer-address-address) (deref) (deref))))
+  (prn "F - 'm' multiply redirects addresses"))
+(when (~iso (annotate 'record '(34 nil)) (m '((4 integer-boolean-pair))))
+  (prn "F - 'm' supports compound records"))
+(= memory*.5 35)
+(= memory*.6 36)
+(when (~iso (annotate 'record '(34 35 36)) (m '((4 integer-point-pair))))
+  (prn "F - 'm' supports records with compound fields"))
+(when (~iso (annotate 'record '(34 35 36)) (m '((3 integer-point-pair-address) (deref))))
+  (prn "F - 'm' supports indirect access to records"))
+(= memory*.4 2)
+(when (~iso (annotate 'record '(2 35 36)) (m '((4 integer-array))))
+  (prn "F - 'm' supports access to arrays"))
+(when (~iso (annotate 'record '(2 35 36)) (m '((3 integer-array-address) (deref))))
+  (prn "F - 'm' supports indirect access to arrays"))
+
+(= routine* make-routine!foo)
+(= memory*.10 5)  ; fake array
+(= memory*.12 34)
+(= rep.routine*!globals 10)
+(when (~iso 34 (m '((1 integer) (space global))))
+  (prn "F - 'm' supports access to per-routine globals"))
+
+; setm
+(prn "== setm")
+(reset)
+(setm '((4 integer)) 34)
+(when (~is 34 memory*.4)
+  (prn "F - 'setm' writes primitives to memory"))
+(setm '((3 integer-address)) 4)
+(when (~is 4 memory*.3)
+  (prn "F - 'setm' writes addresses to memory"))
+(setm '((3 integer-address) (deref)) 35)
+(when (~is 35 memory*.4)
+  (prn "F - 'setm' redirects writes"))
+(= memory*.2 3)
+(setm '((2 integer-address-address) (deref) (deref)) 36)
+(when (~is 36 memory*.4)
+  (prn "F - 'setm' multiply redirects writes"))
+;? (prn 505)
+(setm '((4 integer-integer-pair)) (annotate 'record '(23 24)))
+(when (~memory-contains 4 '(23 24))
+  (prn "F - 'setm' writes compound records"))
+(assert (is memory*.7 nil))
+;? (prn 506)
+(setm '((7 integer-point-pair)) (annotate 'record '(23 24 25)))
+(when (~memory-contains 7 '(23 24 25))
+  (prn "F - 'setm' writes records with compound fields"))
+(= routine* make-routine!foo)
+(setm '((4 integer-point-pair)) (annotate 'record '(33 34)))
+(when (~posmatch "incorrect size" rep.routine*!error)
+  (prn "F - 'setm' checks size of target"))
+(wipe routine*)
+(setm '((3 integer-point-pair-address) (deref)) (annotate 'record '(43 44 45)))
+(when (~memory-contains 4 '(43 44 45))
+  (prn "F - 'setm' supports indirect writes to records"))
+(setm '((2 integer-point-pair-address-address) (deref) (deref)) (annotate 'record '(53 54 55)))
+(when (~memory-contains 4 '(53 54 55))
+  (prn "F - 'setm' supports multiply indirect writes to records"))
+(setm '((4 integer-array)) (annotate 'record '(2 31 32)))
+(when (~memory-contains 4 '(2 31 32))
+  (prn "F - 'setm' writes arrays"))
+(setm '((3 integer-array-address) (deref)) (annotate 'record '(2 41 42)))
+(when (~memory-contains 4 '(2 41 42))
+  (prn "F - 'setm' supports indirect writes to arrays"))
+(= routine* make-routine!foo)
+(setm '((4 integer-array)) (annotate 'record '(2 31 32 33)))
+(when (~posmatch "invalid array" rep.routine*!error)
+  (prn "F - 'setm' checks that array written is well-formed"))
+(= routine* make-routine!foo)
+;? (prn 111)
+;? (= dump-trace* (obj whitelist '("sizeof" "mem")))
+(setm '((4 integer-boolean-pair-array)) (annotate 'record '(2 31 nil 32 nil 33)))
+(when (~posmatch "invalid array" rep.routine*!error)
+  (prn "F - 'setm' checks that array of records is well-formed"))
+(= routine* make-routine!foo)
+;? (prn 222)
+(setm '((4 integer-boolean-pair-array)) (annotate 'record '(2 31 nil 32 nil)))
+(when (posmatch "invalid array" rep.routine*!error)
+  (prn "F - 'setm' checks that array of records is well-formed - 2"))
+(wipe routine*)
+
+(reset)  ; end file with this to persist the trace for the final test
diff --git a/archive/1.vm.arc/mu.arc.t.html b/archive/1.vm.arc/mu.arc.t.html
new file mode 100644
index 00000000..dd641472
--- /dev/null
+++ b/archive/1.vm.arc/mu.arc.t.html
@@ -0,0 +1,4154 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<title>mu.arc.t</title>
+<meta name="Generator" content="Vim/7.4">
+<meta name="plugin-version" content="vim7.4_v1">
+<meta name="syntax" content="scheme">
+<meta name="settings" content="use_css,pre_wrap,no_foldcolumn,expand_tabs,prevent_copy=">
+<meta name="colorscheme" content="minimal">
+<style type="text/css">
+<!--
+pre { white-space: pre-wrap; font-family: monospace; color: #aaaaaa; background-color: #000000; }
+body { font-family: monospace; color: #aaaaaa; background-color: #000000; }
+a { color:#4444ff; }
+* { font-size: 1em; }
+.Global { color: #00af87; }
+.SalientComment { color: #00ffff; }
+.CommentedCode { color: #666666; }
+.Mu, .Mu .Normal, .Mu .Constant { color: #eeeeee; }
+.Op { color: #ff8888; }
+.Delimiter { color: #600060; }
+.Normal { color: #aaaaaa; }
+.Comment { color: #8080ff; }
+.Constant, .MuConstant { color: #008080; }
+.Identifier { color: #008080; }
+-->
+</style>
+
+<script type='text/javascript'>
+<!--
+
+-->
+</script>
+</head>
+<body>
+<pre id='vimCodeElement'>
+<span class="Comment">; Mu: An exploration on making the global structure of programs more accessible.</span>
+<span class="Comment">;</span>
+<span class="Comment">;   &quot;Is it a language, or an operating system, or a virtual machine? Mu.&quot;</span>
+<span class="Comment">;   (with apologies to Robert Pirsig: <a href="http://en.wikipedia.org/wiki/Mu_%28negative%29#In_popular_culture">http://en.wikipedia.org/wiki/Mu_%28negative%29#In_popular_culture</a>)</span>
+<span class="Comment">;</span>
+<span class="SalientComment">;; Motivation</span>
+<span class="Comment">;</span>
+<span class="Comment">; I want to live in a world where I can have an itch to tweak a program, clone</span>
+<span class="Comment">; its open-source repository, orient myself on how it's organized, and make</span>
+<span class="Comment">; the simple change I envisioned, all in an afternoon. This codebase tries to</span>
+<span class="Comment">; make this possible for its readers. (More details: <a href="http://akkartik.name/about">http://akkartik.name/about</a>)</span>
+<span class="Comment">;</span>
+<span class="Comment">; What helps comprehend the global structure of programs? For starters, let's</span>
+<span class="Comment">; enumerate what doesn't: idiomatic code, adherence to a style guide or naming</span>
+<span class="Comment">; convention, consistent indentation, API documentation for each class, etc.</span>
+<span class="Comment">; These conventional considerations improve matters in the small, but don't</span>
+<span class="Comment">; help understand global organization. They help existing programmers manage</span>
+<span class="Comment">; day-to-day operations, but they can't turn outsider programmers into</span>
+<span class="Comment">; insiders. (Elaboration: <a href="http://akkartik.name/post/readable-bad">http://akkartik.name/post/readable-bad</a>)</span>
+<span class="Comment">;</span>
+<span class="Comment">; In my experience, two things have improved matters so far: version control</span>
+<span class="Comment">; and automated tests. Version control lets me rewind back to earlier, simpler</span>
+<span class="Comment">; times when the codebase was simpler, when its core skeleton was easier to</span>
+<span class="Comment">; ascertain. Indeed, arguably what came first is by definition the skeleton of</span>
+<span class="Comment">; a program, modulo major rewrites. Once you understand the skeleton, it</span>
+<span class="Comment">; becomes tractable to 'play back' later major features one by one. (Previous</span>
+<span class="Comment">; project that fleshed out this idea: <a href="http://akkartik.name/post/wart-layers">http://akkartik.name/post/wart-layers</a>)</span>
+<span class="Comment">;</span>
+<span class="Comment">; The second and biggest boost to comprehension comes from tests. Tests are</span>
+<span class="Comment">; good for writers for well-understood reasons: they avoid regressions, and</span>
+<span class="Comment">; they can influence code to be more decoupled and easier to change. In</span>
+<span class="Comment">; addition, tests are also good for the outsider reader because they permit</span>
+<span class="Comment">; active reading. If you can't build a program and run its tests it can't help</span>
+<span class="Comment">; you understand it. It hangs limp at best, and might even be actively</span>
+<span class="Comment">; misleading. If you can run its tests, however, it comes alive. You can step</span>
+<span class="Comment">; through scenarios in a debugger. You can add logging and scan logs to make</span>
+<span class="Comment">; sense of them. You can run what-if scenarios: &quot;why is this line not written</span>
+<span class="Comment">; like this?&quot; Make a change, rerun tests: &quot;Oh, that's why.&quot; (Elaboration:</span>
+<span class="Comment">; <a href="http://akkartik.name/post/literate-programming">http://akkartik.name/post/literate-programming</a>)</span>
+<span class="Comment">;</span>
+<span class="Comment">; However, tests are only useful to the extent that they exist. Think back to</span>
+<span class="Comment">; your most recent codebase. Do you feel comfortable releasing a new version</span>
+<span class="Comment">; just because the tests pass? I'm not aware of any such project. There's just</span>
+<span class="Comment">; too many situations envisaged by the authors that were never encoded in a</span>
+<span class="Comment">; test. Even disciplined authors can't test for performance or race conditions</span>
+<span class="Comment">; or fault tolerance. If a line is phrased just so because of some subtle</span>
+<span class="Comment">; performance consideration, it's hard to communicate to newcomers.</span>
+<span class="Comment">;</span>
+<span class="Comment">; This isn't an arcane problem, and it isn't just a matter of altruism. As</span>
+<span class="Comment">; more and more such implicit considerations proliferate, and as the original</span>
+<span class="Comment">; authors are replaced by latecomers for day-to-day operations, knowledge is</span>
+<span class="Comment">; actively forgotten and lost. The once-pristine codebase turns into legacy</span>
+<span class="Comment">; code that is hard to modify without expensive and stress-inducing</span>
+<span class="Comment">; regressions.</span>
+<span class="Comment">;</span>
+<span class="Comment">; How to write tests for performance, fault tolerance, race conditions, etc.?</span>
+<span class="Comment">; How can we state and verify that a codepath doesn't ever perform memory</span>
+<span class="Comment">; allocation, or write to disk? It requires better, more observable primitives</span>
+<span class="Comment">; than we currently have. Modern operating systems have their roots in the</span>
+<span class="Comment">; 70s. Their interfaces were not designed to be testable. They provide no way</span>
+<span class="Comment">; to simulate a full disk, or a specific sequence of writes from different</span>
+<span class="Comment">; threads. We need something better.</span>
+<span class="Comment">;</span>
+<span class="Comment">; This project tries to move, groping, towards that 'something better', a</span>
+<span class="Comment">; platform that is both thoroughly tested and allows programs written for it</span>
+<span class="Comment">; to be thoroughly tested. It tries to answer the question:</span>
+<span class="Comment">;</span>
+<span class="Comment">;   If Denis Ritchie and Ken Thompson were to set out today to co-design unix</span>
+<span class="Comment">;   and C, knowing what we know about automated tests, what would they do</span>
+<span class="Comment">;   differently?</span>
+<span class="Comment">;</span>
+<span class="Comment">; To try to impose *some* constraints on this gigantic yak-shave, we'll try to</span>
+<span class="Comment">; keep both language and OS as simple as possible, focused entirely on</span>
+<span class="Comment">; permitting more kinds of tests, on first *collecting* all the information</span>
+<span class="Comment">; about implicit considerations in some form so that readers and tools can</span>
+<span class="Comment">; have at least some hope of making sense of it.</span>
+<span class="Comment">;</span>
+<span class="Comment">; The initial language will be just assembly. We'll try to make it convenient</span>
+<span class="Comment">; to program in with some simple localized rewrite rules inspired by lisp</span>
+<span class="Comment">; macros and literate programming. Programmers will have to do their own</span>
+<span class="Comment">; memory management and register allocation, but we'll provide libraries to</span>
+<span class="Comment">; help with them.</span>
+<span class="Comment">;</span>
+<span class="Comment">; The initial OS will provide just memory management and concurrency</span>
+<span class="Comment">; primitives. No users or permissions (we don't live on mainframes anymore),</span>
+<span class="Comment">; no kernel- vs user-mode, no virtual memory or process abstraction, all</span>
+<span class="Comment">; threads sharing a single address space (use VMs for security and</span>
+<span class="Comment">; sandboxing). The only use case we care about is getting a test harness to</span>
+<span class="Comment">; run some code, feed it data through blocking channels, stop it and observe</span>
+<span class="Comment">; its internals. The code under test is expected to cooperate in such testing,</span>
+<span class="Comment">; by logging important events for the test harness to observe. (More info:</span>
+<span class="Comment">; <a href="http://akkartik.name/post/tracing-tests">http://akkartik.name/post/tracing-tests</a>)</span>
+<span class="Comment">;</span>
+<span class="Comment">; The common thread here is elimination of abstractions, and it's not an</span>
+<span class="Comment">; accident. Abstractions help insiders manage the evolution of a codebase, but</span>
+<span class="Comment">; they actively hinder outsiders in understanding it from scratch. This</span>
+<span class="Comment">; matters, because the funnel to turn outsiders into insiders is critical to</span>
+<span class="Comment">; the long-term life of a codebase. Perhaps authors should raise their</span>
+<span class="Comment">; estimation of the costs of abstraction, and go against their instincts for</span>
+<span class="Comment">; introducing it. That's what I'll be trying to do: question every abstraction</span>
+<span class="Comment">; before I introduce it. We'll see how it goes.</span>
+
+<span class="Comment">; ---</span>
+
+<span class="SalientComment">;; Getting started</span>
+<span class="Comment">;</span>
+<span class="Comment">; Mu is currently built atop Racket and Arc, but this is temporary and</span>
+<span class="Comment">; contingent. We want to keep our options open, whether to port to a different</span>
+<span class="Comment">; host language, and easy to rewrite to native code for any platform. So we'll</span>
+<span class="Comment">; try to avoid 'cheating': relying on the host platform for advanced</span>
+<span class="Comment">; functionality.</span>
+<span class="Comment">;</span>
+<span class="Comment">; Other than that, we'll say no more about the code, and focus in the rest of</span>
+<span class="Comment">; this file on the scenarios the code cares about.</span>
+
+<span class="Delimiter">(</span>selective-load <span class="Constant">&quot;mu.arc&quot;</span> section-level<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">20</span>
+
+<span class="Comment">; Our language is assembly-like in that functions consist of series of</span>
+<span class="Comment">; statements, and statements consist of an operation and its arguments (input</span>
+<span class="Comment">; and output).</span>
+<span class="Comment">;</span>
+<span class="Comment">;   oarg1, oarg2, ... <span class="Op">&lt;-</span> op arg1, arg2, ...</span>
+<span class="Comment">;</span>
+<span class="Comment">; Args must be atomic, like an integer or a memory address, they can't be</span>
+<span class="Comment">; expressions doing arithmetic or function calls. But we can have any number</span>
+<span class="Comment">; of them.</span>
+<span class="Comment">;</span>
+<span class="Comment">; Since we're building on lisp, our code samples won't look quite like the</span>
+<span class="Comment">; idealized syntax above. For now they will look like this:</span>
+<span class="Comment">;</span>
+<span class="Comment">;   (function f [</span>
+<span class="Comment">;     (oarg1 oarg2 ... <span class="Op">&lt;-</span> op arg1 arg2 ...)</span>
+<span class="Comment">;     ...</span>
+<span class="Comment">;     ...</span>
+<span class="Comment">;    ])</span>
+<span class="Comment">;</span>
+<span class="Comment">; Each arg/oarg can contain metadata separated by slashes and colons. In this</span>
+<span class="Comment">; first example below, the only metadata is types: 'integer' for a memory</span>
+<span class="Comment">; location containing an integer, and 'literal' for a value included directly</span>
+<span class="Comment">; in code. (Assembly languages traditionally call them 'immediate' operands.)</span>
+<span class="Comment">; In the future a simple tool will check that the types line up as expected in</span>
+<span class="Comment">; each op. A different tool might add types where they aren't provided.</span>
+<span class="Comment">; Instead of a monolithic compiler I want to build simple, lightweight tools</span>
+<span class="Comment">; that can be combined in various ways, say for using different typecheckers</span>
+<span class="Comment">; in different subsystems.</span>
+<span class="Comment">;</span>
+<span class="Comment">; In our tests we'll define such mu functions using a call to 'add-code', so</span>
+<span class="Comment">; look for it when reading the code examples. Everything outside 'add-code' is</span>
+<span class="Comment">; just test-harness details that can be skipped at first.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Constant">23</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'copy' writes its lone 'arg' after the instruction name to its lone 'oarg' or output arg before the arrow. After this test, the value 23 is stored in memory address 1.&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; Our basic arithmetic ops can operate on memory locations or literals.</span>
+<span class="Comment">; (Ignore hardware details like registers for now.)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;add&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">1</span>  <span class="Constant">2</span> <span class="Constant">3</span>  <span class="Constant">3</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'add' operates on two addresses&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;add-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> add <span class="MuConstant">2</span>:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Constant">5</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - ops can take 'literal' operands (but not return them)&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;sub-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> subtract <span class="MuConstant">1</span>:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Constant">-2</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'subtract'&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;mul-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> multiply <span class="MuConstant">2</span>:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Constant">6</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'multiply'&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;div-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> divide <span class="MuConstant">8</span>:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Delimiter">(</span>/ real.8 <span class="Constant">3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'divide'&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;idiv-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Constant">2</span>:integer <span class="Op">&lt;-</span> divide-with-remainder <span class="MuConstant">23</span>:literal <span class="MuConstant">6</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">3</span>  <span class="Constant">2</span> <span class="Constant">5</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'divide-with-remainder' performs integer division&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;dummy-oarg&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>_ <span class="Constant">2</span>:integer <span class="Op">&lt;-</span> divide-with-remainder <span class="MuConstant">23</span>:literal <span class="MuConstant">6</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">2</span> <span class="Constant">5</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - '_' oarg can ignore some results&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; Basic boolean operations: and, or, not</span>
+<span class="Comment">; There are easy ways to encode booleans in binary, but we'll skip past those</span>
+<span class="Comment">; details for now.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;and-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> and <span class="MuConstant">t</span>:literal <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> nil<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - logical 'and' for booleans&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; Basic comparison operations</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;lt-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> less-than <span class="MuConstant">4</span>:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> nil<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'less-than' inequality operator&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;le-literal-false&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> lesser-or-equal <span class="MuConstant">4</span>:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> nil<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'lesser-or-equal'&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;le-literal-true&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> lesser-or-equal <span class="MuConstant">4</span>:literal <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> t<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'lesser-or-equal' returns true for equal operands&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;le-literal-true-2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> lesser-or-equal <span class="MuConstant">4</span>:literal <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> t<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'lesser-or-equal' - 2&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; Control flow operations: jump, jump-if, jump-unless</span>
+<span class="Comment">; These introduce a new type -- 'offset' -- for literals that refer to memory</span>
+<span class="Comment">; locations relative to the current location.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;jump-skip&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">8</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">jump</span> <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; should be skipped</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">8</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'jump' skips some instructions&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;jump-target&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">8</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">jump</span> <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; should be skipped</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>  <span class="Comment">; never reached</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">8</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'jump' doesn't skip too many instructions&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;jump-if-skip&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> equal <span class="MuConstant">1</span>:literal <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">jump-if</span> <span class="Constant">1</span>:boolean <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> t  <span class="Constant">2</span> <span class="Constant">1</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'jump-if' is a conditional 'jump'&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;jump-if-fallthrough&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> equal <span class="MuConstant">1</span>:literal <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">jump-if</span> <span class="Constant">3</span>:boolean <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> nil  <span class="Constant">2</span> <span class="Constant">3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - if 'jump-if's first arg is false, it doesn't skip any instructions&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;jump-if-backward&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Comment">; loop</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">2</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> equal <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">jump-if</span> <span class="Constant">3</span>:boolean <span class="MuConstant">-3</span>:offset<span class="Delimiter">)</span>  <span class="Comment">; to loop</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">4</span>  <span class="Constant">3</span> nil  <span class="Constant">4</span> <span class="Constant">3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'jump-if' can take a negative offset to make backward jumps&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;jump-label&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Identifier">loop</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">2</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> equal <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">jump-if</span> <span class="Constant">3</span>:boolean <span class="Identifier">loop</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;-&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">4</span>  <span class="Constant">3</span> nil  <span class="Constant">4</span> <span class="Constant">3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'jump-if' can take a negative offset to make backward jumps&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; Data movement relies on addressing modes:</span>
+<span class="Comment">;   'direct' - refers to a memory location; default for most types.</span>
+<span class="Comment">;   'literal' - directly encoded in the code; implicit for some types like 'offset'.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;direct-addressing&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="Constant">1</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'copy' performs direct addressing&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; 'Indirect' addressing refers to an address stored in a memory location.</span>
+<span class="Comment">; Indicated by the metadata 'deref'. Usually requires an address type.</span>
+<span class="Comment">; In the test below, the memory location 1 contains '2', so an indirect read</span>
+<span class="Comment">; of location 1 returns the value of location 2.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;indirect-addressing&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer-address <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe; can't do this in general</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="Constant">1</span>:integer-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">34</span>  <span class="Constant">3</span> <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'copy' performs indirect addressing&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; Output args can use indirect addressing. In the test below the value is</span>
+<span class="Comment">; stored at the location stored in location 1 (i.e. location 2).</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;indirect-addressing-oarg&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer-address <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer-address/deref <span class="Op">&lt;-</span> add <span class="Constant">2</span>:integer <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">36</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - instructions can perform indirect addressing on output arg&quot;</span><span class="Delimiter">))</span>
+
+<span class="SalientComment">;; Compound data types</span>
+<span class="Comment">;</span>
+<span class="Comment">; Until now we've dealt with scalar types like integers and booleans and</span>
+<span class="Comment">; addresses, where mu looks like other assembly languages. In addition, mu</span>
+<span class="Comment">; provides first-class support for compound types: arrays and and-records.</span>
+<span class="Comment">;</span>
+<span class="Comment">; 'get' accesses fields in and-records</span>
+<span class="Comment">; 'index' accesses indices in arrays</span>
+<span class="Comment">;</span>
+<span class="Comment">; Both operations require knowledge about the types being worked on, so all</span>
+<span class="Comment">; types used in mu programs are defined in a single global system-wide table</span>
+<span class="Comment">; (see type* in mu.arc for the complete list of types; we'll add to it over</span>
+<span class="Comment">; time).</span>
+
+<span class="Comment">; first a sanity check that the table of types is consistent</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each <span class="Delimiter">(</span>typ typeinfo<span class="Delimiter">)</span> <span class="Global">type*</span>
+  <span class="Delimiter">(</span>when typeinfo!and-record
+    <span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is typeinfo!size <span class="Delimiter">(</span>len typeinfo!elems<span class="Delimiter">)))</span>
+    <span class="Delimiter">(</span>when typeinfo!fields
+      <span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is typeinfo!size <span class="Delimiter">(</span>len typeinfo!fields<span class="Delimiter">))))))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;get-record&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> get <span class="Constant">1</span>:integer-boolean-pair <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:integer-boolean-pair <span class="MuConstant">0</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> nil  <span class="Constant">3</span> nil  <span class="Constant">4</span> <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'get' accesses fields of and-records&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;get-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer-boolean-pair-address <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:boolean <span class="Op">&lt;-</span> get <span class="Constant">3</span>:integer-boolean-pair-address/deref <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">3</span>:integer-boolean-pair-address/deref <span class="MuConstant">0</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> nil  <span class="Constant">3</span> <span class="Constant">1</span>  <span class="Constant">4</span> nil  <span class="Constant">5</span> <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'get' accesses fields of and-record address&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;get-indirect-repeated&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">35</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">36</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer-point-pair-address <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer-point-pair-address-address <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer-integer-pair <span class="Op">&lt;-</span> get <span class="Constant">5</span>:integer-point-pair-address-address/deref/deref <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">5</span>:integer-point-pair-address-address/deref/deref <span class="MuConstant">0</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains <span class="Constant">6</span> <span class="Delimiter">'(</span><span class="Constant">35</span> <span class="Constant">36</span> <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'get' can deref multiple times&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;get-compound-field&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">35</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">36</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer-integer-pair <span class="Op">&lt;-</span> get <span class="Constant">1</span>:integer-point-pair <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> <span class="Constant">35</span>  <span class="Constant">3</span> <span class="Constant">36</span>  <span class="Constant">4</span> <span class="Constant">35</span>  <span class="Constant">5</span> <span class="Constant">36</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'get' accesses fields spanning multiple locations&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;get-address&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean-address <span class="Op">&lt;-</span> get-address <span class="Constant">1</span>:integer-boolean-pair <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> t  <span class="Constant">3</span> <span class="Constant">2</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'get-address' returns address of fields of and-records&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;get-address-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer-boolean-pair-address <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:boolean-address <span class="Op">&lt;-</span> get-address <span class="Constant">3</span>:integer-boolean-pair-address/deref <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> t  <span class="Constant">3</span> <span class="Constant">1</span>  <span class="Constant">4</span> <span class="Constant">2</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'get-address' accesses fields of and-record address&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;index-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer-boolean-pair <span class="Op">&lt;-</span> index <span class="Constant">1</span>:integer-boolean-pair-array <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">23</span> <span class="Constant">3</span> nil  <span class="Constant">4</span> <span class="Constant">24</span> <span class="Constant">5</span> t  <span class="Constant">6</span> <span class="Constant">24</span> <span class="Constant">7</span> t<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'index' accesses indices of arrays&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;index-direct&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer-boolean-pair <span class="Op">&lt;-</span> index <span class="Constant">1</span>:integer-boolean-pair-array <span class="Constant">6</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">23</span> <span class="Constant">3</span> nil  <span class="Constant">4</span> <span class="Constant">24</span> <span class="Constant">5</span> t  <span class="Constant">6</span> <span class="Constant">1</span>  <span class="Constant">7</span> <span class="Constant">24</span> <span class="Constant">8</span> t<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'index' accesses indices of arrays&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;index-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer-boolean-pair-array-address <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer-boolean-pair <span class="Op">&lt;-</span> index <span class="Constant">7</span>:integer-boolean-pair-array-address/deref <span class="Constant">6</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;cvt0&quot; &quot;cvt1&quot;)))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">23</span> <span class="Constant">3</span> nil  <span class="Constant">4</span> <span class="Constant">24</span> <span class="Constant">5</span> t  <span class="Constant">6</span> <span class="Constant">1</span>  <span class="Constant">7</span> <span class="Constant">1</span>  <span class="Constant">8</span> <span class="Constant">24</span> <span class="Constant">9</span> t<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'index' accesses indices of array address&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;index-indirect-multiple&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">25</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">26</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer-array-address <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer-array-address-address <span class="Op">&lt;-</span> copy <span class="MuConstant">6</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer <span class="Op">&lt;-</span> index <span class="Constant">7</span>:integer-array-address-address/deref/deref <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.8</span> <span class="Constant">24</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'index' can deref multiple times&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;index-address&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer-boolean-pair-address <span class="Op">&lt;-</span> index-address <span class="Constant">1</span>:integer-boolean-pair-array <span class="Constant">6</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">23</span> <span class="Constant">3</span> nil  <span class="Constant">4</span> <span class="Constant">24</span> <span class="Constant">5</span> t  <span class="Constant">6</span> <span class="Constant">1</span>  <span class="Constant">7</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'index-address' returns addresses of indices of arrays&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;index-address-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer-boolean-pair-array-address <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer-boolean-pair-address <span class="Op">&lt;-</span> index-address <span class="Constant">7</span>:integer-boolean-pair-array-address/deref <span class="Constant">6</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">2</span>  <span class="Constant">2</span> <span class="Constant">23</span> <span class="Constant">3</span> nil  <span class="Constant">4</span> <span class="Constant">24</span> <span class="Constant">5</span> t  <span class="Constant">6</span> <span class="Constant">1</span>  <span class="Constant">7</span> <span class="Constant">1</span>  <span class="Constant">8</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'index-address' returns addresses of indices of array addresses&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; Array values know their length. Record lengths are saved in the types table.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;len-array&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> length <span class="Constant">1</span>:integer-boolean-pair-array<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.6</span> <span class="Constant">2</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'length' of array&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;len-array-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer-address <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> length <span class="Constant">6</span>:integer-boolean-pair-array-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;cvt0&quot; &quot;cvt1&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.7</span> <span class="Constant">2</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'length' of array address&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; 'sizeof' is a helper to determine the amount of memory required by a type.</span>
+<span class="Comment">; Only for non-arrays.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;sizeof-record&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> sizeof integer-boolean-pair:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Constant">2</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' returns space required by arg&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;sizeof-record-not-len&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> sizeof integer-point-pair:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>is <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Constant">2</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' is different from number of elems&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; Regardless of a type's length, you can move it around just like a primitive.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;copy-record&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer-boolean-pair <span class="Op">&lt;-</span> copy <span class="Constant">1</span>:integer-boolean-pair<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> nil  <span class="Constant">3</span> <span class="Constant">34</span>  <span class="Constant">4</span> nil<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - ops can operate on records spanning multiple locations&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;copy-record2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">35</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">36</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer-point-pair <span class="Op">&lt;-</span> copy <span class="Constant">1</span>:integer-point-pair<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;sizeof&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> <span class="Constant">35</span>  <span class="Constant">3</span> <span class="Constant">36</span>
+                       <span class="Comment">; result</span>
+                       <span class="Constant">4</span> <span class="Constant">34</span>  <span class="Constant">5</span> <span class="Constant">35</span>  <span class="Constant">6</span> <span class="Constant">36</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - ops can operate on records with fields spanning multiple locations&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 20</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">100</span>
+
+<span class="Comment">; A special kind of record is the 'tagged type'. It lets us represent</span>
+<span class="Comment">; dynamically typed values, which save type information in memory rather than</span>
+<span class="Comment">; in the code to use them. This will let us do things like create heterogenous</span>
+<span class="Comment">; lists containing both integers and strings. Tagged values admit two</span>
+<span class="Comment">; operations:</span>
+<span class="Comment">;</span>
+<span class="Comment">;   'save-type' - turns a regular value into a tagged-value of the appropriate type</span>
+<span class="Comment">;   'maybe-coerce' - turns a tagged value into a regular value if the type matches</span>
+<span class="Comment">;</span>
+<span class="Comment">; The payload of a tagged value must occupy just one location. Save pointers</span>
+<span class="Comment">; to records.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;tagged-value&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;cvt0&quot; &quot;cvt1&quot;)))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:type <span class="Op">&lt;-</span> copy integer:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Constant">4</span>:boolean <span class="Op">&lt;-</span> maybe-coerce <span class="Constant">1</span>:tagged-value integer:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn completed-routines*)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">34</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.4</span> t<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'maybe-coerce' copies value only if type tag matches&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;tagged-value-2&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:type <span class="Op">&lt;-</span> copy integer-address:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Constant">4</span>:boolean <span class="Op">&lt;-</span> maybe-coerce <span class="Constant">1</span>:tagged-value boolean:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">0</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.4</span> nil<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'maybe-coerce' doesn't copy value when type tag doesn't match&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;save-type&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">1</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj  <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> <span class="Delimiter">'</span>integer  <span class="Constant">3</span> <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'save-type' saves the type of a value at runtime, turning it into a tagged-value&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;init-tagged-value&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value integer:literal <span class="Constant">1</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Constant">4</span>:boolean <span class="Op">&lt;-</span> maybe-coerce <span class="Constant">2</span>:tagged-value-address/deref integer:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;cvt0&quot; &quot;cvt1&quot; &quot;sizeof&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">34</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.4</span> t<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'init-tagged-value' is the converse of 'maybe-coerce'&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; Now that we can package values together with their types, we can construct a</span>
+<span class="Comment">; dynamically typed list.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;list&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Comment">; 1 points at first node: tagged-value (int 34)</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:list-address <span class="Op">&lt;-</span> new list:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:tagged-value-address <span class="Op">&lt;-</span> list-value-address <span class="Constant">1</span>:list-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:type-address <span class="Op">&lt;-</span> get-address <span class="Constant">2</span>:tagged-value-address/deref type:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:type-address/deref <span class="Op">&lt;-</span> copy integer:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:location <span class="Op">&lt;-</span> get-address <span class="Constant">2</span>:tagged-value-address/deref payload:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:location/deref <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:list-address-address <span class="Op">&lt;-</span> get-address <span class="Constant">1</span>:list-address/deref cdr:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:list-address-address/deref <span class="Op">&lt;-</span> new list:literal<span class="Delimiter">)</span></span>
+      <span class="Comment">; 6 points at second node: tagged-value (boolean t)</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:list-address <span class="Op">&lt;-</span> copy <span class="Constant">5</span>:list-address-address/deref<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:tagged-value-address <span class="Op">&lt;-</span> list-value-address <span class="Constant">6</span>:list-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:type-address <span class="Op">&lt;-</span> get-address <span class="Constant">7</span>:tagged-value-address/deref type:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:type-address/deref <span class="Op">&lt;-</span> copy boolean:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">9</span>:location <span class="Op">&lt;-</span> get-address <span class="Constant">7</span>:tagged-value-address/deref payload:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">9</span>:location/deref <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">10</span>:list-address <span class="Op">&lt;-</span> get <span class="Constant">6</span>:list-address/deref <span class="MuConstant">1</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> first rep.routine!alloc
+<span class="CommentedCode">;?     (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="CommentedCode">;?     (set dump-trace*)</span>
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+<span class="CommentedCode">;?     (prn memory*)</span>
+    <span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+      <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~all first <span class="Delimiter">(</span>map <span class="Global">memory*</span> <span class="Delimiter">'(</span><span class="Constant">1</span> <span class="Constant">2</span> <span class="Constant">3</span><span class="Delimiter">)))</span>
+            <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.first  <span class="Delimiter">'</span>integer<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.4</span> <span class="Delimiter">(</span>+ first <span class="Constant">1</span><span class="Delimiter">))</span>
+            <span class="Delimiter">(</span>~is <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ first <span class="Constant">1</span><span class="Delimiter">))</span>  <span class="Constant">34</span><span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.5</span> <span class="Delimiter">(</span>+ first <span class="Constant">2</span><span class="Delimiter">))</span>
+            <span class="Delimiter">(</span><span class="Normal">let</span> second <span class="Global">memory*</span><span class="Constant">.6</span>
+              <span class="Delimiter">(</span><span class="Normal">or</span>
+                <span class="Delimiter">(</span>~is <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ first <span class="Constant">2</span><span class="Delimiter">))</span> second<span class="Delimiter">)</span>
+                <span class="Delimiter">(</span>~all second <span class="Delimiter">(</span>map <span class="Global">memory*</span> <span class="Delimiter">'(</span><span class="Constant">6</span> <span class="Constant">7</span> <span class="Constant">8</span><span class="Delimiter">)))</span>
+                <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.second <span class="Delimiter">'</span>boolean<span class="Delimiter">)</span>
+                <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.9</span> <span class="Delimiter">(</span>+ second <span class="Constant">1</span><span class="Delimiter">))</span>
+                <span class="Delimiter">(</span>~is <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ second <span class="Constant">1</span><span class="Delimiter">))</span> t<span class="Delimiter">)</span>
+                <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.10</span> nil<span class="Delimiter">))))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - lists can contain elements of different types&quot;</span><span class="Delimiter">))))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">10</span>:list-address <span class="Op">&lt;-</span> list-next <span class="Constant">1</span>:list-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>test2<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.10</span> <span class="Global">memory*</span><span class="Constant">.6</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'list-next can move a list pointer to the next node&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; 'init-list' takes a variable number of args and constructs a list containing</span>
+<span class="Comment">; them. Just integers for now.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;init-list&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> init-list <span class="MuConstant">3</span>:literal <span class="MuConstant">4</span>:literal <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;cvt0&quot; &quot;cvt1&quot; &quot;sizeof&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> first <span class="Global">memory*</span><span class="Constant">.1</span>
+<span class="CommentedCode">;?   (prn first)</span>
+  <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.first  <span class="Delimiter">'</span>integer<span class="Delimiter">)</span>
+          <span class="Delimiter">(</span>~is <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ first <span class="Constant">1</span><span class="Delimiter">))</span>  <span class="Constant">3</span><span class="Delimiter">)</span>
+          <span class="Delimiter">(</span><span class="Normal">let</span> second <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ first <span class="Constant">2</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;?             (prn second)</span>
+            <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.second <span class="Delimiter">'</span>integer<span class="Delimiter">)</span>
+                <span class="Delimiter">(</span>~is <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ second <span class="Constant">1</span><span class="Delimiter">))</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+                <span class="Delimiter">(</span><span class="Normal">let</span> third <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ second <span class="Constant">2</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;?                   (prn third)</span>
+                  <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.third <span class="Delimiter">'</span>integer<span class="Delimiter">)</span>
+                      <span class="Delimiter">(</span>~is <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ third <span class="Constant">1</span><span class="Delimiter">))</span> <span class="Constant">5</span><span class="Delimiter">)</span>
+                      <span class="Delimiter">(</span>~is <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ third <span class="Constant">2</span><span class="Delimiter">)</span> nil<span class="Delimiter">)))))))</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'init-list' can construct a list of integers&quot;</span><span class="Delimiter">)))</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 100</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">20</span>
+
+<span class="SalientComment">;; Functions</span>
+<span class="Comment">;</span>
+<span class="Comment">; Just like the table of types is centralized, functions are conceptualized as</span>
+<span class="Comment">; a centralized table of operations just like the &quot;primitives&quot; we've seen so</span>
+<span class="Comment">; far. If you create a function you can call it like any other op.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">1</span>  <span class="Constant">2</span> <span class="Constant">3</span>  <span class="Constant">3</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - calling a user-defined function runs its instructions&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-once&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">2</span> <span class="Global">curr-cycle*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - calling a user-defined function runs its instructions exactly once &quot;</span> <span class="Global">curr-cycle*</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; User-defined functions communicate with their callers through two</span>
+<span class="Comment">; primitives:</span>
+<span class="Comment">;</span>
+<span class="Comment">;   'arg' - to access inputs</span>
+<span class="Comment">;   'reply' - to return outputs</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-reply&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">1</span>  <span class="Constant">2</span> <span class="Constant">3</span>  <span class="Constant">3</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'reply' stops executing the current function&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-reply-nested&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> test2<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function test2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">2</span> <span class="Constant">34</span>  <span class="Constant">3</span> <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'reply' stops executing any callers as necessary&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-reply-once&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">5</span> <span class="Global">curr-cycle*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'reply' executes instructions exactly once &quot;</span> <span class="Global">curr-cycle*</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;reply-increments-caller-pc&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function callee <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function caller <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>freeze <span class="Global">function*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> <span class="Delimiter">(</span>make-routine <span class="Delimiter">'</span>caller<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is <span class="Constant">0</span> pc.routine*<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>push-stack <span class="Global">routine*</span> <span class="Delimiter">'</span>callee<span class="Delimiter">)</span>  <span class="Comment">; pretend call was at first instruction of caller</span>
+<span class="Delimiter">(</span>run-for-time-slice <span class="Constant">1</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> pc.routine*<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'reply' should increment pc in caller (to move past calling instruction)&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-sequential&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">4</span>:integer <span class="Constant">5</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">1</span>  <span class="Constant">2</span> <span class="Constant">3</span>  <span class="Constant">3</span> <span class="Constant">4</span>
+                       <span class="Comment">; test1's temporaries</span>
+                       <span class="Constant">4</span> <span class="Constant">1</span>  <span class="Constant">5</span> <span class="Constant">3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'arg' accesses in order the operands of the most recent function call (the caller)&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-random-access&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">input</span> <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">input</span> <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">4</span>:integer <span class="Constant">5</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; should never run</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">1</span>  <span class="Constant">2</span> <span class="Constant">3</span>  <span class="Constant">3</span> <span class="Constant">4</span>
+                       <span class="Comment">; test's temporaries</span>
+                       <span class="Constant">4</span> <span class="Constant">1</span>  <span class="Constant">5</span> <span class="Constant">3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'arg' with index can access function call arguments out of order&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-random-then-sequential&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>_ <span class="Op">&lt;-</span> <span class="Identifier">input</span> <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span>  <span class="Comment">; takes next arg after index 1</span></span>
+     <span class="Delimiter">])</span>  <span class="Comment">; should never run</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="MuConstant">1</span>:literal <span class="MuConstant">2</span>:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'arg' with index resets index for later calls&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-status&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">4</span> <span class="Constant">1</span>  <span class="Constant">5</span> t<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'arg' sets a second oarg when arg exists&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-missing&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">4</span> <span class="Constant">1</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - missing 'arg' doesn't cause error&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-missing-2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">4</span> <span class="Constant">1</span>  <span class="Constant">6</span> nil<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - missing 'arg' wipes second oarg when provided&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-missing-3&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+    <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+    <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">4</span> <span class="Constant">1</span>  <span class="Constant">6</span> nil<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - missing 'arg' consistently wipes its oarg&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-missing-4&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Comment">; if given two args, adds them; if given one arg, increments</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-if</span> <span class="Constant">6</span>:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      }
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">4</span>:integer <span class="Constant">5</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">4</span> <span class="Constant">34</span>  <span class="Constant">5</span> <span class="Constant">1</span>  <span class="Constant">6</span> nil  <span class="Constant">7</span> <span class="Constant">35</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - function with optional second arg&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-arg-by-value&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; overwrite caller memory</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>  <span class="Comment">; arg not clobbered</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="Constant">1</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">0</span>  <span class="Constant">2</span> <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'arg' passes by value&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;arg-record&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer-boolean-pair <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="Constant">1</span>:integer-boolean-pair<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> nil  <span class="Constant">4</span> <span class="Constant">34</span>  <span class="Constant">5</span> nil<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'arg' can copy records spanning multiple locations&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;arg-record-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer-boolean-pair <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer-boolean-pair-address <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>test1 <span class="Constant">3</span>:integer-boolean-pair-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">34</span>  <span class="Constant">2</span> nil  <span class="Constant">3</span> <span class="Constant">1</span>  <span class="Constant">4</span> <span class="Constant">34</span>  <span class="Constant">5</span> nil<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'arg' can copy records spanning multiple locations in indirect mode&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-reply-oarg&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">4</span>:integer <span class="Constant">5</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">6</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> test1 <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">1</span>  <span class="Constant">2</span> <span class="Constant">3</span>  <span class="Constant">3</span> <span class="Constant">4</span>
+                       <span class="Comment">; test1's temporaries</span>
+                       <span class="Constant">4</span> <span class="Constant">1</span>  <span class="Constant">5</span> <span class="Constant">3</span>  <span class="Constant">6</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'reply' can take aguments that are returned, or written back into output args of caller&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-reply-oarg-multiple&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">4</span>:integer <span class="Constant">5</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">6</span>:integer <span class="Constant">5</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Constant">7</span>:integer <span class="Op">&lt;-</span> test1 <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">1</span>  <span class="Constant">2</span> <span class="Constant">3</span>  <span class="Constant">3</span> <span class="Constant">4</span>    <span class="Constant">7</span> <span class="Constant">3</span>
+                         <span class="Comment">; test1's temporaries</span>
+                         <span class="Constant">4</span> <span class="Constant">1</span>  <span class="Constant">5</span> <span class="Constant">3</span>  <span class="Constant">6</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'reply' permits a function to return multiple values at once&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-fn-prepare-reply&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">4</span>:integer <span class="Constant">5</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>prepare-reply <span class="Constant">6</span>:integer <span class="Constant">5</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Constant">7</span>:integer <span class="Op">&lt;-</span> test1 <span class="Constant">1</span>:integer <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">1</span>  <span class="Constant">2</span> <span class="Constant">3</span>  <span class="Constant">3</span> <span class="Constant">4</span>    <span class="Constant">7</span> <span class="Constant">3</span>
+                         <span class="Comment">; test1's temporaries</span>
+                         <span class="Constant">4</span> <span class="Constant">1</span>  <span class="Constant">5</span> <span class="Constant">3</span>  <span class="Constant">6</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - without args, 'reply' returns values from previous 'prepare-reply'.&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 20</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">11</span>
+
+<span class="SalientComment">;; Structured programming</span>
+<span class="Comment">;</span>
+<span class="Comment">; Our jump operators are quite inconvenient to use, so mu provides a</span>
+<span class="Comment">; lightweight tool called 'convert-braces' to work in a slightly more</span>
+<span class="Comment">; convenient format with nested braces:</span>
+<span class="Comment">;</span>
+<span class="Comment">;   {</span>
+<span class="Comment">;     some instructions</span>
+<span class="Comment">;     {</span>
+<span class="Comment">;       more instructions</span>
+<span class="Comment">;     }</span>
+<span class="Comment">;   }</span>
+<span class="Comment">;</span>
+<span class="Comment">; Braces are like labels in assembly language, they require no special</span>
+<span class="Comment">; parsing. The operations 'loop' and 'break' jump to just after the enclosing</span>
+<span class="Comment">; '{' and '}' respectively.</span>
+<span class="Comment">;</span>
+<span class="Comment">; Conditional and unconditional 'loop' and 'break' should give us 80% of the</span>
+<span class="Comment">; benefits of the control-flow primitives we're used to in other languages,</span>
+<span class="Comment">; like 'if', 'while', 'for', etc.</span>
+<span class="Comment">;</span>
+<span class="Comment">; Compare 'unquoted blocks' using {} with 'quoted blocks' using [] that we've</span>
+<span class="Comment">; gotten used to seeing. Quoted blocks are used by top-level instructions to</span>
+<span class="Comment">; provide code without running it.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-braces&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;c{0&quot; &quot;c{1&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>  <span class="Comment">; 'begin' is just a hack because racket turns braces into parens</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>not-equal<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">3</span> integer<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break-if</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">4</span> boolean<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              }
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">reply</span><span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>not-equal<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">3</span> integer<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump-if</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">reply</span><span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-braces replaces break-if with a jump-if to after the next close-brace&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-braces-empty-block&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;c{0&quot; &quot;c{1&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break</span><span class="Delimiter">)))</span></span>
+              }
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">reply</span><span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">reply</span><span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-braces works for degenerate blocks&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-braces-nested-break&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>not-equal<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">3</span> integer<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break-if</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">4</span> boolean<span class="Delimiter">)))</span></span>
+                { <span class="CommentedCode">begin</span>
+                  <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                }
+              }
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">reply</span><span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>not-equal<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">3</span> integer<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump-if</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">reply</span><span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-braces balances braces when converting break&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-braces-repeated-jump&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;c{0&quot; &quot;c{1&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break</span><span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              }
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break</span><span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              }
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-braces handles jumps on jumps&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-braces-nested-loop&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                { <span class="CommentedCode">begin</span>
+                  <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>not-equal<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">3</span> integer<span class="Delimiter">)))</span></span>
+                }
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">loop-if</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">4</span> boolean<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              }
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">reply</span><span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>not-equal<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">3</span> integer<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump-if</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">4</span> boolean<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">-3</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">reply</span><span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-braces balances braces when converting 'loop'&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-braces-label&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              foo
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            foo
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-braces skips past labels&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-braces-label-increments-offset&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break</span><span class="Delimiter">)))</span></span>
+                foo
+              }
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))</span>
+            foo
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-braces treats labels as instructions&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-braces-label-increments-offset2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;c{0&quot; &quot;c{1&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break</span><span class="Delimiter">)))</span></span>
+                foo
+              }
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break</span><span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              }
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))</span></span>
+            foo
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-braces treats labels as instructions - 2&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;break-multiple&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;-&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                { <span class="CommentedCode">begin</span>
+                  <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">break</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">2</span> blocks<span class="Delimiter">)))</span></span>
+                }
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              }<span class="Delimiter">))</span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">4</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'break' can take an extra arg with number of nested blocks to exit&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;loop&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">loop</span><span class="Delimiter">)))</span></span>
+              }<span class="Delimiter">))</span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">-2</span> offset<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'loop' jumps to start of containing block&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; todo: fuzz-test invariant: convert-braces offsets should be robust to any</span>
+<span class="Comment">; number of inner blocks inside but not around the loop block.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;loop-nested&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                { <span class="CommentedCode">begin</span>
+                  <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                }
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">loop</span><span class="Delimiter">)))</span></span>
+              }<span class="Delimiter">))</span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">-3</span> offset<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'loop' correctly jumps back past nested braces&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;loop-multiple&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;-&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-braces
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              { <span class="CommentedCode">begin</span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+                { <span class="CommentedCode">begin</span>
+                  <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">loop</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">2</span> blocks<span class="Delimiter">)))</span></span>
+                }
+              }<span class="Delimiter">))</span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">-3</span> offset<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'loop' can take an extra arg with number of nested blocks to exit&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-labels&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-labels
+            <span class="Mu"><span class="Delimiter">'(</span><span class="Identifier">loop</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Identifier">loop</span> offset<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'(</span><span class="Identifier">loop</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Identifier">jump</span><span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">-2</span> offset<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'convert-labels' rewrites jumps to labels&quot;</span><span class="Delimiter">))</span>
+
+<span class="SalientComment">;; Variables</span>
+<span class="Comment">;</span>
+<span class="Comment">; A big convenience high-level languages provide is the ability to name memory</span>
+<span class="Comment">; locations. In mu, a lightweight tool called 'convert-names' provides this</span>
+<span class="Comment">; convenience.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span>y integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span>z integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names renames symbolic names to integer locations&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-compound&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Comment">; copying 0 into pair is meaningless; just for testing</span>
+            <span class="Mu"><span class="Delimiter">'((((</span>x integer-boolean-pair<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span>y integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names increments integer locations by the size of the type of the previous var&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-nil&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span>y integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Comment">; nil location is meaningless; just for testing</span>
+              <span class="Mu"><span class="Delimiter">(((</span>nil integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span>nil integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names never renames nil&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-string&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer-address<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>new<span class="Delimiter">))</span> <span class="Constant">&quot;foo&quot;</span><span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer-address<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>new<span class="Delimiter">))</span> <span class="Constant">&quot;foo&quot;</span><span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;convert-names passes through raw strings (just a convenience arg for 'new')&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-raw&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span>y integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>raw<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span>y integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>raw<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names never renames raw operands&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Comment">; meaningless; just for testing</span>
+            <span class="Mu"><span class="Delimiter">'((((</span>x literal<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span>x literal<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names never renames literals&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-literal-2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span>x boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span>x literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span>x literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names never renames literals, even when the name matches a variable&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; kludgy support for 'fork' below</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-functions&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span>y integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Comment">; meaningless; just for testing</span>
+              <span class="Mu"><span class="Delimiter">(((</span>z fn<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span>z fn<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names never renames fns&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-record-fields&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;cn0&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">34</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Delimiter">((</span>bool offset<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">34</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names replaces record field offsets&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-record-fields-ambiguous&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>errsafe <span class="Delimiter">(</span>convert-names
+               <span class="Mu"><span class="Delimiter">'((((</span>bool boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">t</span> literal<span class="Delimiter">)))</span></span>
+                 <span class="Mu"><span class="Delimiter">(((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">34</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Delimiter">((</span>bool offset<span class="Delimiter">))))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names doesn't allow offsets and variables with the same name in a function&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-record-fields-ambiguous-2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>errsafe <span class="Delimiter">(</span>convert-names
+               <span class="Mu"><span class="Delimiter">'((((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">34</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Delimiter">((</span>bool offset<span class="Delimiter">)))</span></span>
+                 <span class="Mu"><span class="Delimiter">(((</span>bool boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">t</span> literal<span class="Delimiter">))))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names doesn't allow offsets and variables with the same name in a function - 2&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-record-fields-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;cn0&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">34</span> integer-boolean-pair-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))</span> <span class="Delimiter">((</span>bool offset<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">34</span> integer-boolean-pair-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names replaces field offsets for record addresses&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-record-fields-multiple&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">2</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Delimiter">((</span>bool offset<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Delimiter">((</span>bool offset<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">2</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">3</span> boolean<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>get<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer-boolean-pair<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">1</span> offset<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names replaces field offsets with multiple mentions&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-label&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+              foo<span class="Delimiter">))</span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            foo<span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names skips past labels&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 11</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">20</span>
+
+<span class="Comment">; A rudimentary memory allocator. Eventually we want to write this in mu.</span>
+<span class="Comment">;</span>
+<span class="Comment">; No deallocation yet; let's see how much code we can build in mu before we</span>
+<span class="Comment">; feel the need for it.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-primitive&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer-address <span class="Op">&lt;-</span> new integer:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> before rep.routine!alloc
+<span class="CommentedCode">;?     (set dump-trace*)</span>
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+<span class="CommentedCode">;?     (prn memory*)</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.1</span> before<span class="Delimiter">)</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' returns current high-water mark&quot;</span><span class="Delimiter">))</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso rep.routine!alloc <span class="Delimiter">(</span>+ before <span class="Constant">1</span><span class="Delimiter">))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' on primitive types increments high-water mark by their size&quot;</span><span class="Delimiter">))))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-array-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:type-array-address <span class="Op">&lt;-</span> new type-array:literal <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> before rep.routine!alloc
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+<span class="CommentedCode">;?     (prn memory*)</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.1</span> before<span class="Delimiter">)</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' on array with literal size returns current high-water mark&quot;</span><span class="Delimiter">))</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso rep.routine!alloc <span class="Delimiter">(</span>+ before <span class="Constant">6</span><span class="Delimiter">))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' on primitive arrays increments high-water mark by their size&quot;</span><span class="Delimiter">))))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;new-array-direct&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:type-array-address <span class="Op">&lt;-</span> new type-array:literal <span class="Constant">1</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> before rep.routine!alloc
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+<span class="CommentedCode">;?     (prn memory*)</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.2</span> before<span class="Delimiter">)</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' on array with variable size returns current high-water mark&quot;</span><span class="Delimiter">))</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso rep.routine!alloc <span class="Delimiter">(</span>+ before <span class="Constant">6</span><span class="Delimiter">))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' on primitive arrays increments high-water mark by their (variable) size&quot;</span><span class="Delimiter">))))</span>
+
+<span class="Comment">; Even though our memory locations can now have names, the names are all</span>
+<span class="Comment">; globals, accessible from any function. To isolate functions from their</span>
+<span class="Comment">; callers we need local variables, and mu provides them using a special</span>
+<span class="Comment">; variable called default-space. When you initialize such a variable (likely</span>
+<span class="Comment">; with a call to our just-defined memory allocator) mu interprets memory</span>
+<span class="Comment">; locations as offsets from its value. If default-space is set to 1000, for</span>
+<span class="Comment">; example, reads and writes to memory location 1 will really go to 1001.</span>
+<span class="Comment">;</span>
+<span class="Comment">; 'default-space' is itself hard-coded to be function-local; it's nil in a new</span>
+<span class="Comment">; function, and it's restored when functions return to their callers. But the</span>
+<span class="Comment">; actual space allocation is independent. So you can define closures, or do</span>
+<span class="Comment">; even more funky things like share locals between two coroutines.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;set-default-space&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> before rep.routine!alloc
+<span class="CommentedCode">;?     (set dump-trace*)</span>
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+<span class="CommentedCode">;?     (prn memory*)</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~and <span class="Delimiter">(</span>~is <span class="Constant">23</span> <span class="Global">memory*</span><span class="Constant">.1</span><span class="Delimiter">)</span>
+              <span class="Delimiter">(</span>is <span class="Constant">23</span> <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ before <span class="Constant">2</span><span class="Delimiter">))))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - default-space implicitly modifies variable locations&quot;</span><span class="Delimiter">))))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;set-default-space-skips-offset&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> before rep.routine!alloc
+<span class="CommentedCode">;?     (set dump-trace*)</span>
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+<span class="CommentedCode">;?     (prn memory*)</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~and <span class="Delimiter">(</span>~is <span class="Constant">23</span> <span class="Global">memory*</span><span class="Constant">.1</span><span class="Delimiter">)</span>
+              <span class="Delimiter">(</span>is <span class="Constant">23</span> <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ before <span class="Constant">2</span><span class="Delimiter">))))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - default-space skips 'offset' types just like literals&quot;</span><span class="Delimiter">))))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;default-space-bounds-check&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine <span class="Delimiter">(</span>car <span class="Global">completed-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>no rep.routine!error<span class="Delimiter">)</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - default-space checks bounds&quot;</span><span class="Delimiter">)))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;default-space-and-get-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer-boolean-pair-address <span class="Op">&lt;-</span> new integer-boolean-pair:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer-address <span class="Op">&lt;-</span> get-address <span class="Constant">1</span>:integer-boolean-pair-address/deref <span class="MuConstant">0</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer-address/deref <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer/raw <span class="Op">&lt;-</span> get <span class="Constant">1</span>:integer-boolean-pair-address/deref <span class="MuConstant">0</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;cvt0&quot; &quot;cvt1&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="CommentedCode">;? (prn completed-routines*)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">34</span> <span class="Global">memory*</span><span class="Constant">.3</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - indirect 'get' works in the presence of default-space&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;default-space-and-index-indirect&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer-array-address <span class="Op">&lt;-</span> new integer-array:literal <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer-address <span class="Op">&lt;-</span> index-address <span class="Constant">1</span>:integer-array-address/deref <span class="MuConstant">2</span>:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer-address/deref <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer/raw <span class="Op">&lt;-</span> index <span class="Constant">1</span>:integer-array-address/deref <span class="MuConstant">2</span>:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;array-info&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="CommentedCode">;? (prn completed-routines*)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">34</span> <span class="Global">memory*</span><span class="Constant">.3</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - indirect 'index' works in the presence of default-space&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-names-default-space&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-names
+            <span class="Mu"><span class="Delimiter">'((((</span>x integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">4</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Mu"><span class="Delimiter">(((</span>y integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">2</span> literal<span class="Delimiter">)))</span></span>
+              <span class="Comment">; unsafe in general; don't write random values to 'default-space'</span>
+              <span class="Mu"><span class="Delimiter">(((</span>default-space integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>add<span class="Delimiter">))</span> <span class="Delimiter">((</span>x integer<span class="Delimiter">))</span> <span class="Delimiter">((</span>y integer<span class="Delimiter">)))))</span></span>
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">4</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">2</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span>default-space integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>add<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="Constant">2</span> integer<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-names never renames default-space&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;suppress-default-space&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer/raw <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> before rep.routine!alloc
+<span class="CommentedCode">;?     (set dump-trace*)</span>
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+<span class="CommentedCode">;?     (prn memory*)</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~and <span class="Delimiter">(</span>is <span class="Constant">23</span> <span class="Global">memory*</span><span class="Constant">.1</span><span class="Delimiter">)</span>
+              <span class="Delimiter">(</span>~is <span class="Constant">23</span> <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ before <span class="Constant">1</span><span class="Delimiter">))))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - default-space skipped for locations with metadata 'raw'&quot;</span><span class="Delimiter">))))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;array-copy-indirect-scoped&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">10</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; pretend allocation</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> copy <span class="MuConstant">10</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; raw location 12</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer-boolean-pair-array-address <span class="Op">&lt;-</span> copy <span class="MuConstant">12</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer-boolean-pair-array <span class="Op">&lt;-</span> copy <span class="Constant">6</span>:integer-boolean-pair-array-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;m&quot; &quot;sizeof&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.18</span> <span class="Constant">2</span><span class="Delimiter">)</span>  <span class="Comment">; variable 7</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - indirect array copy in the presence of 'default-space'&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;len-array-indirect-scoped&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">10</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; pretend allocation</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> copy <span class="MuConstant">10</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; raw location 12</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> copy <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer-address <span class="Op">&lt;-</span> copy <span class="MuConstant">12</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; unsafe</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> length <span class="Constant">6</span>:integer-boolean-pair-array-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;addr&quot; &quot;sz&quot; &quot;array-len&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.18</span> <span class="Constant">2</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'len' accesses length of array address&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;default-space-shared&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function init-counter <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; initialize to 3</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> default-space:space-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function increment-counter <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">1</span>:integer <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; increment</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">1</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:space-address <span class="Op">&lt;-</span> init-counter<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> increment-counter <span class="Constant">1</span>:space-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> increment-counter <span class="Constant">1</span>:space-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">5</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - multiple calls to a function can share locals&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;default-space-closure&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function init-counter <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; initialize to 3</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> default-space:space-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function increment-counter <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">0</span>:space-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span>  <span class="Comment">; share outer space</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer/space:1 <span class="Op">&lt;-</span> add <span class="Constant">1</span>:integer/space:1 <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; increment</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; dummy</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">1</span>:integer/space:1<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:space-address <span class="Op">&lt;-</span> init-counter<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> increment-counter <span class="Constant">1</span>:space-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> increment-counter <span class="Constant">1</span>:space-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">5</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - multiple calls to a function can share locals&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;default-space-closure-with-names&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function init-counter <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>x:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>y:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; correct copy of y</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> default-space:space-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function increment-counter <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">0</span>:space-address/names:init-counter <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span>  <span class="Comment">; outer space must be created by 'init-counter' above</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>y:integer/space:1 <span class="Op">&lt;-</span> add y:integer/space:1 <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; increment</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>y:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; dummy</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> y:integer/space:1<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:space-address/names:init-counter <span class="Op">&lt;-</span> init-counter<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> increment-counter <span class="Constant">1</span>:space-address/names:init-counter<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> increment-counter <span class="Constant">1</span>:space-address/names:init-counter<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">5</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - multiple calls to a function can share locals&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 20</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">100</span>
+
+<span class="SalientComment">;; Dynamic dispatch</span>
+<span class="Comment">;</span>
+<span class="Comment">; Putting it all together, here's how you define generic functions that run</span>
+<span class="Comment">; different code based on the types of their args.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;dispatch-clause&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Comment">; doesn't matter too much how many locals you allocate space for (here 20)</span>
+      <span class="Comment">; if it's slightly too many -- memory is plentiful</span>
+      <span class="Comment">; if it's too few -- mu will raise an error</span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">20</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>first-arg-box:tagged-value-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Comment">; if given integers, add them</span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span>first-arg:integer match?:boolean <span class="Op">&lt;-</span> maybe-coerce first-arg-box:tagged-value-address/deref integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> match?:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg-box:tagged-value-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg:integer <span class="Op">&lt;-</span> maybe-coerce second-arg-box:tagged-value-address/deref integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>result:integer <span class="Op">&lt;-</span> add first-arg:integer second-arg:integer<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> result:integer<span class="Delimiter">)</span></span>
+      }
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value integer:literal <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value integer:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> test1 <span class="Constant">1</span>:tagged-value-address <span class="Constant">2</span>:tagged-value-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">37</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - an example function that checks that its oarg is an integer&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;dispatch-multiple-clauses&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">20</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>first-arg-box:tagged-value-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Comment">; if given integers, add them</span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span>first-arg:integer match?:boolean <span class="Op">&lt;-</span> maybe-coerce first-arg-box:tagged-value-address/deref integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> match?:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg-box:tagged-value-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg:integer <span class="Op">&lt;-</span> maybe-coerce second-arg-box:tagged-value-address/deref integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>result:integer <span class="Op">&lt;-</span> add first-arg:integer second-arg:integer<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> result:integer<span class="Delimiter">)</span></span>
+      }
+      <span class="Comment">; if given booleans, or them (it's a silly kind of generic function)</span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span>first-arg:boolean match?:boolean <span class="Op">&lt;-</span> maybe-coerce first-arg-box:tagged-value-address/deref boolean:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> match?:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg-box:tagged-value-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg:boolean <span class="Op">&lt;-</span> maybe-coerce second-arg-box:tagged-value-address/deref boolean:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>result:boolean <span class="Op">&lt;-</span> or first-arg:boolean second-arg:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> result:integer<span class="Delimiter">)</span></span>
+      }
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value boolean:literal <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value boolean:literal <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> test1 <span class="Constant">1</span>:tagged-value-address <span class="Constant">2</span>:tagged-value-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (each stmt function*!test-fn</span>
+<span class="CommentedCode">;?   (prn &quot;  &quot; stmt))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (wipe dump-trace*)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.3</span> t<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - an example function that can do different things (dispatch) based on the type of its args or oargs&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;dispatch-multiple-calls&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">20</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>first-arg-box:tagged-value-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Comment">; if given integers, add them</span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span>first-arg:integer match?:boolean <span class="Op">&lt;-</span> maybe-coerce first-arg-box:tagged-value-address/deref integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> match?:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg-box:tagged-value-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg:integer <span class="Op">&lt;-</span> maybe-coerce second-arg-box:tagged-value-address/deref integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>result:integer <span class="Op">&lt;-</span> add first-arg:integer second-arg:integer<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> result:integer<span class="Delimiter">)</span></span>
+      }
+      <span class="Comment">; if given booleans, or them (it's a silly kind of generic function)</span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span>first-arg:boolean match?:boolean <span class="Op">&lt;-</span> maybe-coerce first-arg-box:tagged-value-address/deref boolean:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> match?:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg-box:tagged-value-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>second-arg:boolean <span class="Op">&lt;-</span> maybe-coerce second-arg-box:tagged-value-address/deref boolean:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span>result:boolean <span class="Op">&lt;-</span> or first-arg:boolean second-arg:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> result:integer<span class="Delimiter">)</span></span>
+      }
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value boolean:literal <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value boolean:literal <span class="MuConstant">nil</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> test1 <span class="Constant">1</span>:tagged-value-address <span class="Constant">2</span>:tagged-value-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">10</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value integer:literal <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">11</span>:tagged-value-address <span class="Op">&lt;-</span> init-tagged-value integer:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">12</span>:integer <span class="Op">&lt;-</span> test1 <span class="Constant">10</span>:tagged-value-address <span class="Constant">11</span>:tagged-value-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~and <span class="Delimiter">(</span>is <span class="Global">memory*</span><span class="Constant">.3</span> t<span class="Delimiter">)</span> <span class="Delimiter">(</span>is <span class="Global">memory*</span><span class="Constant">.12</span> <span class="Constant">37</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - different calls can exercise different clauses of the same function&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; We can also dispatch based on the type of the operands or results at the</span>
+<span class="Comment">; caller.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;dispatch-otype&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:type <span class="Op">&lt;-</span> otype <span class="MuConstant">0</span>:offset<span class="Delimiter">)</span></span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> equal <span class="Constant">4</span>:type integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> <span class="Constant">5</span>:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">6</span>:integer <span class="Constant">7</span>:integer<span class="Delimiter">)</span></span>
+      }
+      <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">8</span>:integer<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> test1 <span class="MuConstant">1</span>:literal <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - an example function that checks that its oarg is an integer&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;dispatch-otype-multiple-clauses&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:type <span class="Op">&lt;-</span> otype <span class="MuConstant">0</span>:offset<span class="Delimiter">)</span></span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Comment">; integer needed? add args</span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> equal <span class="Constant">4</span>:type integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> <span class="Constant">5</span>:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">6</span>:integer <span class="Constant">7</span>:integer<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">8</span>:integer<span class="Delimiter">)</span></span>
+      }
+      { <span class="CommentedCode">begin</span>
+        <span class="Comment">; boolean needed? 'or' args</span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> equal <span class="Constant">4</span>:type boolean:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> <span class="Constant">5</span>:boolean <span class="MuConstant">4</span>:offset<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:boolean <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:boolean <span class="Op">&lt;-</span> or <span class="Constant">6</span>:boolean <span class="Constant">7</span>:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">8</span>:boolean<span class="Delimiter">)</span></span>
+      }<span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> test1 <span class="MuConstant">t</span>:literal <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (each stmt function*!test1</span>
+<span class="CommentedCode">;?   (prn &quot;  &quot; stmt))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (wipe dump-trace*)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.1</span> t<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - an example function that can do different things (dispatch) based on the type of its args or oargs&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;dispatch-otype-multiple-calls&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function test1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:type <span class="Op">&lt;-</span> otype <span class="MuConstant">0</span>:offset<span class="Delimiter">)</span></span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> equal <span class="Constant">4</span>:type integer:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> <span class="Constant">5</span>:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">6</span>:integer <span class="Constant">7</span>:integer<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">8</span>:integer<span class="Delimiter">)</span></span>
+      }
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> equal <span class="Constant">4</span>:type boolean:literal<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">break-unless</span> <span class="Constant">5</span>:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:boolean <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:boolean <span class="Op">&lt;-</span> or <span class="Constant">6</span>:boolean <span class="Constant">7</span>:boolean<span class="Delimiter">)</span></span>
+        <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="Constant">8</span>:boolean<span class="Delimiter">)</span></span>
+      }<span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:boolean <span class="Op">&lt;-</span> test1 <span class="MuConstant">t</span>:literal <span class="MuConstant">t</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> test1 <span class="MuConstant">3</span>:literal <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~and <span class="Delimiter">(</span>is <span class="Global">memory*</span><span class="Constant">.1</span> t<span class="Delimiter">)</span> <span class="Delimiter">(</span>is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">7</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - different calls can exercise different clauses of the same function&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 100</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">20</span>
+
+<span class="SalientComment">;; Concurrency</span>
+<span class="Comment">;</span>
+<span class="Comment">; A rudimentary process scheduler. You can 'run' multiple functions at once,</span>
+<span class="Comment">; and they share the virtual processor.</span>
+<span class="Comment">;</span>
+<span class="Comment">; There's also a 'fork' primitive to let functions create new threads of</span>
+<span class="Comment">; execution (we call them routines).</span>
+<span class="Comment">;</span>
+<span class="Comment">; Eventually we want to allow callers to influence how much of their CPU they</span>
+<span class="Comment">; give to their 'children', or to rescind a child's running privileges.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1 <span class="Delimiter">'</span>f2<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>when <span class="Delimiter">(</span>~iso <span class="Constant">2</span> <span class="Global">curr-cycle*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler didn't run the right number of instructions: &quot;</span> <span class="Global">curr-cycle*</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span> <span class="Delimiter">(</span>obj <span class="Constant">1</span> <span class="Constant">3</span>  <span class="Constant">2</span> <span class="Constant">4</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler runs multiple functions: &quot;</span> <span class="Global">memory*</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>check-trace-contents <span class="Constant">&quot;scheduler orders functions correctly&quot;</span>
+  <span class="Delimiter">'((</span><span class="Constant">&quot;schedule&quot;</span> <span class="Constant">&quot;f1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;schedule&quot;</span> <span class="Constant">&quot;f2&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">))</span>
+<span class="Delimiter">(</span>check-trace-contents <span class="Constant">&quot;scheduler orders schedule and run events correctly&quot;</span>
+  <span class="Delimiter">'((</span><span class="Constant">&quot;schedule&quot;</span> <span class="Constant">&quot;f1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 0&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;schedule&quot;</span> <span class="Constant">&quot;f2&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f2 0&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler-alternate&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>= <span class="Global">scheduling-interval*</span> <span class="Constant">1</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1 <span class="Delimiter">'</span>f2<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>check-trace-contents <span class="Constant">&quot;scheduler alternates between routines&quot;</span>
+  <span class="Delimiter">'((</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 0&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f2 0&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f2 1&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler-sleep&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Comment">; add one baseline routine to run (empty running-routines* handled below)</span>
+<span class="Delimiter">(</span>enq make-routine!f1 <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is <span class="Constant">1</span> len.running-routines*<span class="Delimiter">))</span>
+<span class="Comment">; sleeping routine</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!f2
+  <span class="Mu"><span class="Delimiter">(</span>= rep.routine!sleep <span class="Delimiter">'(</span>for-some-cycles <span class="Constant">23</span><span class="Delimiter">))</span></span>
+  <span class="Delimiter">(</span>set <span class="Global">sleeping-routines*</span>.routine<span class="Delimiter">))</span>
+<span class="Comment">; not yet time for it to wake up</span>
+<span class="Delimiter">(</span>= <span class="Global">curr-cycle*</span> <span class="Constant">23</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;schedule&quot;)))</span>
+<span class="Delimiter">(</span>update-scheduler-state<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> len.running-routines*<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler lets routines sleep&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler-wakeup&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Comment">; add one baseline routine to run (empty running-routines* handled below)</span>
+<span class="Delimiter">(</span>enq make-routine!f1 <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is <span class="Constant">1</span> len.running-routines*<span class="Delimiter">))</span>
+<span class="Comment">; sleeping routine</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!f2
+  <span class="Delimiter">(</span>= rep.routine!sleep <span class="Delimiter">'(</span>for-some-cycles <span class="Constant">23</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>set <span class="Global">sleeping-routines*</span>.routine<span class="Delimiter">))</span>
+<span class="Comment">; time for it to wake up</span>
+<span class="Delimiter">(</span>= <span class="Global">curr-cycle*</span> <span class="Constant">24</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>update-scheduler-state<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">2</span> len.running-routines*<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler wakes up sleeping routines at the right time&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler-sleep-location&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Comment">; add one baseline routine to run (empty running-routines* handled below)</span>
+<span class="Delimiter">(</span>enq make-routine!f1 <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is <span class="Constant">1</span> len.running-routines*<span class="Delimiter">))</span>
+<span class="Comment">; blocked routine waiting for location 23 to change</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!f2
+  <span class="Delimiter">(</span>= rep.routine!sleep <span class="Delimiter">'(</span>until-location-changes <span class="Constant">23</span> <span class="Constant">0</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>set <span class="Global">sleeping-routines*</span>.routine<span class="Delimiter">))</span>
+<span class="Comment">; leave memory location 23 unchanged</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.23</span> <span class="Constant">0</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="CommentedCode">;? (prn running-routines*)</span>
+<span class="CommentedCode">;? (prn sleeping-routines*)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;schedule&quot;)))</span>
+<span class="Delimiter">(</span>update-scheduler-state<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn running-routines*)</span>
+<span class="CommentedCode">;? (prn sleeping-routines*)</span>
+<span class="Comment">; routine remains blocked</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> len.running-routines*<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler lets routines block on locations&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler-wakeup-location&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Comment">; add one baseline routine to run (empty running-routines* handled below)</span>
+<span class="Delimiter">(</span>enq make-routine!f1 <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is <span class="Constant">1</span> len.running-routines*<span class="Delimiter">))</span>
+<span class="Comment">; blocked routine waiting for location 23 to change</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!f2
+  <span class="Delimiter">(</span>= rep.routine!sleep <span class="Delimiter">'(</span>until-location-changes <span class="Constant">23</span> <span class="Constant">0</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>set <span class="Global">sleeping-routines*</span>.routine<span class="Delimiter">))</span>
+<span class="Comment">; change memory location 23</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.23</span> <span class="Constant">1</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>update-scheduler-state<span class="Delimiter">)</span>
+<span class="Comment">; routine unblocked</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">2</span> len.running-routines*<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler unblocks routines blocked on locations&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler-skip&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Comment">; running-routines* is empty</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>empty <span class="Global">running-routines*</span><span class="Delimiter">))</span>
+<span class="Comment">; sleeping routine</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!f1
+  <span class="Delimiter">(</span>= rep.routine!sleep <span class="Delimiter">'(</span>for-some-cycles <span class="Constant">34</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>set <span class="Global">sleeping-routines*</span>.routine<span class="Delimiter">))</span>
+<span class="Comment">; long time left for it to wake up</span>
+<span class="Delimiter">(</span>= <span class="Global">curr-cycle*</span> <span class="Constant">0</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>update-scheduler-state<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is <span class="Global">curr-cycle*</span> <span class="Constant">35</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> len.running-routines*<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler skips ahead to earliest sleeping routines when nothing to run&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler-deadlock&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>empty <span class="Global">running-routines*</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>empty <span class="Global">completed-routines*</span><span class="Delimiter">))</span>
+<span class="Comment">; blocked routine</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!f1
+  <span class="Delimiter">(</span>= rep.routine!sleep <span class="Delimiter">'(</span>until-location-changes <span class="Constant">23</span> <span class="Constant">0</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>set <span class="Global">sleeping-routines*</span>.routine<span class="Delimiter">))</span>
+<span class="Comment">; location it's waiting on is 'unchanged'</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.23</span> <span class="Constant">0</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>update-scheduler-state<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>~empty <span class="Global">completed-routines*</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (prn completed-routines*)</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine <span class="Global">completed-routines*</span><span class="Constant">.0</span>
+  <span class="Delimiter">(</span>when <span class="Delimiter">(</span>~posmatch <span class="Constant">&quot;deadlock&quot;</span> rep.routine!error<span class="Delimiter">)</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler detects deadlock&quot;</span><span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;scheduler-deadlock2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Comment">; running-routines* is empty</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>empty <span class="Global">running-routines*</span><span class="Delimiter">))</span>
+<span class="Comment">; blocked routine</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!f1
+  <span class="Delimiter">(</span>= rep.routine!sleep <span class="Delimiter">'(</span>until-location-changes <span class="Constant">23</span> <span class="Constant">0</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>set <span class="Global">sleeping-routines*</span>.routine<span class="Delimiter">))</span>
+<span class="Comment">; but is about to become ready</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.23</span> <span class="Constant">1</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>update-scheduler-state<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>when <span class="Delimiter">(</span>~empty <span class="Global">completed-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - scheduler ignores sleeping but ready threads when detecting deadlock&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;sleep&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>sleep for-some-cycles:literal <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;schedule&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1 <span class="Delimiter">'</span>f2<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>check-trace-contents <span class="Constant">&quot;scheduler handles sleeping routines&quot;</span>
+  <span class="Delimiter">'((</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 0&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;sleeping until 2&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;schedule&quot;</span> <span class="Constant">&quot;pushing f1 to sleep queue&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f2 0&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f2 1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;schedule&quot;</span> <span class="Constant">&quot;waking up f1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 2&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;sleep-long&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>sleep for-some-cycles:literal <span class="MuConstant">20</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;schedule&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1 <span class="Delimiter">'</span>f2<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>check-trace-contents <span class="Constant">&quot;scheduler progresses sleeping routines when there are no routines left to run&quot;</span>
+  <span class="Delimiter">'((</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 0&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;sleeping until 21&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;schedule&quot;</span> <span class="Constant">&quot;pushing f1 to sleep queue&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f2 0&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f2 1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;schedule&quot;</span> <span class="Constant">&quot;waking up f1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 1&quot;</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Constant">&quot;run&quot;</span> <span class="Constant">&quot;f1 2&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;sleep-location&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Comment">; waits for memory location 1 to be set, before computing its successor</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>sleep until-location-changes:literal <span class="Constant">1</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">1</span>:integer <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>sleep for-some-cycles:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; set to value</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;schedule&quot;)))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1 <span class="Delimiter">'</span>f2<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn int-canon.memory*)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>  <span class="Comment">; successor of value</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - sleep can block on a memory location&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;sleep-scoped-location&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Comment">; waits for memory location 1 to be changed, before computing its successor</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">10</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; array of locals</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> copy <span class="MuConstant">10</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; really location 12</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>sleep until-location-changes:literal <span class="Constant">1</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> add <span class="Constant">1</span>:integer <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>sleep for-some-cycles:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">12</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; set to value</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;schedule&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1 <span class="Delimiter">'</span>f2<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.13</span> <span class="Constant">4</span><span class="Delimiter">)</span>  <span class="Comment">; successor of value</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - sleep can block on a scoped memory location&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;fork&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>fork f2:fn<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - fork works&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;fork-with-args&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>fork f2:fn <span class="MuConstant">nil</span>:literal <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - fork can pass args&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;fork-copies-args&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>x:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>fork f2:fn <span class="MuConstant">nil</span>:literal x:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>x:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; should be ignored</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f2 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>f1<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - fork passes args by value&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;fork-global&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer/raw <span class="Op">&lt;-</span> copy <span class="Constant">2</span>:integer/space:global<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>fork f1:fn default-space:space-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>awhen rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">memory*</span><span class="Constant">.1</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - fork can take a space of global variables to access&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; The scheduler needs to keep track of the call stack for each routine.</span>
+<span class="Comment">; Eventually we'll want to save this information in mu's address space itself,</span>
+<span class="Comment">; along with the types array, the magic buffers for args and oargs, and so on.</span>
+<span class="Comment">;</span>
+<span class="Comment">; Eventually we want the right stack-management primitives to build delimited</span>
+<span class="Comment">; continuations in mu.</span>
+
+<span class="Comment">; Routines can throw errors.</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;array-bounds-check&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">23</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> index <span class="Constant">1</span>:integer-array <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine <span class="Delimiter">(</span>car <span class="Global">completed-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>no rep.routine!error<span class="Delimiter">)</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'index' throws an error if out of bounds&quot;</span><span class="Delimiter">)))</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 20</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">100</span>
+
+<span class="SalientComment">;; Synchronization</span>
+<span class="Comment">;</span>
+<span class="Comment">; Mu synchronizes using channels rather than locks, like Erlang and Go.</span>
+<span class="Comment">;</span>
+<span class="Comment">; The two ends of a channel will usually belong to different routines, but</span>
+<span class="Comment">; each end should only be used by a single one. Don't try to read from or</span>
+<span class="Comment">; write to it from multiple routines at once.</span>
+<span class="Comment">;</span>
+<span class="Comment">; To avoid locking, writer and reader will never write to the same location.</span>
+<span class="Comment">; So channels will include fields in pairs, one for the writer and one for the</span>
+<span class="Comment">; reader.</span>
+
+<span class="Comment">; The core circular buffer contains values at index 'first-full' up to (but</span>
+<span class="Comment">; not including) index 'first-empty'. The reader always modifies it at</span>
+<span class="Comment">; first-full, while the writer always modifies it at first-empty.</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-new&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-full:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-free:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Constant">0</span> <span class="Global">memory*</span><span class="Constant">.2</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Constant">0</span> <span class="Global">memory*</span><span class="Constant">.3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'init-channel' initializes 'first-full and 'first-free to 0&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-write&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-full:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-free:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (prn function*!write)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;array-len&quot; &quot;cvt0&quot; &quot;cvt1&quot;)))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;jump&quot;)))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;reply&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (prn canon.memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Constant">0</span> <span class="Global">memory*</span><span class="Constant">.5</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Global">memory*</span><span class="Constant">.6</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'write' enqueues item to channel&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-read&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:tagged-value <span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> read <span class="Constant">1</span>:channel-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> maybe-coerce <span class="Constant">5</span>:tagged-value integer:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-full:offset<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">9</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-free:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;array-len&quot; &quot;cvt0&quot; &quot;cvt1&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn int-canon.memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.7</span> <span class="Constant">34</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'read' returns written value&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Global">memory*</span><span class="Constant">.8</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Global">memory*</span><span class="Constant">.9</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'read' dequeues item from channel&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-write-wrap&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Comment">; channel with 1 slot</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Comment">; write a value</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Comment">; first-free will now be 1</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-free:offset<span class="Delimiter">)</span></span>
+      <span class="Comment">; read one value</span>
+      <span class="Mu"><span class="Delimiter">(</span>_ <span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> read <span class="Constant">1</span>:channel-address<span class="Delimiter">)</span></span>
+      <span class="Comment">; write a second value; verify that first-free wraps around to 0.</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-free:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;array-len&quot; &quot;cvt0&quot; &quot;cvt1&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn canon.memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Global">memory*</span><span class="Constant">.5</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Constant">0</span> <span class="Global">memory*</span><span class="Constant">.6</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'write' can wrap pointer back to start&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-read-wrap&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Comment">; channel with 1 slot</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Comment">; write a value</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Comment">; read one value</span>
+      <span class="Mu"><span class="Delimiter">(</span>_ <span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> read <span class="Constant">1</span>:channel-address<span class="Delimiter">)</span></span>
+      <span class="Comment">; first-full will now be 1</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-full:offset<span class="Delimiter">)</span></span>
+      <span class="Comment">; write a second value</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Comment">; read second value; verify that first-full wraps around to 0.</span>
+      <span class="Mu"><span class="Delimiter">(</span>_ <span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> read <span class="Constant">1</span>:channel-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> get <span class="Constant">1</span>:channel-address/deref first-full:offset<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj blacklist '(&quot;sz&quot; &quot;m&quot; &quot;setm&quot; &quot;addr&quot; &quot;array-len&quot; &quot;cvt0&quot; &quot;cvt1&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn canon.memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Global">memory*</span><span class="Constant">.5</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is <span class="Constant">0</span> <span class="Global">memory*</span><span class="Constant">.6</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'read' can wrap pointer back to start&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-new-empty-not-full&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:boolean <span class="Op">&lt;-</span> empty? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:boolean <span class="Op">&lt;-</span> full? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is t <span class="Global">memory*</span><span class="Constant">.2</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is nil <span class="Global">memory*</span><span class="Constant">.3</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - a new channel is always empty, never full&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-write-not-empty&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> empty? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> full? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is nil <span class="Global">memory*</span><span class="Constant">.5</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is nil <span class="Global">memory*</span><span class="Constant">.6</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - a channel after writing is never empty&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-write-full&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> empty? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> full? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is nil <span class="Global">memory*</span><span class="Constant">.5</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is t <span class="Global">memory*</span><span class="Constant">.6</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - a channel after writing may be full&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-read-not-full&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>_ <span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> read <span class="Constant">1</span>:channel-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> empty? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> full? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is nil <span class="Global">memory*</span><span class="Constant">.5</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is nil <span class="Global">memory*</span><span class="Constant">.6</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - a channel after reading is never full&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-read-empty&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>_ <span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> read <span class="Constant">1</span>:channel-address<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:boolean <span class="Op">&lt;-</span> empty? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:boolean <span class="Op">&lt;-</span> full? <span class="Constant">1</span>:channel-address/deref<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is t <span class="Global">memory*</span><span class="Constant">.5</span><span class="Delimiter">)</span>
+        <span class="Delimiter">(</span>~is nil <span class="Global">memory*</span><span class="Constant">.6</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - a channel after reading may be empty&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; The key property of channels; writing to a full channel blocks the current</span>
+<span class="Comment">; routine until it creates space. Ditto reading from an empty channel.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-read-block&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Comment">; channel is empty, but receives a read</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:tagged-value <span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> read <span class="Constant">1</span>:channel-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;schedule&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn int-canon.memory*)</span>
+<span class="CommentedCode">;? (prn sleeping-routines*)</span>
+<span class="CommentedCode">;? (prn completed-routines*)</span>
+<span class="Comment">; read should cause the routine to sleep, and</span>
+<span class="Comment">; the sole sleeping routine should trigger the deadlock detector</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine <span class="Delimiter">(</span>car <span class="Global">completed-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>when <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>no routine<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>no rep.routine!error<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>~posmatch <span class="Constant">&quot;deadlock&quot;</span> rep.routine!error<span class="Delimiter">))</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'read' on empty channel blocks (puts the routine to sleep until the channel gets data)&quot;</span><span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-write-block&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">1</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">34</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:tagged-value <span class="Op">&lt;-</span> save-type <span class="Constant">2</span>:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+      <span class="Comment">; channel has capacity 1, but receives a second write</span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address <span class="Constant">3</span>:tagged-value<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;schedule&quot; &quot;addr&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn int-canon.memory*)</span>
+<span class="CommentedCode">;? (prn running-routines*)</span>
+<span class="CommentedCode">;? (prn sleeping-routines*)</span>
+<span class="CommentedCode">;? (prn completed-routines*)</span>
+<span class="Comment">; second write should cause the routine to sleep, and</span>
+<span class="Comment">; the sole sleeping routine should trigger the deadlock detector</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine <span class="Delimiter">(</span>car <span class="Global">completed-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>when <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>no routine<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>no rep.routine!error<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>~posmatch <span class="Constant">&quot;deadlock&quot;</span> rep.routine!error<span class="Delimiter">))</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'write' on full channel blocks (puts the routine to sleep until the channel gets data)&quot;</span><span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-handoff&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function consumer <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>chan:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; create a channel</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>fork producer:fn <span class="MuConstant">nil</span>:literal chan:channel-address<span class="Delimiter">)</span>  <span class="Comment">; fork a routine to produce a value in it</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:tagged-value/raw <span class="Op">&lt;-</span> read chan:channel-address<span class="Delimiter">)</span>  <span class="Comment">; wait for input on channel</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function producer <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>n:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>ochan:channel-address <span class="Op">&lt;-</span> <span class="Identifier">next-input</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>x:tagged-value <span class="Op">&lt;-</span> save-type n:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>ochan:channel-address/deref <span class="Op">&lt;-</span> write ochan:channel-address x:tagged-value<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;schedule&quot; &quot;run&quot; &quot;addr&quot;)))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;-&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>consumer<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">24</span> <span class="Global">memory*</span><span class="Constant">.2</span><span class="Delimiter">)</span>  <span class="Comment">; location 1 contains tagged-value x above</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - channels are meant to be shared between routines&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;channel-handoff-routine&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function consumer <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address <span class="Op">&lt;-</span> init-channel <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span>  <span class="Comment">; create a channel</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>fork producer:fn default-space:space-address<span class="Delimiter">)</span>  <span class="Comment">; pass it as a global to another routine</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:tagged-value/raw <span class="Op">&lt;-</span> read <span class="Constant">1</span>:channel-address<span class="Delimiter">)</span>  <span class="Comment">; wait for input on channel</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function producer <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>n:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">24</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span>x:tagged-value <span class="Op">&lt;-</span> save-type n:integer<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:channel-address/space:global/deref <span class="Op">&lt;-</span> write <span class="Constant">1</span>:channel-address/space:global x:tagged-value<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>consumer<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">24</span> <span class="Global">memory*</span><span class="Constant">.2</span><span class="Delimiter">)</span>  <span class="Comment">; location 1 contains tagged-value x above</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - channels are meant to be shared between routines&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 100</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">10</span>
+
+<span class="SalientComment">;; Separating concerns</span>
+<span class="Comment">;</span>
+<span class="Comment">; Lightweight tools can also operate on quoted lists of statements surrounded</span>
+<span class="Comment">; by square brackets. In the example below, we mimic Go's 'defer' keyword</span>
+<span class="Comment">; using 'convert-quotes'. It lets us write code anywhere in a function, but</span>
+<span class="Comment">; have it run just before the function exits. Great for keeping code to</span>
+<span class="Comment">; reclaim memory or other resources close to the code to allocate it. (C++</span>
+<span class="Comment">; programmers know this as RAII.) We'll use 'defer' when we build a memory</span>
+<span class="Comment">; deallocation routine like C's 'free'.</span>
+<span class="Comment">;</span>
+<span class="Comment">; More powerful reorderings are also possible like in Literate Programming or</span>
+<span class="Comment">; Aspect-Oriented Programming; one advantage of prohibiting arbitrarily nested</span>
+<span class="Comment">; code is that we can naturally name 'join points' wherever we want.</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-quotes-defer&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-quotes
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span>defer <span class="Delimiter">[</span></span>
+                       <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">6</span>:literal<span class="Delimiter">)</span></span>
+                     <span class="Delimiter">])</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">5</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">6</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-quotes can handle 'defer'&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-quotes-defer-reply&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-quotes
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span>defer <span class="Delimiter">[</span></span>
+                       <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                     <span class="Delimiter">])</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-quotes inserts code at early exits&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-quotes-defer-reply-arg&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-quotes
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span>defer <span class="Delimiter">[</span></span>
+                       <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                     <span class="Delimiter">])</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span> <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span>prepare-reply <span class="MuConstant">2</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Identifier">reply</span><span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-quotes inserts code at early exits&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;convert-quotes-label&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>convert-quotes
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+              foo
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">5</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+            foo
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">5</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - convert-quotes can handle labels&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>before label1 <span class="Delimiter">[</span></span>
+     <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+    <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>as cons <span class="Global">before*</span>!label1<span class="Delimiter">)</span>
+          <span class="Delimiter">'(</span><span class="Comment">; fragment</span>
+            <span class="Delimiter">(</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'before' records fragments of code to insert before labels&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>insert-code
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu">label1</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu">label1</span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'insert-code' can insert fragments before labels&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before-multiple&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>before label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>before label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>as cons <span class="Global">before*</span>!label1<span class="Delimiter">)</span>
+          <span class="Delimiter">'(</span><span class="Comment">; fragment</span>
+            <span class="Delimiter">(</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))</span></span>
+            <span class="Delimiter">(</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'before' records fragments in order&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>insert-code
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu">label1</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu">label1</span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'insert-code' can insert multiple fragments in order before label&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before-scoped&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>before f/label1 <span class="Delimiter">[</span>  <span class="Comment">; label1 only inside function f</span></span>
+     <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+    <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>insert-code
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu">label1</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))</span></span>
+            <span class="Delimiter">'</span>f<span class="Delimiter">)</span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu">label1</span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'insert-code' can insert fragments before labels just in specified functions&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before-scoped2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>before f/label1 <span class="Delimiter">[</span>  <span class="Comment">; label1 only inside function f</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>insert-code
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu">label1</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu">label1</span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'insert-code' ignores labels not in specified functions&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;after&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>after label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>as cons <span class="Global">after*</span>!label1<span class="Delimiter">)</span>
+          <span class="Delimiter">'(</span><span class="Comment">; fragment</span>
+            <span class="Delimiter">(</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'after' records fragments of code to insert after labels&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>insert-code
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu">label1</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu">label1</span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'insert-code' can insert fragments after labels&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;after-multiple&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>after label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>as cons <span class="Global">after*</span>!label1<span class="Delimiter">)</span>
+          <span class="Delimiter">'(</span><span class="Comment">; fragment</span>
+            <span class="Delimiter">(</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))</span></span>
+            <span class="Delimiter">(</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'after' records fragments in *reverse* order&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>insert-code
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu">label1</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu">label1</span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'insert-code' can insert multiple fragments in order after label&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before-after&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>before label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">and</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>as cons <span class="Global">before*</span>!label1<span class="Delimiter">)</span>
+               <span class="Delimiter">'(</span><span class="Comment">; fragment</span>
+                 <span class="Delimiter">(</span>
+                   <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))))</span></span>
+         <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>as cons <span class="Global">after*</span>!label1<span class="Delimiter">)</span>
+               <span class="Delimiter">'(</span><span class="Comment">; fragment</span>
+                 <span class="Delimiter">(</span>
+                   <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'before' and 'after' fragments work together&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>insert-code
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu">label1</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu">label1</span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'insert-code' can insert multiple fragments around label&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before-after-multiple&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>before label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>before label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>as cons <span class="Global">before*</span>!label1<span class="Delimiter">)</span>
+              <span class="Delimiter">'(</span><span class="Comment">; fragment</span>
+                <span class="Delimiter">(</span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))</span></span>
+                <span class="Delimiter">(</span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))))</span></span>
+        <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>as cons <span class="Global">after*</span>!label1<span class="Delimiter">)</span>
+              <span class="Delimiter">'(</span><span class="Comment">; fragment</span>
+                <span class="Delimiter">(</span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">))</span></span>
+                <span class="Delimiter">(</span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - multiple 'before' and 'after' fragments at once&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>insert-code
+            <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+              <span class="Mu">label1</span>
+              <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+          <span class="Mu"><span class="Delimiter">'((</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu">label1</span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">6</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">7</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+            <span class="Mu"><span class="Delimiter">(</span><span class="Constant">8</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'insert-code' can insert multiple fragments around label - 2&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before-after-independent&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span><span class="Normal">do</span>
+            <span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>add-code
+              <span class="Mu"><span class="Delimiter">'((</span>before label1 <span class="Delimiter">[</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                 <span class="Delimiter">])</span>
+                <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                 <span class="Delimiter">])</span>
+                <span class="Mu"><span class="Delimiter">(</span>before label1 <span class="Delimiter">[</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                 <span class="Delimiter">])</span>
+                <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                 <span class="Delimiter">])))</span>
+            <span class="Delimiter">(</span>list <span class="Global">before*</span>!label1 <span class="Global">after*</span>!label1<span class="Delimiter">))</span>
+          <span class="Delimiter">(</span><span class="Normal">do</span>
+            <span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>add-code
+              <span class="Mu"><span class="Delimiter">'((</span>before label1 <span class="Delimiter">[</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                 <span class="Delimiter">])</span>
+                <span class="Mu"><span class="Delimiter">(</span>before label1 <span class="Delimiter">[</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                 <span class="Delimiter">])</span>
+                <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                 <span class="Delimiter">])</span>
+                <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+                  <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+                 <span class="Delimiter">])))</span>
+            <span class="Delimiter">(</span>list <span class="Global">before*</span>!label1 <span class="Global">after*</span>!label1<span class="Delimiter">)))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - order matters between 'before' and between 'after' fragments, but not *across* 'before' and 'after' fragments&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before-after-braces&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">function*</span> <span class="Delimiter">(</span>table<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>after label1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f1 <span class="Delimiter">[</span></span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu">label1</span>
+      }
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;cn0&quot;)))</span>
+<span class="Delimiter">(</span>freeze <span class="Global">function*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">function*</span>!f1
+          <span class="Mu"><span class="Delimiter">'(</span>label1</span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - before/after works inside blocks&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;before-after-any-order&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">function*</span> <span class="Delimiter">(</span>table<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      { <span class="CommentedCode">begin</span>
+        <span class="Mu">label1</span>
+      }
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>after label1 <span class="Delimiter">[</span></span>
+       <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>freeze <span class="Global">function*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">function*</span>!f1
+          <span class="Mu"><span class="Delimiter">'(</span>label1</span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - before/after can come after the function they need to modify&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;multiple-defs&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">function*</span> <span class="Delimiter">(</span>table<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>freeze <span class="Global">function*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">function*</span>!f1
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))</span></span>
+            <span class="Mu"><span class="Delimiter">(((</span><span class="Constant">1</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - multiple 'def' of the same function add clauses&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;def!&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">traces*</span> <span class="Delimiter">(</span>queue<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">function*</span> <span class="Delimiter">(</span>table<span class="Delimiter">))</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])</span>
+    <span class="Mu"><span class="Delimiter">(</span>function! f1 <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> copy <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>freeze <span class="Global">function*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">function*</span>!f1
+          <span class="Mu"><span class="Delimiter">'((((</span><span class="Constant">2</span> integer<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>copy<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">0</span> literal<span class="Delimiter">)))))</span></span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'def!' clears all previous clauses&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 10</span>
+
+<span class="SalientComment">;; ---</span>
+
+<span class="Delimiter">(</span>section <span class="Constant">100</span>  <span class="Comment">; string utilities</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-new&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new string:literal <span class="MuConstant">5</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> before rep.routine!alloc
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso rep.routine!alloc <span class="Delimiter">(</span>+ before <span class="Constant">5</span> <span class="Constant">1</span><span class="Delimiter">))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' allocates arrays of bytes for strings&quot;</span><span class="Delimiter">))))</span>
+
+<span class="Comment">; Convenience: initialize strings using string literals</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-literal&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;hello&quot;</span><span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> routine make-routine!main
+  <span class="Delimiter">(</span>enq routine <span class="Global">running-routines*</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span><span class="Normal">let</span> before rep.routine!alloc
+<span class="CommentedCode">;?     (set dump-trace*)</span>
+<span class="CommentedCode">;?     (= dump-trace* (obj whitelist '(&quot;schedule&quot; &quot;run&quot; &quot;addr&quot;)))</span>
+    <span class="Delimiter">(</span>run<span class="Delimiter">)</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso rep.routine!alloc <span class="Delimiter">(</span>+ before <span class="Constant">5</span> <span class="Constant">1</span><span class="Delimiter">))</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' allocates arrays of bytes for string literals&quot;</span><span class="Delimiter">))</span>
+    <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains-array before <span class="Constant">&quot;hello&quot;</span><span class="Delimiter">)</span>
+      <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'new' initializes allocated memory to string literal&quot;</span><span class="Delimiter">))))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;strcat&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;hello,&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot; world!&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:string-address <span class="Op">&lt;-</span> strcat <span class="Constant">1</span>:string-address <span class="Constant">2</span>:string-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains-array <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">&quot;hello, world!&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'strcat' concatenates strings&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;interpolate&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;hello, _!&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:string-address <span class="Op">&lt;-</span> interpolate <span class="Constant">1</span>:string-address <span class="Constant">2</span>:string-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains-array <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">&quot;hello, abc!&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'interpolate' splices strings&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;interpolate-empty&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;hello!&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:string-address <span class="Op">&lt;-</span> interpolate <span class="Constant">1</span>:string-address <span class="Constant">2</span>:string-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains-array <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">&quot;hello!&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'interpolate' without underscore returns template&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;interpolate-at-start&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;_, hello!&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:string-address <span class="Op">&lt;-</span> interpolate <span class="Constant">1</span>:string-address <span class="Constant">2</span>:string-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains-array <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">&quot;abc, hello&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'interpolate' splices strings at start&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;interpolate-at-end&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;hello, _&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:string-address <span class="Op">&lt;-</span> interpolate <span class="Constant">1</span>:string-address <span class="Constant">2</span>:string-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains-array <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">&quot;hello, abc&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'interpolate' splices strings at start&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;interpolate-varargs&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;hello, _, _, and _!&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">3</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;def&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">4</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;ghi&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">5</span>:string-address <span class="Op">&lt;-</span> interpolate <span class="Constant">1</span>:string-address <span class="Constant">2</span>:string-address <span class="Constant">3</span>:string-address <span class="Constant">4</span>:string-address<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot; &quot;array-info&quot;)))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (quit)</span>
+<span class="CommentedCode">;? (up i 1 (+ 1 (memory* memory*.5))</span>
+<span class="CommentedCode">;?   (prn (memory* (+ memory*.5 i))))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains-array <span class="Global">memory*</span><span class="Constant">.5</span> <span class="Constant">&quot;hello, abc, def, and ghi!&quot;</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'interpolate' splices in any number of strings&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-find-next&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;a/b&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> find-next <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">))</span> <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">1</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'find-next' finds first location of a character&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-find-next-empty&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> find-next <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">))</span> <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">0</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'find-next' finds first location of a character&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-find-next-initial&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;/abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> find-next <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">))</span> <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">0</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'find-next' handles prefix match&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-find-next-final&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc/&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> find-next <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">))</span> <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*.2)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">3</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'find-next' handles suffix match&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-find-next-missing&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> find-next <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">))</span> <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn memory*.2)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">3</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'find-next' handles no match&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-find-next-invalid-index&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> find-next <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">))</span> <span class="MuConstant">4</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="CommentedCode">;? (prn memory*.2)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'find-next' skips invalid index (past end of string)&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-find-next-first&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;ab/c/&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> find-next <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">))</span> <span class="MuConstant">0</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">2</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'find-next' finds first of multiple options&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-find-next-second&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;ab/c/&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:integer <span class="Op">&lt;-</span> find-next <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">))</span> <span class="MuConstant">3</span>:literal<span class="Delimiter">)</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'find-next' finds second of multiple options&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-split&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;a/b&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address-array-address <span class="Op">&lt;-</span> split <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">)))</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> base <span class="Global">memory*</span><span class="Constant">.2</span>
+<span class="CommentedCode">;?   (prn base &quot; &quot; memory*.base)</span>
+  <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.base <span class="Constant">2</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;?           (do1 nil prn.111)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">1</span><span class="Delimiter">))</span> <span class="Constant">&quot;a&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;?           (do1 nil prn.111)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">2</span><span class="Delimiter">))</span> <span class="Constant">&quot;b&quot;</span><span class="Delimiter">))</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'split' cuts string at delimiter&quot;</span><span class="Delimiter">)))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-split2&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;a/b/c&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address-array-address <span class="Op">&lt;-</span> split <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">)))</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> base <span class="Global">memory*</span><span class="Constant">.2</span>
+<span class="CommentedCode">;?   (prn base &quot; &quot; memory*.base)</span>
+  <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.base <span class="Constant">3</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;?           (do1 nil prn.111)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">1</span><span class="Delimiter">))</span> <span class="Constant">&quot;a&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;?           (do1 nil prn.111)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">2</span><span class="Delimiter">))</span> <span class="Constant">&quot;b&quot;</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;?           (do1 nil prn.111)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">3</span><span class="Delimiter">))</span> <span class="Constant">&quot;c&quot;</span><span class="Delimiter">))</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'split' cuts string at two delimiters&quot;</span><span class="Delimiter">)))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-split-missing&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address-array-address <span class="Op">&lt;-</span> split <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">)))</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> base <span class="Global">memory*</span><span class="Constant">.2</span>
+  <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.base <span class="Constant">1</span><span class="Delimiter">)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">1</span><span class="Delimiter">))</span> <span class="Constant">&quot;abc&quot;</span><span class="Delimiter">))</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'split' handles missing delimiter&quot;</span><span class="Delimiter">)))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-split-empty&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address-array-address <span class="Op">&lt;-</span> split <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">)))</span></span>
+     <span class="Delimiter">])))</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;run&quot;)))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> base <span class="Global">memory*</span><span class="Constant">.2</span>
+<span class="CommentedCode">;?   (prn base &quot; &quot; memory*.base)</span>
+  <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.base <span class="Constant">0</span><span class="Delimiter">)</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'split' handles empty string&quot;</span><span class="Delimiter">)))</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;string-split-empty-piece&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Mu"><span class="Delimiter">'((</span>function main <span class="Delimiter">[</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">1</span>:string-address <span class="Op">&lt;-</span> new <span class="Constant">&quot;a/b//c&quot;</span><span class="Delimiter">)</span></span>
+      <span class="Mu"><span class="Delimiter">(</span><span class="Constant">2</span>:string-address-array-address <span class="Op">&lt;-</span> split <span class="Constant">1</span>:string-address <span class="Delimiter">((</span><span class="MuConstant">#\/</span> literal<span class="Delimiter">)))</span></span>
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span>run <span class="Delimiter">'</span>main<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>each routine <span class="Global">completed-routines*</span>
+  <span class="Delimiter">(</span>aif rep.routine!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">let</span> base <span class="Global">memory*</span><span class="Constant">.2</span>
+  <span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span><span class="Normal">or</span> <span class="Delimiter">(</span>~is <span class="Global">memory*</span>.base <span class="Constant">4</span><span class="Delimiter">)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">1</span><span class="Delimiter">))</span> <span class="Constant">&quot;a&quot;</span><span class="Delimiter">)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">2</span><span class="Delimiter">))</span> <span class="Constant">&quot;b&quot;</span><span class="Delimiter">)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">3</span><span class="Delimiter">))</span> <span class="Constant">&quot;&quot;</span><span class="Delimiter">)</span>
+          <span class="Delimiter">(</span>~memory-contains-array <span class="Delimiter">(</span><span class="Global">memory*</span> <span class="Delimiter">(</span>+ base <span class="Constant">4</span><span class="Delimiter">))</span> <span class="Constant">&quot;c&quot;</span><span class="Delimiter">))</span>
+    <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'split' cuts string at two delimiters&quot;</span><span class="Delimiter">)))</span>
+
+<span class="Delimiter">)</span>  <span class="Comment">; section 100 for string utilities</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>new-trace <span class="Constant">&quot;parse-and-record&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>add-code
+  <span class="Delimiter">'((</span>and-record foo <span class="Delimiter">[</span>
+      x:string
+      y:integer
+      z:boolean
+     <span class="Delimiter">])))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Global">type*</span>!foo <span class="Delimiter">(</span>obj size <span class="Constant">3</span>  and-record t  elems <span class="Delimiter">'((</span>string<span class="Delimiter">)</span> <span class="Delimiter">(</span>integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>boolean<span class="Delimiter">))</span>  <span class="Normal">fields</span> <span class="Delimiter">'(</span>x y z<span class="Delimiter">)))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'add-code' can add new and-records&quot;</span><span class="Delimiter">))</span>
+
+<span class="SalientComment">;; unit tests for various helpers</span>
+
+<span class="Comment">; tokenize-args</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== tokenize-args&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>assert:iso <span class="Delimiter">'((</span>a b<span class="Delimiter">)</span> <span class="Delimiter">(</span>c d<span class="Delimiter">))</span>
+            <span class="Delimiter">(</span>tokenize-arg <span class="Delimiter">'</span>a:b/c:d<span class="Delimiter">))</span>
+<span class="Comment">; numbers are not symbols</span>
+<span class="Delimiter">(</span>assert:iso <span class="Delimiter">'((</span>a b<span class="Delimiter">)</span> <span class="Delimiter">(</span><span class="Constant">1</span> d<span class="Delimiter">))</span>
+            <span class="Delimiter">(</span>tokenize-arg <span class="Delimiter">'</span>a:b/1:d<span class="Delimiter">))</span>
+<span class="Comment">; special symbols are skipped</span>
+<span class="Mu"><span class="Delimiter">(</span>assert:iso <span class="Delimiter">'</span><span class="Op">&lt;-</span></span>
+            <span class="Mu"><span class="Delimiter">(</span>tokenize-arg <span class="Delimiter">'</span><span class="Op">&lt;-</span><span class="Delimiter">))</span></span>
+<span class="Delimiter">(</span>assert:iso <span class="Delimiter">'</span>_
+            <span class="Delimiter">(</span>tokenize-arg <span class="Delimiter">'</span>_<span class="Delimiter">))</span>
+
+<span class="Comment">; idempotent</span>
+<span class="Delimiter">(</span>assert:iso <span class="Delimiter">(</span>tokenize-arg:tokenize-arg <span class="Delimiter">'</span>a:b/c:d<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>tokenize-arg              <span class="Delimiter">'</span>a:b/c:d<span class="Delimiter">))</span>
+
+<span class="Comment">; support labels</span>
+<span class="Mu"><span class="Delimiter">(</span>assert:iso <span class="Delimiter">'((((</span>default-space space-address<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>new<span class="Delimiter">))</span> <span class="Delimiter">((</span>space literal<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">30</span> literal<span class="Delimiter">)))</span></span>
+              foo<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>tokenize-args
+              <span class="Mu"><span class="Delimiter">'((</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+                foo<span class="Delimiter">)))</span>
+
+<span class="Comment">; support braces</span>
+<span class="Mu"><span class="Delimiter">(</span>assert:iso <span class="Delimiter">'((((</span>default-space space-address<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>new<span class="Delimiter">))</span> <span class="Delimiter">((</span>space literal<span class="Delimiter">))</span> <span class="Delimiter">((</span><span class="MuConstant">30</span> literal<span class="Delimiter">)))</span></span>
+              foo
+              { <span class="CommentedCode">begin</span>
+                bar
+                <span class="Mu"><span class="Delimiter">(((</span>a b<span class="Delimiter">))</span> <span class="Op">&lt;-</span> <span class="Delimiter">((</span>op<span class="Delimiter">))</span> <span class="Delimiter">((</span>c d<span class="Delimiter">))</span> <span class="Delimiter">((</span>e f<span class="Delimiter">)))</span></span>
+              }<span class="Delimiter">)</span>
+            <span class="Delimiter">(</span>tokenize-args
+              <span class="Mu"><span class="Delimiter">'((</span>default-space:space-address <span class="Op">&lt;-</span> new space:literal <span class="MuConstant">30</span>:literal<span class="Delimiter">)</span></span>
+                foo
+                { <span class="CommentedCode">begin</span>
+                  bar
+                  <span class="Mu"><span class="Delimiter">(</span>a:b <span class="Op">&lt;-</span> op c:d e:f<span class="Delimiter">)</span></span>
+                }<span class="Delimiter">)))</span>
+
+<span class="Comment">; space</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== space&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Constant">0</span> <span class="Delimiter">(</span>space <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'space' is 0 by default&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Constant">1</span> <span class="Delimiter">(</span>space <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>space <span class="Constant">1</span><span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'space' picks up space when available&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'</span>global <span class="Delimiter">(</span>space <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>space global<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'space' understands routine-global space&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; absolutize</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== absolutize&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Delimiter">(</span>absolutize <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'absolutize' works without routine&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> make-routine!foo<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Delimiter">(</span>absolutize <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'absolutize' works without default-space&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= rep.routine*!call-stack.0!default-space <span class="Constant">10</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.10</span> <span class="Constant">5</span><span class="Delimiter">)</span>  <span class="Comment">; bounds check for default-space</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span><span class="Constant">15</span> integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>raw<span class="Delimiter">))</span>
+          <span class="Delimiter">(</span>absolutize <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'absolutize' works with default-space&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>absolutize <span class="Delimiter">'((</span><span class="Constant">5</span> integer<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~posmatch <span class="Constant">&quot;no room&quot;</span> rep.routine*!error<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'absolutize' checks against default-space bounds&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span>_ integer<span class="Delimiter">))</span> <span class="Delimiter">(</span>absolutize <span class="Delimiter">'((</span>_ integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'absolutize' passes dummy args right through&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.20</span> <span class="Constant">5</span><span class="Delimiter">)</span>  <span class="Comment">; pretend array</span>
+<span class="Delimiter">(</span>= rep.routine*!globals <span class="Constant">20</span><span class="Delimiter">)</span>  <span class="Comment">; provide it to routine global</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span><span class="Constant">22</span> integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>raw<span class="Delimiter">))</span>
+          <span class="Delimiter">(</span>absolutize <span class="Delimiter">'((</span><span class="Constant">1</span> integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>space global<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'absolutize' handles variables in the global space&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; deref</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== deref&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span>
+          <span class="Delimiter">(</span>deref <span class="Delimiter">'((</span><span class="Constant">3</span> integer-address<span class="Delimiter">)</span>
+                   <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'deref' handles simple addresses&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))</span>
+          <span class="Delimiter">(</span>deref <span class="Delimiter">'((</span><span class="Constant">3</span> integer-address<span class="Delimiter">)</span>
+                   <span class="Delimiter">(</span>deref<span class="Delimiter">)</span>
+                   <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'deref' deletes just one deref&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.4</span> <span class="Constant">5</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span><span class="Constant">5</span> integer<span class="Delimiter">))</span>
+          <span class="Delimiter">(</span>deref:deref <span class="Delimiter">'((</span><span class="Constant">3</span> integer-address-address<span class="Delimiter">)</span>
+                         <span class="Delimiter">(</span>deref<span class="Delimiter">)</span>
+                         <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'deref' can be chained&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">'((</span><span class="Constant">5</span> integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>foo<span class="Delimiter">))</span>
+          <span class="Delimiter">(</span>deref:deref <span class="Delimiter">'((</span><span class="Constant">3</span> integer-address-address<span class="Delimiter">)</span>
+                         <span class="Delimiter">(</span>deref<span class="Delimiter">)</span>
+                         <span class="Delimiter">(</span>foo<span class="Delimiter">)</span>
+                         <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'deref' skips junk&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; addr</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== addr&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> nil<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn 111)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - directly addressed operands are their own address&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer-address<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - directly addressed operands are their own address - 2&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="MuConstant">4</span> literal<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'addr' doesn't understand literals&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (prn 201)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.4</span> <span class="Constant">23</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn 202)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">23</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'addr' works with indirectly-addressed 'deref'&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">23</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">3</span> integer-address-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'addr' works with multiple 'deref'&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> make-routine!foo<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - directly addressed operands are their own address inside routines&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer-address<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - directly addressed operands are their own address inside routines - 2&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="MuConstant">4</span> literal<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'addr' doesn't understand literals inside routines&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.4</span> <span class="Constant">23</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">23</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'addr' works with indirectly-addressed 'deref' inside routines&quot;</span><span class="Delimiter">))</span>
+
+<span class="CommentedCode">;? (prn 301)</span>
+<span class="Delimiter">(</span>= rep.routine*!call-stack.0!default-space <span class="Constant">10</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn 302)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.10</span> <span class="Constant">5</span><span class="Delimiter">)</span>  <span class="Comment">; bounds check for default-space</span>
+<span class="CommentedCode">;? (prn 303)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">15</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - directly addressed operands in routines add default-space&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">15</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer-address<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - directly addressed operands in routines add default-space - 2&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">15</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="MuConstant">4</span> literal<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'addr' doesn't understand literals&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.15</span> <span class="Constant">23</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">23</span> <span class="Delimiter">(</span>addr <span class="Delimiter">'((</span><span class="Constant">4</span> integer-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'addr' adds default-space before 'deref', not after&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; array-len</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== array-len&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.35</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>array-len <span class="Delimiter">'((</span><span class="Constant">35</span> integer-boolean-pair-array<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'array-len'&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.34</span> <span class="Constant">35</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>array-len <span class="Delimiter">'((</span><span class="Constant">34</span> integer-boolean-pair-array-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'array-len'&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; sizeof</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== sizeof&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (set dump-trace*)</span>
+<span class="CommentedCode">;? (prn 401)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span>_ integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on primitives&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span>_ integer-address<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on addresses&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">2</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span>_ integer-boolean-pair<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on and-records&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">3</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span>_ integer-point-pair<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on and-records with and-record fields&quot;</span><span class="Delimiter">))</span>
+
+<span class="CommentedCode">;? (prn 410)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">34</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on primitive operands&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">1</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">34</span> integer-address<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on address operands&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">2</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">34</span> integer-boolean-pair<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on and-record operands&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">3</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">34</span> integer-point-pair<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on and-record operands with and-record fields&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">2</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">34</span> integer-boolean-pair-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on pointers to and-records&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.35</span> <span class="Constant">4</span><span class="Delimiter">)</span>  <span class="Comment">; size of array</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.34</span> <span class="Constant">35</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;sizeof&quot; &quot;array-len&quot;)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">9</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">34</span> integer-boolean-pair-array-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on pointers to arrays&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="CommentedCode">;? (prn 420)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.4</span> <span class="Constant">23</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">24</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">4</span> integer-array<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' reads array lengths from memory&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">24</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">3</span> integer-array-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' handles pointers to arrays&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.15</span> <span class="Constant">34</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> make-routine!foo<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">24</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">4</span> integer-array<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' reads array lengths from memory inside routines&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= rep.routine*!call-stack.0!default-space <span class="Constant">10</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.10</span> <span class="Constant">5</span><span class="Delimiter">)</span>  <span class="Comment">; bounds check for default-space</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">35</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">4</span> integer-array<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' reads array lengths from memory using default-space&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.35</span> <span class="Constant">4</span><span class="Delimiter">)</span>  <span class="Comment">; size of array</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.15</span> <span class="Constant">35</span><span class="Delimiter">)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;sizeof&quot;)))</span>
+<span class="Delimiter">(</span>aif rep.routine*!error <span class="Delimiter">(</span>prn <span class="Constant">&quot;error - &quot;</span> it<span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">9</span> <span class="Delimiter">(</span>sizeof <span class="Delimiter">'((</span><span class="Constant">4</span> integer-boolean-pair-array-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'sizeof' works on pointers to arrays using default-space&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (quit)</span>
+
+<span class="Comment">; m</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== m&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="MuConstant">4</span> literal<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' avoids reading memory for literals&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="MuConstant">4</span> offset<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' avoids reading memory for offsets&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.4</span> <span class="Constant">34</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">34</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' reads memory for simple types&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.3</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">34</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">3</span> integer-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' redirects addresses&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">3</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">34</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">2</span> integer-address-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' multiply redirects addresses&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">34</span> nil<span class="Delimiter">))</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">4</span> integer-boolean-pair<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' supports compound records&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.5</span> <span class="Constant">35</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.6</span> <span class="Constant">36</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">34</span> <span class="Constant">35</span> <span class="Constant">36</span><span class="Delimiter">))</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">4</span> integer-point-pair<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' supports records with compound fields&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">34</span> <span class="Constant">35</span> <span class="Constant">36</span><span class="Delimiter">))</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">3</span> integer-point-pair-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' supports indirect access to records&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.4</span> <span class="Constant">2</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">35</span> <span class="Constant">36</span><span class="Delimiter">))</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">4</span> integer-array<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' supports access to arrays&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">35</span> <span class="Constant">36</span><span class="Delimiter">))</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">3</span> integer-array-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' supports indirect access to arrays&quot;</span><span class="Delimiter">))</span>
+
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> make-routine!foo<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.10</span> <span class="Constant">5</span><span class="Delimiter">)</span>  <span class="Comment">; fake array</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.12</span> <span class="Constant">34</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>= rep.routine*!globals <span class="Constant">10</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~iso <span class="Constant">34</span> <span class="Delimiter">(</span>m <span class="Delimiter">'((</span><span class="Constant">1</span> integer<span class="Delimiter">)</span> <span class="Delimiter">(</span>space global<span class="Delimiter">))))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'm' supports access to per-routine globals&quot;</span><span class="Delimiter">))</span>
+
+<span class="Comment">; setm</span>
+<span class="Delimiter">(</span>prn <span class="Constant">&quot;== setm&quot;</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">4</span> integer<span class="Delimiter">))</span> <span class="Constant">34</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">34</span> <span class="Global">memory*</span><span class="Constant">.4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' writes primitives to memory&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">3</span> integer-address<span class="Delimiter">))</span> <span class="Constant">4</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">4</span> <span class="Global">memory*</span><span class="Constant">.3</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' writes addresses to memory&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">3</span> integer-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))</span> <span class="Constant">35</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">35</span> <span class="Global">memory*</span><span class="Constant">.4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' redirects writes&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">memory*</span><span class="Constant">.2</span> <span class="Constant">3</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">2</span> integer-address-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))</span> <span class="Constant">36</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~is <span class="Constant">36</span> <span class="Global">memory*</span><span class="Constant">.4</span><span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' multiply redirects writes&quot;</span><span class="Delimiter">))</span>
+<span class="CommentedCode">;? (prn 505)</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">4</span> integer-integer-pair<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">23</span> <span class="Constant">24</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains <span class="Constant">4</span> <span class="Delimiter">'(</span><span class="Constant">23</span> <span class="Constant">24</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' writes compound records&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>assert <span class="Delimiter">(</span>is <span class="Global">memory*</span><span class="Constant">.7</span> nil<span class="Delimiter">))</span>
+<span class="CommentedCode">;? (prn 506)</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">7</span> integer-point-pair<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">23</span> <span class="Constant">24</span> <span class="Constant">25</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains <span class="Constant">7</span> <span class="Delimiter">'(</span><span class="Constant">23</span> <span class="Constant">24</span> <span class="Constant">25</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' writes records with compound fields&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> make-routine!foo<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">4</span> integer-point-pair<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">33</span> <span class="Constant">34</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~posmatch <span class="Constant">&quot;incorrect size&quot;</span> rep.routine*!error<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' checks size of target&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>wipe <span class="Global">routine*</span><span class="Delimiter">)</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">3</span> integer-point-pair-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">43</span> <span class="Constant">44</span> <span class="Constant">45</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains <span class="Constant">4</span> <span class="Delimiter">'(</span><span class="Constant">43</span> <span class="Constant">44</span> <span class="Constant">45</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' supports indirect writes to records&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">2</span> integer-point-pair-address-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">53</span> <span class="Constant">54</span> <span class="Constant">55</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains <span class="Constant">4</span> <span class="Delimiter">'(</span><span class="Constant">53</span> <span class="Constant">54</span> <span class="Constant">55</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' supports multiply indirect writes to records&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">4</span> integer-array<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">31</span> <span class="Constant">32</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains <span class="Constant">4</span> <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">31</span> <span class="Constant">32</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' writes arrays&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">3</span> integer-array-address<span class="Delimiter">)</span> <span class="Delimiter">(</span>deref<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">41</span> <span class="Constant">42</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~memory-contains <span class="Constant">4</span> <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">41</span> <span class="Constant">42</span><span class="Delimiter">))</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' supports indirect writes to arrays&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> make-routine!foo<span class="Delimiter">)</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">4</span> integer-array<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">31</span> <span class="Constant">32</span> <span class="Constant">33</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~posmatch <span class="Constant">&quot;invalid array&quot;</span> rep.routine*!error<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' checks that array written is well-formed&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> make-routine!foo<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn 111)</span>
+<span class="CommentedCode">;? (= dump-trace* (obj whitelist '(&quot;sizeof&quot; &quot;setm&quot;)))</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">4</span> integer-boolean-pair-array<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">31</span> nil <span class="Constant">32</span> nil <span class="Constant">33</span><span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>~posmatch <span class="Constant">&quot;invalid array&quot;</span> rep.routine*!error<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' checks that array of records is well-formed&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>= <span class="Global">routine*</span> make-routine!foo<span class="Delimiter">)</span>
+<span class="CommentedCode">;? (prn 222)</span>
+<span class="Delimiter">(</span>setm <span class="Delimiter">'((</span><span class="Constant">4</span> integer-boolean-pair-array<span class="Delimiter">))</span> <span class="Delimiter">(</span>annotate <span class="Delimiter">'</span>record <span class="Delimiter">'(</span><span class="Constant">2</span> <span class="Constant">31</span> nil <span class="Constant">32</span> nil<span class="Delimiter">)))</span>
+<span class="Delimiter">(</span><span class="Normal">if</span> <span class="Delimiter">(</span>posmatch <span class="Constant">&quot;invalid array&quot;</span> rep.routine*!error<span class="Delimiter">)</span>
+  <span class="Delimiter">(</span>prn <span class="Constant">&quot;F - 'setm' checks that array of records is well-formed - 2&quot;</span><span class="Delimiter">))</span>
+<span class="Delimiter">(</span>wipe <span class="Global">routine*</span><span class="Delimiter">)</span>
+
+<span class="Delimiter">(</span>reset<span class="Delimiter">)</span>  <span class="Comment">; end file with this to persist the trace for the final test</span>
+</pre>
+</body>
+</html>
+<!-- vim: set foldmethod=manual : -->
diff --git a/archive/1.vm.arc/render.vim b/archive/1.vm.arc/render.vim
new file mode 100644
index 00000000..f005f48b
--- /dev/null
+++ b/archive/1.vm.arc/render.vim
@@ -0,0 +1,93 @@
+" Highlight current matches (:help matchadd()) in html files.
+" Run this after TOhtml.vim converts your source file to html.
+
+" from $VIMRUNTIME/syntax/2html.vim
+let s:cterm_color = {
+	\   0: "#000000", 1: "#c00000", 2: "#008000", 3: "#804000",
+	\   4: "#0000c0", 5: "#c000c0", 6: "#008080", 7: "#c0c0c0",
+	\   8: "#808080", 9: "#ff6060", 10: "#00ff00", 11: "#ffff00",
+	\   12: "#8080ff", 13: "#ff40ff", 14: "#00ffff", 15: "#ffffff"
+	\ }
+if &t_Co == 256
+  call extend(s:cterm_color, {
+	\   16: "#000000", 17: "#00005f", 18: "#000087", 19: "#0000af",
+	\   20: "#0000d7", 21: "#0000ff", 22: "#005f00", 23: "#005f5f",
+	\   24: "#005f87", 25: "#005faf", 26: "#005fd7", 27: "#005fff",
+	\   28: "#008700", 29: "#00875f", 30: "#008787", 31: "#0087af",
+	\   32: "#0087d7", 33: "#0087ff", 34: "#00af00", 35: "#00af5f",
+	\   36: "#00af87", 37: "#00afaf", 38: "#00afd7", 39: "#00afff",
+	\   40: "#00d700", 41: "#00d75f", 42: "#00d787", 43: "#00d7af",
+	\   44: "#00d7d7", 45: "#00d7ff", 46: "#00ff00", 47: "#00ff5f",
+	\   48: "#00ff87", 49: "#00ffaf", 50: "#00ffd7", 51: "#00ffff",
+	\   52: "#5f0000", 53: "#5f005f", 54: "#5f0087", 55: "#5f00af",
+	\   56: "#5f00d7", 57: "#5f00ff", 58: "#5f5f00", 59: "#5f5f5f",
+	\   60: "#5f5f87", 61: "#5f5faf", 62: "#5f5fd7", 63: "#5f5fff",
+	\   64: "#5f8700"
+	\ })
+  call extend(s:cterm_color, {
+	\   65: "#5f875f", 66: "#5f8787", 67: "#5f87af", 68: "#5f87d7",
+	\   69: "#5f87ff", 70: "#5faf00", 71: "#5faf5f", 72: "#5faf87",
+	\   73: "#5fafaf", 74: "#5fafd7", 75: "#5fafff", 76: "#5fd700",
+	\   77: "#5fd75f", 78: "#5fd787", 79: "#5fd7af", 80: "#5fd7d7",
+	\   81: "#5fd7ff", 82: "#5fff00", 83: "#5fff5f", 84: "#5fff87",
+	\   85: "#5fffaf", 86: "#5fffd7", 87: "#5fffff", 88: "#870000",
+	\   89: "#87005f", 90: "#870087", 91: "#8700af", 92: "#8700d7",
+	\   93: "#8700ff", 94: "#875f00", 95: "#875f5f", 96: "#875f87",
+	\   97: "#875faf", 98: "#875fd7", 99: "#875fff", 100: "#878700",
+	\   101: "#87875f", 102: "#878787", 103: "#8787af", 104: "#8787d7",
+	\   105: "#8787ff", 106: "#87af00", 107: "#87af5f", 108: "#87af87",
+	\   109: "#87afaf", 110: "#87afd7", 111: "#87afff", 112: "#87d700"
+	\ })
+  call extend(s:cterm_color, {
+	\   113: "#87d75f", 114: "#87d787", 115: "#87d7af", 116: "#87d7d7",
+	\   117: "#87d7ff", 118: "#87ff00", 119: "#87ff5f", 120: "#87ff87",
+	\   121: "#87ffaf", 122: "#87ffd7", 123: "#87ffff", 124: "#af0000",
+	\   125: "#af005f", 126: "#af0087", 127: "#af00af", 128: "#af00d7",
+	\   129: "#af00ff", 130: "#af5f00", 131: "#af5f5f", 132: "#af5f87",
+	\   133: "#af5faf", 134: "#af5fd7", 135: "#af5fff", 136: "#af8700",
+	\   137: "#af875f", 138: "#af8787", 139: "#af87af", 140: "#af87d7",
+	\   141: "#af87ff", 142: "#afaf00", 143: "#afaf5f", 144: "#afaf87",
+	\   145: "#afafaf", 146: "#afafd7", 147: "#afafff", 148: "#afd700",
+	\   149: "#afd75f", 150: "#afd787", 151: "#afd7af", 152: "#afd7d7",
+	\   153: "#afd7ff", 154: "#afff00", 155: "#afff5f", 156: "#afff87",
+	\   157: "#afffaf", 158: "#afffd7"
+	\ })
+  call extend(s:cterm_color, {
+	\   159: "#afffff", 160: "#d70000", 161: "#d7005f", 162: "#d70087",
+	\   163: "#d700af", 164: "#d700d7", 165: "#d700ff", 166: "#d75f00",
+	\   167: "#d75f5f", 168: "#d75f87", 169: "#d75faf", 170: "#d75fd7",
+	\   171: "#d75fff", 172: "#d78700", 173: "#d7875f", 174: "#d78787",
+	\   175: "#d787af", 176: "#d787d7", 177: "#d787ff", 178: "#d7af00",
+	\   179: "#d7af5f", 180: "#d7af87", 181: "#d7afaf", 182: "#d7afd7",
+	\   183: "#d7afff", 184: "#d7d700", 185: "#d7d75f", 186: "#d7d787",
+	\   187: "#d7d7af", 188: "#d7d7d7", 189: "#d7d7ff", 190: "#d7ff00",
+	\   191: "#d7ff5f", 192: "#d7ff87", 193: "#d7ffaf", 194: "#d7ffd7",
+	\   195: "#d7ffff", 196: "#ff0000", 197: "#ff005f", 198: "#ff0087",
+	\   199: "#ff00af", 200: "#ff00d7", 201: "#ff00ff", 202: "#ff5f00",
+	\   203: "#ff5f5f", 204: "#ff5f87"
+	\ })
+  call extend(s:cterm_color, {
+	\   205: "#ff5faf", 206: "#ff5fd7", 207: "#ff5fff", 208: "#ff8700",
+	\   209: "#ff875f", 210: "#ff8787", 211: "#ff87af", 212: "#ff87d7",
+	\   213: "#ff87ff", 214: "#ffaf00", 215: "#ffaf5f", 216: "#ffaf87",
+	\   217: "#ffafaf", 218: "#ffafd7", 219: "#ffafff", 220: "#ffd700",
+	\   221: "#ffd75f", 222: "#ffd787", 223: "#ffd7af", 224: "#ffd7d7",
+	\   225: "#ffd7ff", 226: "#ffff00", 227: "#ffff5f", 228: "#ffff87",
+	\   229: "#ffffaf", 230: "#ffffd7", 231: "#ffffff", 232: "#080808",
+	\   233: "#121212", 234: "#1c1c1c", 235: "#262626", 236: "#303030",
+	\   237: "#3a3a3a", 238: "#444444", 239: "#4e4e4e", 240: "#585858",
+	\   241: "#626262", 242: "#6c6c6c", 243: "#767676", 244: "#808080",
+	\   245: "#8a8a8a", 246: "#949494", 247: "#9e9e9e", 248: "#a8a8a8",
+	\   249: "#b2b2b2", 250: "#bcbcbc", 251: "#c6c6c6", 252: "#d0d0d0",
+	\   253: "#dadada", 254: "#e4e4e4", 255: "#eeeeee"
+	\ })
+endif
+
+set isk+=-
+redir > /tmp/highlight
+  for matchinfo in getmatches()
+    if !has_key(matchinfo, 'pattern') | continue | endif
+    echo "%s,".matchinfo.pattern.",<span style='color:".s:cterm_color[str2nr(synIDattr(hlID(matchinfo.group), 'fg'))]."'>&</span>,g"
+  endfor
+redir END
+so /tmp/highlight
diff --git a/archive/1.vm.arc/scratch.vim b/archive/1.vm.arc/scratch.vim
new file mode 100644
index 00000000..83b05539
--- /dev/null
+++ b/archive/1.vm.arc/scratch.vim
@@ -0,0 +1,50 @@
+" random commands used interactively to build mu.arc.t.html
+
+TOhtml
+%s,<.*&lt;-.*,<span class="Mu">&</span>,gc
+%s/Special">&lt;/Op">\&lt;/g
+%s, &lt;-, <span class="Op">&</span>,gc
+%s/Constant[^>]*>[^>]*>[: ]literal/Mu&/gc
+%s/Constant[^>]*>[^>]*>[: ]offset/Mu&/gc
+%s,\<nil literal,<span class="MuConstant">t</span> literal,gc
+%s,\<t literal,<span class="MuConstant">t</span> literal,gc
+%s,\<nil:literal\>,<span class="MuConstant">nil</span>:literal,gc
+%s,\<t:literal\>,<span class="MuConstant">t</span>:literal,gc
+
+map ` :s,[^ ].*,<span class="Mu">&</span>,<CR>
+/function.*[
+"b = `/<Up><Up><Enter>n
+map ; @b
+/jump
+/break
+/reply
+/loop
+/sleep
+/fork
+/defer
+/label1
+/before.*[
+/after.*[
+
+  " supercedes
+  %s,<.*break.*,<span class="Mu">&</span>,gc
+  %s,<.*continue.*,<span class="Mu">&</span>,gc
+  %s,<.*reply.*,<span class="Mu">&</span>,gc
+  %s,<.*jump.*,<span class="Mu">&</span>,gc
+  %s,<.*main.*,<span class="Mu">&</span>,gc
+  %s,<.*test1.*,<span class="Mu">&</span>,gc
+  %s,<.*test2.*,<span class="Mu">&</span>,gc
+  %s,<.*f1.*,<span class="Mu">&</span>,gc
+  %s,<.*f2.*,<span class="Mu">&</span>,gc
+
+pre { white-space: pre-wrap; font-family: monospace; color: #aaaaaa; background-color: #000000; }
+body { font-family: monospace; color: #aaaaaa; background-color: #000000; }
+a { color:#4444ff; }
+* { font-size: 1em; }
+.Constant, .MuConstant { color: #008080; }
+.Comment { color: #8080ff; }
+.Delimiter { color: #600060; }
+.Normal { color: #aaaaaa; }
+.Mu, .Mu .Normal, .Mu .Constant { color: #ffffff; }
+.Op { color: #ff8888; }
+.CommentedCode { color: #666666; }
diff --git a/archive/1.vm.arc/stdin.mu b/archive/1.vm.arc/stdin.mu
new file mode 100644
index 00000000..87598667
--- /dev/null
+++ b/archive/1.vm.arc/stdin.mu
@@ -0,0 +1,27 @@
+; reads and prints keys until you hit 'q'
+; no need to hit 'enter', and 'enter' has no special meaning
+; dies if you wait a while, because so far we never free memory
+(function main [
+  (default-space:space-address <- new space:literal 30:literal)
+  (cursor-mode)
+  ; hook up stdin
+  (stdin:channel-address <- init-channel 1:literal)
+;?   ($print (("main: stdin is " literal)))
+;?   ($print stdin:channel-address)
+;?   ($print (("\n" literal)))
+  (fork-helper send-keys-to-stdin:fn nil:literal/globals nil:literal/limit nil:literal/keyboard stdin:channel-address)
+  ; now read characters from stdin until a 'q' is typed
+  ($print (("? " literal)))
+  { begin
+    (x:tagged-value stdin:channel-address/deref <- read stdin:channel-address)
+    (c:character <- maybe-coerce x:tagged-value character:literal)
+;?     ($print (("main: stdin is " literal)))
+;?     ($print stdin:channel-address)
+;?     ($print (("\n" literal)))
+;?     ($print (("check: " literal)))
+;?     ($print c:character)
+    (done?:boolean <- equal c:character ((#\q literal)))
+    (break-if done?:boolean)
+    (loop)
+  }
+])
diff --git a/archive/1.vm.arc/tangle.mu b/archive/1.vm.arc/tangle.mu
new file mode 100644
index 00000000..3e73dd89
--- /dev/null
+++ b/archive/1.vm.arc/tangle.mu
@@ -0,0 +1,35 @@
+; To demonstrate tangle directives, we'll construct a factorial function with
+; separate base and recursive cases. Compare factorial.mu.
+; This isn't a very realistic example, just a simple demonstration of
+; possibilities.
+
+(function factorial [
+  (default-space:space-address <- new space:literal 30:literal)
+  (n:integer <- next-input)
+  { begin
+    base-case
+  }
+  recursive-case
+])
+
+(after base-case [
+  ; if n=0 return 1
+  (zero?:boolean <- equal n:integer 0:literal)
+  (break-unless zero?:boolean)
+  (reply 1:literal)
+])
+
+(after recursive-case [
+  ; return n*factorial(n-1)
+  (x:integer <- subtract n:integer 1:literal)
+  (subresult:integer <- factorial x:integer)
+  (result:integer <- multiply subresult:integer n:integer)
+  (reply result:integer)
+])
+
+(function main [
+  (1:integer <- factorial 5:literal)
+  ($print (("result: " literal)))
+  (print-integer nil:literal/terminal 1:integer)
+  (print-character nil:literal/terminal ((#\newline literal)))
+])
diff --git a/archive/1.vm.arc/trace.arc.t b/archive/1.vm.arc/trace.arc.t
new file mode 100644
index 00000000..6dcebe0a
--- /dev/null
+++ b/archive/1.vm.arc/trace.arc.t
@@ -0,0 +1,1659 @@
+(selective-load "mu.arc" section-level)
+(test-only-settings)
+(add-code:readfile "trace.mu")
+(ero "running tests in trace.arc.t (takes ~10 mins)")
+(freeze function*)
+(load-system-functions)
+
+(reset2)
+(new-trace "print-trace")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"schedule: main
+run: main 0: (((1 integer)) <- ((copy)) ((1 literal)))
+run: main 0: 1 => ((1 integer))
+mem: ((1 integer)): 1 <= 1
+run: main 1: (((2 integer)) <- ((copy)) ((3 literal)))
+run: main 1: 3 => ((2 integer))
+mem: ((2 integer)): 2 <= 3
+run: main 2: (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))
+mem: ((1 integer)) => 1
+mem: ((2 integer)) => 3
+run: main 2: 4 => ((3 integer))
+mem: ((3 integer)): 3 <= 4
+schedule:  done with routine")
+  (s:stream-address <- init-stream x:string-address)
+  (traces:instruction-trace-address-array-address <- parse-traces s:stream-address)
+  (screen:terminal-address <- init-fake-terminal 70:literal 15:literal)
+  (browser-state:space-address <- browser-state traces:instruction-trace-address-array-address 30:literal/screen-height)
+  (print-traces-collapsed browser-state:space-address screen:terminal-address)
+  (1:string-address/raw <- get screen:terminal-address/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+;? (prn memory*.1)
+(when (~screen-contains memory*.1 70
+         (+ "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - print-traces-collapsed works"))
+;? (quit) ;? 1
+
+(reset2)
+(new-trace "print-trace-from-middle-of-screen")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"schedule: main
+run: main 0: (((1 integer)) <- ((copy)) ((1 literal)))
+run: main 0: 1 => ((1 integer))
+mem: ((1 integer)): 1 <= 1
+run: main 1: (((2 integer)) <- ((copy)) ((3 literal)))
+run: main 1: 3 => ((2 integer))
+mem: ((2 integer)): 2 <= 3
+run: main 2: (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))
+mem: ((1 integer)) => 1
+mem: ((2 integer)) => 3
+run: main 2: 4 => ((3 integer))
+mem: ((3 integer)): 3 <= 4
+schedule:  done with routine")
+  (s:stream-address <- init-stream x:string-address)
+  (traces:instruction-trace-address-array-address <- parse-traces s:stream-address)
+  (1:terminal-address/raw <- init-fake-terminal 70:literal 15:literal)
+  ; position the cursor away from top of screen
+  (cursor-down 1:terminal-address/raw)
+  (cursor-down 1:terminal-address/raw)
+  (browser-state:space-address <- browser-state traces:instruction-trace-address-array-address 30:literal/screen-height)
+  (print-traces-collapsed browser-state:space-address 1:terminal-address/raw traces:instruction-trace-address-array-address)
+  (2:string-address/raw <- get 1:terminal-address/raw/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.2 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - print-traces-collapsed works anywhere on the screen"))
+(run-code main2
+  (print-character 1:terminal-address/raw ((#\* literal))))
+(when (~screen-contains memory*.2 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "+ main/ 2 : 4 => ((3 integer))                                        "
+            "*                                                                     "))
+  (prn "F - print-traces-collapsed leaves cursor at next line"))
+
+(reset2)
+(new-trace "process-key-move-up-down")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"schedule: main
+run: main 0: (((1 integer)) <- ((copy)) ((1 literal)))
+run: main 0: 1 => ((1 integer))
+mem: ((1 integer)): 1 <= 1
+run: main 1: (((2 integer)) <- ((copy)) ((3 literal)))
+run: main 1: 3 => ((2 integer))
+mem: ((2 integer)): 2 <= 3
+run: main 2: (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))
+mem: ((1 integer)) => 1
+mem: ((2 integer)) => 3
+run: main 2: 4 => ((3 integer))
+mem: ((3 integer)): 3 <= 4
+schedule:  done with routine")
+  (s:stream-address <- init-stream x:string-address)
+  (1:instruction-trace-address-array-address/raw <- parse-traces s:stream-address)
+  (2:terminal-address/raw <- init-fake-terminal 70:literal 15:literal)
+  ; position the cursor away from top of screen
+  (cursor-down 2:terminal-address/raw)
+  (cursor-down 2:terminal-address/raw)
+  (3:space-address/raw <- browser-state 1:instruction-trace-address-array-address/raw 30:literal/screen-height)
+  ; draw trace
+  (print-traces-collapsed 3:space-address/raw/browser-state 2:terminal-address/raw 1:instruction-trace-address-array-address/raw)
+  ; move cursor up
+  ; we have no way yet to test special keys like up-arrow
+  (s:string-address <- new "k")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  ; draw cursor
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  (4:string-address/raw <- get 2:terminal-address/raw/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "* main/ 2 : 4 => ((3 integer))                                        "))
+            ;^cursor
+  (prn "F - process-key can move up the cursor"))
+(run-code main2
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; reset previous cursor
+  (replace-character 2:terminal-address/raw ((#\+ literal)))
+  ; move cursor up 3 more lines
+  (s:string-address <- new "kkk")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  )
+; cursor is now at line 3
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "* main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            ;^cursor
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - process-key can move up multiple times"))
+; try to move cursor up thrice more
+(run-code main3
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (replace-character 2:terminal-address/raw ((#\+ literal)))
+  (s:string-address <- new "kkk")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  )
+; cursor doesn't go beyond the first line printed
+; stuff on screen before browser-state was initialized is inviolate
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "* main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            ;^cursor
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - process-key doesn't move above bounds"))
+; now move cursor down 4 times
+(run-code main4
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (replace-character 2:terminal-address/raw ((#\+ literal)))
+  (s:string-address <- new "jjjj")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  )
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "* main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            ;^cursor
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - process-key can move down multiple times"))
+; try to move cursor down 4 more times
+(run-code main5
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (replace-character 2:terminal-address/raw ((#\+ literal)))
+  (s:string-address <- new "jjjj")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  )
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "+ main/ 2 : 4 => ((3 integer))                                        "
+            "*                                                                     "))
+  (prn "F - process-key doesn't move below bounds"))
+
+(reset2)
+(new-trace "process-key-expand")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"schedule: main
+run: main 0: (((1 integer)) <- ((copy)) ((1 literal)))
+run: main 0: 1 => ((1 integer))
+mem: ((1 integer)): 1 <= 1
+run: main 1: (((2 integer)) <- ((copy)) ((3 literal)))
+run: main 1: 3 => ((2 integer))
+mem: ((2 integer)): 2 <= 3
+run: main 2: (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))
+mem: ((1 integer)) => 1
+mem: ((2 integer)) => 3
+run: main 2: 4 => ((3 integer))
+mem: ((3 integer)): 3 <= 4
+schedule:  done with routine")
+  (s:stream-address <- init-stream x:string-address)
+  (1:instruction-trace-address-array-address/raw <- parse-traces s:stream-address)
+  (2:terminal-address/raw <- init-fake-terminal 70:literal 15:literal)
+  ; position the cursor away from top of screen
+  (cursor-down 2:terminal-address/raw)
+  (cursor-down 2:terminal-address/raw)
+  (3:space-address/raw <- browser-state 1:instruction-trace-address-array-address/raw 30:literal/screen-height)
+  ; draw trace
+  (print-traces-collapsed 3:space-address/raw/browser-state 2:terminal-address/raw 1:instruction-trace-address-array-address/raw)
+  (4:string-address/raw <- get 2:terminal-address/raw/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - process-key: before expand"))
+(run-code main2
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; move cursor to final line and expand
+  (s:string-address <- new "k\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+; final line is expanded
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "- main/ 2 : 4 => ((3 integer))                                        "
+            "   mem : ((3 integer)): 3 <= 4                                        "
+            "   schedule :  done with routine                                      "))
+  (prn "F - process-key expands the trace index at cursor on <enter>"))
+; and cursor should remain on the top-level line
+(run-code main3
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  )
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "* main/ 2 : 4 => ((3 integer))                                        "
+            ;^cursor
+            "   mem : ((3 integer)): 3 <= 4                                        "
+            "   schedule :  done with routine                                      "))
+  (prn "F - process-key positions cursor at start of trace index after expanding"))
+
+(reset2)
+(new-trace "process-key-expand-nonlast")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"schedule: main
+run: main 0: (((1 integer)) <- ((copy)) ((1 literal)))
+run: main 0: 1 => ((1 integer))
+mem: ((1 integer)): 1 <= 1
+run: main 1: (((2 integer)) <- ((copy)) ((3 literal)))
+run: main 1: 3 => ((2 integer))
+mem: ((2 integer)): 2 <= 3
+run: main 2: (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))
+mem: ((1 integer)) => 1
+mem: ((2 integer)) => 3
+run: main 2: 4 => ((3 integer))
+mem: ((3 integer)): 3 <= 4
+schedule:  done with routine")
+  (s:stream-address <- init-stream x:string-address)
+  (1:instruction-trace-address-array-address/raw <- parse-traces s:stream-address)
+  (2:terminal-address/raw <- init-fake-terminal 70:literal 15:literal)
+  ; position the cursor away from top of screen
+  (cursor-down 2:terminal-address/raw)
+  (cursor-down 2:terminal-address/raw)
+  (3:space-address/raw <- browser-state 1:instruction-trace-address-array-address/raw 30:literal/screen-height)
+  ; draw trace
+  (print-traces-collapsed 3:space-address/raw/browser-state 2:terminal-address/raw 1:instruction-trace-address-array-address/raw)
+  ; expand penultimate line
+  (s:string-address <- new "kk\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (4:string-address/raw <- get 2:terminal-address/raw/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "- main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "   mem : ((1 integer)) => 1                                           "
+            "   mem : ((2 integer)) => 3                                           "
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - process-key: expanding a line continues to print lines after it"))
+
+(reset2)
+(new-trace "process-key-expanded")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"schedule: main
+run: main 0: (((1 integer)) <- ((copy)) ((1 literal)))
+run: main 0: 1 => ((1 integer))
+mem: ((1 integer)): 1 <= 1
+mem: ((1 integer)): 1 <= 1
+run: main 1: (((2 integer)) <- ((copy)) ((3 literal)))
+run: main 1: 3 => ((2 integer))
+mem: ((2 integer)): 2 <= 3
+run: main 2: (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))
+mem: ((1 integer)) => 1
+mem: ((2 integer)) => 3
+run: main 2: 4 => ((3 integer))
+mem: ((3 integer)): 3 <= 4
+schedule:  done with routine")
+  (s:stream-address <- init-stream x:string-address)
+  (1:instruction-trace-address-array-address/raw <- parse-traces s:stream-address)
+  (2:terminal-address/raw <- init-fake-terminal 70:literal 15:literal)
+  ; position the cursor away from top of screen
+  (cursor-down 2:terminal-address/raw)
+  (cursor-down 2:terminal-address/raw)
+  (3:space-address/raw <- browser-state 1:instruction-trace-address-array-address/raw 30:literal/screen-height)
+  ; draw trace
+  (print-traces-collapsed 3:space-address/raw/browser-state 2:terminal-address/raw 1:instruction-trace-address-array-address/raw)
+  ; expand penultimate line, then move one line down and draw cursor
+  (s:string-address <- new "kk\nj")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  (4:string-address/raw <- get 2:terminal-address/raw/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; cursor should be at next top-level 'run' line
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "- main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "   mem : ((1 integer)) => 1                                           "
+            "   mem : ((2 integer)) => 3                                           "
+            "* main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - process-key: navigation moves between top-level trace indices only"))
+(run-code main2
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; reset previous cursor
+  (replace-character 2:terminal-address/raw ((#\+ literal)))
+  ; move cursor back up one line
+  (s:string-address <- new "k")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  ; show cursor
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; cursor should be back at the top of the expanded line
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "* main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "   mem : ((1 integer)) => 1                                           "
+            "   mem : ((2 integer)) => 3                                           "
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - process-key: navigation moves between top-level indices only - 2"))
+(run-code main3
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; reset previous cursor
+  (replace-character 2:terminal-address/raw ((#\+ literal)))
+  ; press enter
+  (s:string-address <- new "\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; expanded trace should now be collapsed
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "+ main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "+ main/ 2 : 4 => ((3 integer))                                        "
+            "                                                                      "
+            "                                                                      "))
+  (prn "F - process-key: process-key collapses trace indices correctly after moving around"))
+(run-code main4
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; move up a few lines, expand, then move down and expand again
+  (s:string-address <- new "kkk\njjj\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+;?   (replace-character 2:terminal-address/raw ((#\* literal))) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; first expand should have no effect
+(when (~screen-contains memory*.4 70
+         (+ "                                                                      "
+            "                                                                      "
+            "+ main/ 0 : (((1 integer)) <- ((copy)) ((1 literal)))                 "
+            "+ main/ 0 : 1 => ((1 integer))                                        "
+            "+ main/ 1 : (((2 integer)) <- ((copy)) ((3 literal)))                 "
+            "+ main/ 1 : 3 => ((2 integer))                                        "
+            "- main/ 2 : (((3 integer)) <- ((add)) ((1 integer)) ((2 integer)))    "
+            "   mem : ((1 integer)) => 1                                           "
+            "   mem : ((2 integer)) => 3                                           "
+            "+ main/ 2 : 4 => ((3 integer))                                        "))
+  (prn "F - process-key: process-key collapses the previously expanded trace index when expanding elsewhere"))
+
+;; manage screen height
+
+(reset2)
+(new-trace "trace-paginate")
+(run-code main
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"run: main 0: a b c
+mem: 0 a
+run: main 1: d e f
+mem: 1 a
+mem: 1 b
+mem: 1 c
+mem: 1 d
+mem: 1 e
+run: main 2: g hi
+run: main 3: j
+mem: 3 a
+run: main 4: k
+run: main 5: l
+run: main 6: m
+run: main 7: n")
+  (s:stream-address <- init-stream x:string-address)
+  (traces:instruction-trace-address-array-address <- parse-traces s:stream-address)
+  (2:terminal-address/raw <- init-fake-terminal 17:literal 15:literal)
+  (3:space-address/raw/browser-state <- browser-state traces:instruction-trace-address-array-address 3:literal/screen-height)
+  (print-traces-collapsed 3:space-address/raw/browser-state 2:terminal-address/raw)
+  (4:string-address/raw <- get 2:terminal-address/raw/deref data:offset)
+)
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; screen shows a subset of collapsed trace lines
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "+ main/ 1 : d e f"
+            "+ main/ 2 : g hi "))
+  (prn "F - print-traces-collapsed can show just one 'page' of a larger trace"))
+
+; expand top line
+(run-code main2
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "kkk\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; screen shows just first trace line fully expanded
+(when (~screen-contains memory*.4 17
+         (+ "- main/ 0 : a b c"
+            "   mem : 0 a     "
+            "+ main/ 1 : d e f"
+            "                 "))
+  (prn "F - expanding doesn't print past end of page"))
+(run-code main2-2
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  )
+; screen shows part of the second trace line expanded
+(when (~screen-contains memory*.4 17
+         (+ "* main/ 0 : a b c"
+            "   mem : 0 a     "
+            "+ main/ 1 : d e f"
+            "                 "))
+  (prn "F - cursor at right place after expand"))
+
+; expand line below without first collapsing previously expanded line
+(run-code main3
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; reset previous cursor
+  (replace-character 2:terminal-address/raw ((#\- literal)))
+  (s:string-address <- new "j\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; screen shows part of the second trace line expanded
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "- main/ 1 : d e f"
+            "   mem : 1 a     "
+            "                 "
+            "                 "))
+  (prn "F - expanding below expanded line respects screen/page height"))
+(run-code main3-2
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+  )
+; screen shows part of the second trace line expanded
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "* main/ 1 : d e f"
+            "   mem : 1 a     "
+            "                 "
+            "                 "))
+  (prn "F - cursor at right place after expand below"))
+
+; expand line *above* without first collapsing previously expanded line
+(run-code main4
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; reset previous cursor
+  (replace-character 2:terminal-address/raw ((#\- literal)))
+  (s:string-address <- new "k\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; screen again shows first trace line expanded
+(when (~screen-contains memory*.4 17
+         (+ "- main/ 0 : a b c"
+            "   mem : 0 a     "
+            "+ main/ 1 : d e f"
+            "                 "))
+  (prn "F - expanding above expanded line respects screen/page height"))
+;? (quit) ;? 1
+
+; collapse everything and hit page-down
+; again, we can't yet check for special keys like 'page-down so we'll use
+; upper-case J and K for moving a page down or up, respectively.
+(run-code main5
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "\nJ")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; screen shows second page of traces
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 3 : j    "
+            "+ main/ 4 : k    "
+            "+ main/ 5 : l    "
+            "                 "))
+  (prn "F - 'page-down' skips to next page after this one"))
+;? (quit) ;? 1
+
+; move cursor down, then page-down
+(run-code main6
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "jJ")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; page-down behaves the same regardless of where the cursor was
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 6 : m    "
+            "+ main/ 7 : n    "
+            "                 "))
+  (prn "F - 'page-down' skips to same place regardless of cursor position"))
+
+; try to page-down past end of trace
+(run-code main7
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "J")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; no change
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 6 : m    "
+            "+ main/ 7 : n    "
+            "                 "))
+  (prn "F - 'page-down' skips to same place regardless of cursor position"))
+
+; now page-up
+(run-code main8
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; precisely undoes previous page-down
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 3 : j    "
+            "+ main/ 4 : k    "
+            "+ main/ 5 : l    "
+            "                 "))
+  (prn "F - 'page-up' on partial page behaves as if page was full"))
+
+;; back to page 1, expand a line
+(run-code main9
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "Kkk\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+;?   (print-character 2:terminal-address/raw ((#\* literal))) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; now we have an expanded line
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "- main/ 1 : d e f"
+            "   mem : 1 a     "
+            "                 "
+            "                 "))
+  (prn "F - intermediate state after expanding a line"))
+
+; next page
+(run-code main10
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "J")
+  (k:keyboard-address <- init-keyboard s:string-address)
+;?   ($start-tracing) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; no lines skipped, page with just inner traces
+(when (~screen-contains memory*.4 17
+         (+ "   mem : 1 b     "
+            "   mem : 1 c     "
+            "   mem : 1 d     "
+            "                 "
+            "                 "))
+  (prn "F - page down continues existing expanded line"))
+
+; next page
+(run-code main11
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "J")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+; page with partial inner trace and more collapsed
+(when (~screen-contains memory*.4 17
+         (+ "   mem : 1 e     "
+            "+ main/ 2 : g hi "
+            "+ main/ 3 : j    "
+            "                 "
+            "                 "))
+  (prn "F - page down shows collapsed lines after continued expanded line at top of page"))
+;? (quit) ;? 1
+
+; page-up through an expanded line
+(run-code main12
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "   mem : 1 b     "
+            "   mem : 1 c     "
+            "   mem : 1 d     "
+            "                 "
+            "                 "))
+  (prn "F - page up understands expanded lines"))
+
+;; page up scenarios
+; skip ones starting at top of trace for now
+; page-up scenario 2
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f  <- top of page
+;   mem: 1 a
+;   mem: 1 b
+;   mem: 1 c
+;   mem: 1 d
+;   mem: 1 e
+; + run: main 2: g hi
+; + run: main 3: j      <- bottom of page
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main13
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+;?   ($print first-index-on-page:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+;?   ($print first-index-on-page:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 3:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy -1:literal)
+  (expanded-children:integer/space:1 <- copy -1:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "+ main/ 1 : d e f"
+            "+ main/ 2 : g hi "))
+  (prn "F - page-up 2"))
+
+; page-up scenario 3
+; + run: main 0: a b c
+;   mem: 0 a
+; - run: main 1: d e f  <- top of page
+;   mem: 1 a
+;   mem: 1 b            <- bottom of page
+;   mem: 1 c
+;   mem: 1 d
+;   mem: 1 e
+; + run: main 2: g hi
+; + run: main 3: j
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main14pre
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -1:literal)
+  (last-index-on-page:integer/space:1 <- copy 1:literal)
+  (last-subindex-on-page:integer/space:1 <- copy 1:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 5:literal)
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "- main/ 1 : d e f"
+            "   mem : 1 a     "
+            "   mem : 1 b     "
+            "                 "
+            "                 "))
+  (prn "F - page-up 3: initial print-page state"))
+(run-code main14post
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 0:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 1:literal)
+  (last-subindex-on-page:integer/space:1 <- copy 0:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 5:literal)
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "- main/ 1 : d e f"
+            "   mem : 1 a     "
+            "                 "
+            "                 "))
+  (prn "F - page-up 3: expected post page-up state"))
+;? (quit) ;? 1
+(run-code main14
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -1:literal)
+  (last-index-on-page:integer/space:1 <- copy 1:literal)
+  (last-subindex-on-page:integer/space:1 <- copy 1:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 5:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+;?   ($start-tracing) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "- main/ 1 : d e f"
+            "   mem : 1 a     "
+            "                 "
+            "                 "))
+  (prn "F - page-up 3"))
+;? (quit) ;? 1
+
+; page-up scenario 4
+; + run: main 0: a b c
+;   mem: 0 a
+; - run: main 1: d e f
+;   mem: 1 a
+;   mem: 1 b
+;   mem: 1 c            <- top of page
+;   mem: 1 d
+;   mem: 1 e            <- bottom of page
+; + run: main 2: g hi
+; + run: main 3: j
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main15
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy 2:literal)
+  (last-index-on-page:integer/space:1 <- copy 1:literal)
+  (last-subindex-on-page:integer/space:1 <- copy 4:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 5:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "- main/ 1 : d e f"
+            "   mem : 1 a     "
+            "   mem : 1 b     "
+            "                 "
+            "                 "))
+  (prn "F - page-up 4"))
+
+; page-up scenario 5
+; + run: main 0: a b c
+;   mem: 0 a
+; - run: main 1: d e f
+;   mem: 1 a            <- top of page
+;   mem: 1 b
+;   mem: 1 c            <- bottom of page
+;   mem: 1 d
+;   mem: 1 e
+; + run: main 2: g hi
+; + run: main 3: j
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main16pre
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy 0:literal)
+  (last-index-on-page:integer/space:1 <- copy 1:literal)
+  (last-subindex-on-page:integer/space:1 <- copy 2:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 5:literal)
+;?   ($print cursor-row:integer/space:1) ;? 1
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+;?   ($print cursor-row:integer/space:1) ;? 1
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "   mem : 1 a     "
+            "   mem : 1 b     "
+            "   mem : 1 c     "
+            "                 "
+            "                 "))
+  (prn "F - page-up 5: initial print-page state"))
+(run-code main16
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy 0:literal)
+  (last-index-on-page:integer/space:1 <- copy 1:literal)
+  (last-subindex-on-page:integer/space:1 <- copy 2:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 5:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+;?   ($dump-browser-state 3:space-address/raw/browser-state) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "- main/ 1 : d e f"
+            "   mem : 1 a     "
+            "                 "
+            "                 "))
+  (prn "F - page-up 5"))
+
+; page-up scenario 6
+; + run: main 0: a b c
+;   mem: 0 a
+; - run: main 1: d e f
+;   mem: 1 a
+;   mem: 1 b            <- top of page
+;   mem: 1 c
+;   mem: 1 d            <- bottom of page
+;   mem: 1 e
+; + run: main 2: g hi
+; + run: main 3: j
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main17
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy 1:literal)
+  (last-index-on-page:integer/space:1 <- copy 1:literal)
+  (last-subindex-on-page:integer/space:1 <- copy 3:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 5:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "- main/ 1 : d e f"
+            "   mem : 1 a     "
+            "                 "
+            "                 "))
+  (prn "F - page-up 6"))
+
+; page-up scenario 7
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f  <- top of page
+;   mem: 1 a
+;   mem: 1 b
+;   mem: 1 c
+;   mem: 1 d
+;   mem: 1 e
+; + run: main 2: g hi
+; - run: main 3: j      <- bottom of page
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main18
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 3:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -1:literal)
+  (expanded-index:integer/space:1 <- copy 3:literal)
+  (expanded-children:integer/space:1 <- copy 1:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "+ main/ 1 : d e f"
+            "+ main/ 2 : g hi "
+            "                 "))
+  (prn "F - page-up 7 - expanded index starts below bottom"))
+;? (quit) ;? 1
+
+; page-up scenario 8
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f  <- top of page
+;   mem: 1 a
+;   mem: 1 b
+;   mem: 1 c
+;   mem: 1 d
+;   mem: 1 e
+; + run: main 2: g hi
+; + run: main 3: j      <- bottom of page
+;   mem: 3 a
+; - run: main 4: k
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main19
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 3:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -1:literal)
+  (expanded-index:integer/space:1 <- copy 4:literal)
+  (expanded-children:integer/space:1 <- copy 0:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 0 : a b c"
+            "+ main/ 1 : d e f"
+            "+ main/ 2 : g hi "
+            "                 "))
+  (prn "F - page-up 8 - expanded index starts below top - 2"))
+
+; page-up scenario 9
+; - run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f
+;   mem: 1 a
+;   mem: 1 b
+;   mem: 1 c
+;   mem: 1 d
+;   mem: 1 e
+; + run: main 2: g hi
+; + run: main 3: j      <- top of page
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l      <- bottom of page
+; + run: main 6: m
+; + run: main 7: n
+(run-code main20
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 3:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 5:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy 0:literal)
+  (expanded-children:integer/space:1 <- copy 1:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "   mem : 0 a     "
+            "+ main/ 1 : d e f"
+            "+ main/ 2 : g hi "
+            "                 "))
+  (prn "F - page-up 9 - expanded index overlaps target page"))
+
+; page-up scenario 10
+; - run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f
+;   mem: 1 a
+;   mem: 1 b
+;   mem: 1 c
+;   mem: 1 d
+;   mem: 1 e
+; + run: main 2: g hi   <- top of page
+; + run: main 3: j
+;   mem: 3 a
+; + run: main 4: k      <- bottom of page
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main21pre
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 2:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 4:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy 0:literal)
+  (expanded-children:integer/space:1 <- copy 1:literal)
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+;?   ($start-tracing) ;? 2
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 2 : g hi "
+            "+ main/ 3 : j    "
+            "+ main/ 4 : k    "
+            "                 "
+            "                 "))
+  (prn "F - page-up 10: initial print-page state"))
+;? (quit) ;? 1
+(run-code main21
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 2:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 4:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy 0:literal)
+  (expanded-children:integer/space:1 <- copy 1:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "- main/ 0 : a b c"
+            "   mem : 0 a     "
+            "+ main/ 1 : d e f"
+            "                 "
+            "                 "))
+  (prn "F - page-up 10 - expanded index overlaps target page - 2"))
+;? (quit) ;? 2
+
+(reset2)
+(new-trace "trace-paginate2")
+; page-up scenario 11
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f
+; - run: main 2: g hi
+;   mem: 2 a
+; + run: main 3: j      <- top of page
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l      <- bottom of page
+; + run: main 6: m
+; + run: main 7: n
+(run-code main22
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"run: main 0: a b c
+mem: 0 a
+run: main 1: d e f
+run: main 2: g hi
+mem: 2 a
+run: main 3: j
+mem: 3 a
+run: main 4: k
+run: main 5: l
+run: main 6: m
+run: main 7: n")
+  (s:stream-address <- init-stream x:string-address)
+  (traces:instruction-trace-address-array-address <- parse-traces s:stream-address)
+  (2:terminal-address/raw <- init-fake-terminal 17:literal 15:literal)
+  (3:space-address/raw/browser-state <- browser-state traces:instruction-trace-address-array-address 3:literal/screen-height)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (4:string-address/raw <- get 2:terminal-address/raw/deref data:offset)
+  (first-index-on-page:integer/space:1 <- copy 3:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 5:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy 2:literal)
+  (expanded-children:integer/space:1 <- copy 1:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 1 : d e f"
+            "- main/ 2 : g hi "
+            "   mem : 2 a     "
+            "                 "
+            "                 "))
+  (prn "F - page-up 11 - expanded index overlaps target page - 3"))
+
+; page-up scenario 12
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f
+; - run: main 2: g hi
+;   mem: 2 a
+; + run: main 3: j
+;   mem: 3 a
+; + run: main 4: k      <- top of page
+; + run: main 5: l
+; + run: main 6: m      <- bottom of page
+; + run: main 7: n
+(run-code main23
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 4:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 6:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy 2:literal)
+  (expanded-children:integer/space:1 <- copy 1:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "- main/ 2 : g hi "
+            "   mem : 2 a     "
+            "+ main/ 3 : j    "
+            "                 "))
+  (prn "F - page-up 12 - expanded index overlaps target page - 4"))
+
+; page-up scenario 13
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f
+; - run: main 2: g hi
+;   mem: 2 a
+; + run: main 3: j
+;   mem: 3 a
+; + run: main 4: k
+; + run: main 5: l
+; + run: main 6: m      <- top of page
+; + run: main 7: n      <- bottom of page
+(run-code main24
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 6:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 7:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy 2:literal)
+  (expanded-children:integer/space:1 <- copy 1:literal)
+  (s:string-address <- new "K")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 3 : j    "
+            "+ main/ 4 : k    "
+            "+ main/ 5 : l    "
+            "                 "))
+  (prn "F - page-up 13 - expanded index far above target page"))
+
+(run-code main25
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (s:string-address <- new "kk\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 3 : j    "
+            "- main/ 4 : k    "
+            "+ main/ 5 : l    "
+            "                 "))
+  (prn "F - process-key expands a trace index on any page"))
+(run-code main26
+  (replace-character 2:terminal-address/raw ((#\* literal)))
+)
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 3 : j    "
+            "* main/ 4 : k    "
+            "+ main/ 5 : l    "
+            "                 "))
+  (prn "F - process-key resets the cursor after expand"))
+;? (quit) ;? 1
+
+(run-code main27
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  ; reset previous cursor
+  (replace-character 2:terminal-address/raw ((#\- literal)))
+  (s:string-address <- new "j\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 3 : j    "
+            "+ main/ 4 : k    "
+            "- main/ 5 : l    "
+            "                 "))
+  (prn "F - process-key expands a trace index on any page when there's an expanded trace index above it on the same page"))
+
+; expand scenario
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f
+; - run: main 2: g hi
+;   mem: 2 a              <- top of page
+; + run: main 3: j
+;   mem: 3 a
+; + run: main 4: k        <- bottom of page
+; + run: main 5: l
+; + run: main 6: m
+; + run: main 7: n
+(run-code main28
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 2:literal)
+  (first-subindex-on-page:integer/space:1 <- copy 0:literal)
+  (last-index-on-page:integer/space:1 <- copy 4:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy 2:literal)
+  (expanded-children:integer/space:1 <- copy 1:literal)
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+  (s:string-address <- new "kk\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+;?   (replace-character 2:terminal-address/raw ((#\* literal))) ;? 2
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+;?          (+ "   mem : 2 a     "  ; after print-page
+;?             "+ main/ 3 : j    "
+;?             "+ main/ 4 : k    "
+;?             "                 "))
+         (+ "+ main/ 2 : g hi "
+            "- main/ 3 : j    "
+            "   mem : 3 a     "
+            "                 "))
+;?          (+ "- main/ 3 : j    "  ; alternative interpretation in case the above isn't intuitive
+;?             "   mem : 3 a     "
+;?          (+ "- main/ 4 : k    "
+;?             "                 "))
+  (prn "F - process-key expands trace index on a page that starts with a partial expanded trace"))
+
+(reset2)
+(new-trace "trace-paginate3")
+; expand scenario
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f
+;   mem: 1 a
+;   mem: 1 b              <- top of page
+;   mem: 1 c
+; + run: main 2: g hi     <- bottom of page
+;   mem: 2 a
+; + run: main 3: j
+; + run: main 4: k
+; + run: main 5: l
+(run-code main29
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (x:string-address <- new
+"run: main 0: a b c
+mem: 0 a
+run: main 1: d e f
+mem: 1 a
+mem: 1 b
+mem: 1 c
+run: main 2: g hi
+mem: 2 a
+run: main 3: j
+run: main 4: k
+run: main 5: l")
+  (s:stream-address <- init-stream x:string-address)
+  (traces:instruction-trace-address-array-address <- parse-traces s:stream-address)
+  (2:terminal-address/raw <- init-fake-terminal 17:literal 15:literal)
+  (3:space-address/raw/browser-state <- browser-state traces:instruction-trace-address-array-address 3:literal/screen-height)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (4:string-address/raw <- get 2:terminal-address/raw/deref data:offset)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy 1:literal)
+  (last-index-on-page:integer/space:1 <- copy 3:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 3:literal)
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+;?   (replace-character 2:terminal-address/raw ((#\* literal))) ;? 1
+  (s:string-address <- new "k\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+;?          (+ "   mem : 1 b     "  ; after print-page
+;?             "   mem : 1 c     "
+;?             "+ main/ 2 : g hi "
+;?             "*                "
+;?             "                 "))
+         (+ "+ main/ 1 : d e f"
+            "- main/ 2 : g hi "
+            "   mem : 2 a     "
+            "                 "
+            "                 "))
+  (prn "F - process-key expands trace index on a page that starts with a partial expanded trace - 2"))
+
+; expand scenario
+; + run: main 0: a b c
+;   mem: 0 a
+; + run: main 1: d e f
+;   mem: 1 a              <- top of page
+;   mem: 1 b
+;   mem: 1 c              <- bottom of page
+; + run: main 2: g hi
+;   mem: 2 a
+; + run: main 3: j
+; + run: main 4: k
+; + run: main 5: l
+(run-code main30
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  (first-index-on-page:integer/space:1 <- copy 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy 0:literal)
+  (last-index-on-page:integer/space:1 <- copy 1:literal)
+  (last-subindex-on-page:integer/space:1 <- copy 2:literal)
+  (expanded-index:integer/space:1 <- copy 1:literal)
+  (expanded-children:integer/space:1 <- copy 3:literal)
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+  (s:string-address <- new "k\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+;?   (replace-character 2:terminal-address/raw ((#\* literal))) ;? 1
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+;?          (+ "   mem : 1 a     "  ; after print-page
+;?             "   mem : 1 b     "
+;?             "   mem : 1 c     "
+;?             "*                "))
+         (+ "+ main/ 1 : d e f"
+            "+ main/ 2 : g hi "
+            "+ main/ 3 : j    "))
+  (prn "F - process-key expands trace index on a page with only subindex lines"))
+
+(run-code main31
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  ; reinitialize
+  (first-index-on-page:integer/space:1 <- copy 0:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 2:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy -1:literal)
+  (expanded-children:integer/space:1 <- copy -1:literal)
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+;?   (replace-character 2:terminal-address/raw ((#\* literal))) ;? 1
+  (s:string-address <- new "Jjj\n")
+  (k:keyboard-address <- init-keyboard s:string-address)
+;?   ($print (("test: first subindex " literal))) ;? 1
+;?   ($print first-subindex-on-page:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+;?   ($print (("test: first subindex 2 " literal))) ;? 1
+;?   ($print first-subindex-on-page:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+;?   ($print (("test: first subindex 3 " literal))) ;? 1
+;?   ($print first-subindex-on-page:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+;?   ($print (("test: first subindex 4 " literal))) ;? 1
+;?   ($print first-subindex-on-page:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+(when (~screen-contains memory*.4 17
+         (+ "+ main/ 3 : j    "
+            "+ main/ 4 : k    "
+            "- main/ 5 : l    "
+            "                 "
+            "                 "))
+  (prn "F - process-key expands final index of trace at bottom of page"))
+
+(run-code main32
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- copy 3:space-address/raw/browser-state)
+  ; reinitialize
+  (first-index-on-page:integer/space:1 <- copy 0:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (last-index-on-page:integer/space:1 <- copy 2:literal)
+  (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+  (expanded-index:integer/space:1 <- copy -1:literal)
+  (expanded-children:integer/space:1 <- copy -1:literal)
+  (to-top 0:space-address/browser-state 2:terminal-address/raw)
+  (print-page 0:space-address/browser-state 2:terminal-address/raw)
+;?   (replace-character 2:terminal-address/raw ((#\* literal))) ;? 1
+  (s:string-address <- new "kk\nJjj")
+  (k:keyboard-address <- init-keyboard s:string-address)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (process-key 3:space-address/raw/browser-state k:keyboard-address 2:terminal-address/raw)
+  (5:integer-address/raw <- get-address 2:terminal-address/raw/deref cursor-row:offset)
+  )
+(each routine completed-routines*
+  (awhen rep.routine!error
+    (prn "error - " it)))
+;? (prn (memory* memory*.5)) ;? 1
+(when (~is 3 (memory* memory*.5))
+  (prn "F - key movement stays within screen bounds, even when no next trace on page"))
+
+(reset2)
+;? (print-times) ;? 3
diff --git a/archive/1.vm.arc/trace.mu b/archive/1.vm.arc/trace.mu
new file mode 100644
index 00000000..eba9b477
--- /dev/null
+++ b/archive/1.vm.arc/trace.mu
@@ -0,0 +1,1092 @@
+(and-record trace [
+  label:string-address
+  contents:string-address
+])
+(address trace-address (trace))
+(array trace-address-array (trace-address))
+(address trace-address-array-address (trace-address-array))
+(address trace-address-array-address-address (trace-address-array-address))
+
+(and-record instruction-trace [
+  call-stack:string-address-array-address
+  pc:string-address  ; should be integer?
+  instruction:string-address
+  children:trace-address-array-address
+])
+(address instruction-trace-address (instruction-trace))
+(array instruction-trace-address-array (instruction-trace-address))
+(address instruction-trace-address-array-address (instruction-trace-address-array))
+
+(function parse-traces [  ; stream-address -> instruction-trace-address-array-address
+  (default-space:space-address <- new space:literal 30:literal)
+;?   ($print (("parse-traces\n" literal))) ;? 2
+  (in:stream-address <- next-input)
+  ; check input size
+  ($print (("counting lines\n" literal)))
+  (n:integer <- copy 0:literal)
+  { begin
+    (done?:boolean <- end-of-stream? in:stream-address)
+    (break-if done?:boolean)
+;?     ($start-tracing) ;? 1
+    (c:character <- read-character in:stream-address)
+    { begin
+      (newline?:boolean <- equal c:character ((#\newline literal)))
+      (break-unless newline?:boolean)
+      (n:integer <- add n:integer 1:literal)
+      { begin
+;?         (print?:boolean <- divides? n:integer 100:literal)
+;?         (break-unless print?:boolean)
+        ($print ((" " literal)))
+        ($print n:integer)
+        ($print (("\n" literal)))
+      }
+    }
+;?     ($quit) ;? 1
+    (loop)
+  }
+  ($print n:integer)
+  ($print ((" lines\n" literal)))
+  (in:stream-address <- rewind-stream in:stream-address)
+  ; prepare result
+  (result:buffer-address <- init-buffer 30:literal)
+  (curr-tail:instruction-trace-address <- copy nil:literal)
+  (ch:buffer-address <- init-buffer 5:literal)  ; accumulator for traces between instructions
+  (run:string-address/const <- new "run")
+  ($print (("parsing\n" literal)))
+  (n:integer <- copy 0:literal)
+  ; reading each line from 'in'
+  { begin
+    next-line
+    (done?:boolean <- end-of-stream? in:stream-address)
+;?     ($print done?:boolean) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    (break-if done?:boolean)
+    ; parse next line as a generic trace
+    (line:string-address <- read-line in:stream-address)
+    { begin
+      (n:integer <- add n:integer 1:literal)
+      (print?:boolean <- divides? n:integer 100:literal)
+      (break-unless print?:boolean)
+      ($print ((" " literal)))
+      ($print n:integer)
+      ($print (("\n" literal)))
+    }
+;?     (print-string nil:literal/terminal line:string-address) ;? 1
+    (f:trace-address <- parse-trace line:string-address)
+    (l:string-address <- get f:trace-address/deref label:offset)
+    { begin
+      ; if it's an instruction trace with label 'run'
+      (inst?:boolean <- string-equal l:string-address run:string-address/const)
+      (break-unless inst?:boolean)
+      ; add accumulated traces to curr-tail
+      { begin
+        (break-unless curr-tail:instruction-trace-address)
+        (c:trace-address-array-address-address <- get-address curr-tail:instruction-trace-address/deref children:offset)
+        (c:trace-address-array-address-address/deref <- to-array ch:buffer-address)
+        ; clear 'ch'
+        (ch:buffer-address <- init-buffer 5:literal)
+      }
+      ; append a new curr-tail to result
+      (curr-tail:instruction-trace-address <- parse-instruction-trace f:trace-address)
+      (result:buffer-address <- append result:buffer-address curr-tail:instruction-trace-address)
+      (jump next-line:offset)  ; loop
+    }
+    ; otherwise accumulate trace
+    (loop-unless curr-tail:instruction-trace-address)
+    (ch:buffer-address <- append ch:buffer-address f:trace-address)
+    (loop)
+  }
+  ; add accumulated traces to final curr-tail
+  ; todo: test
+  { begin
+    (break-unless curr-tail:instruction-trace-address)
+    (c:trace-address-array-address-address <- get-address curr-tail:instruction-trace-address/deref children:offset)
+    (c:trace-address-array-address-address/deref <- to-array ch:buffer-address)
+  }
+  (s:instruction-trace-address-array-address <- to-array result:buffer-address)
+  (reply s:instruction-trace-address-array-address)
+])
+
+(function parse-instruction-trace [  ; trace-address -> instruction-trace-address
+  (default-space:space-address <- new space:literal 30:literal)
+;?   ($print (("parse-instruction-trace\n" literal))) ;? 1
+  (in:trace-address <- next-input)
+  (buf:string-address <- get in:trace-address/deref contents:offset)
+;?   (print-string nil:literal buf:string-address) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (result:instruction-trace-address <- new instruction-trace:literal)
+  (f1:string-address rest:string-address <- split-first buf:string-address ((#\space literal)))
+;?   ($print (("call-stack: " literal))) ;? 1
+;?   (print-string nil:literal f1:string-address) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (cs:string-address-array-address-address <- get-address result:instruction-trace-address/deref call-stack:offset)
+  (cs:string-address-array-address-address/deref <- split f1:string-address ((#\/ literal)))
+  (p:string-address-address <- get-address result:instruction-trace-address/deref pc:offset)
+  (delim:string-address <- new ": ")
+  (p:string-address-address/deref rest:string-address <- split-first-at-substring rest:string-address delim:string-address)
+  (inst:string-address-address <- get-address result:instruction-trace-address/deref instruction:offset)
+  (inst:string-address-address/deref <- copy rest:string-address)
+  (reply result:instruction-trace-address)
+])
+
+(function parse-trace [  ; string-address -> trace-address
+  (default-space:space-address <- new space:literal 30:literal)
+;?   ($print (("parse-trace\n" literal))) ;? 1
+  (in:string-address <- next-input)
+  (result:trace-address <- new trace:literal)
+  (delim:string-address <- new ": ")
+  (first:string-address rest:string-address <- split-first-at-substring in:string-address delim:string-address)
+  (l:string-address-address <- get-address result:trace-address/deref label:offset)
+  (l:string-address-address/deref <- copy first:string-address)
+  (c:string-address-address <- get-address result:trace-address/deref contents:offset)
+  (c:string-address-address/deref <- copy rest:string-address)
+  (reply result:trace-address)
+])
+
+(function print-trace [
+  (default-space:space-address <- new space:literal 30:literal)
+  (screen:terminal-address <- next-input)
+  (x:trace-address <- next-input)
+  (l:string-address <- get x:trace-address/deref label:offset)
+  (clear-line screen:terminal-address)
+  (print-string screen:terminal-address l:string-address)
+  (print-character screen:terminal-address ((#\space literal)))
+  (print-character screen:terminal-address ((#\: literal)))
+  (print-character screen:terminal-address ((#\space literal)))
+  (c:string-address <- get x:trace-address/deref contents:offset)
+  (print-string screen:terminal-address c:string-address)
+])
+
+(function print-instruction-trace-parent [
+  (default-space:space-address <- new space:literal 30:literal)
+  (screen:terminal-address <- next-input)
+  (x:instruction-trace-address <- next-input)
+  (0:space-address/names:browser-state <- next-input)
+  (clear-line screen:terminal-address)
+  (print-character screen:terminal-address ((#\- literal)))
+  (print-character screen:terminal-address ((#\space literal)))
+  ; print call stack
+  (c:string-address-array-address <- get x:instruction-trace-address/deref call-stack:offset)
+  (i:integer <- copy 0:literal)
+  (len:integer <- length c:string-address-array-address/deref)
+  { begin
+    (done?:boolean <- greater-or-equal i:integer len:integer)
+    (break-if done?:boolean)
+    (s:string-address <- index c:string-address-array-address/deref i:integer)
+    (print-string screen:terminal-address s:string-address)
+    (print-character screen:terminal-address ((#\/ literal)))
+    (i:integer <- add i:integer 1:literal)
+    (loop)
+  }
+  ; print pc
+  (print-character screen:terminal-address ((#\space literal)))
+  (p:string-address <- get x:instruction-trace-address/deref pc:offset)
+  (print-string screen:terminal-address p:string-address)
+  ; print instruction
+  (print-character screen:terminal-address ((#\space literal)))
+  (print-character screen:terminal-address ((#\: literal)))
+  (print-character screen:terminal-address ((#\space literal)))
+  (i:string-address <- get x:instruction-trace-address/deref instruction:offset)
+  (print-string screen:terminal-address i:string-address)
+  (add-line 0:space-address/browser-state screen:terminal-address)
+])
+
+(function print-instruction-trace [
+  (default-space:space-address <- new space:literal 30:literal)
+  (screen:terminal-address <- next-input)
+  (x:instruction-trace-address <- next-input)
+  (0:space-address/names:browser-state <- next-input)
+  (print-instruction-trace-parent screen:terminal-address x:instruction-trace-address 0:space-address/browser-state)
+  ; print children
+  (ch:trace-address-array-address <- get x:instruction-trace-address/deref children:offset)
+  (i:integer <- copy 0:literal)
+  { begin
+    ; todo: test
+    (break-if ch:trace-address-array-address)
+    (reply)
+  }
+  (len:integer <- length ch:trace-address-array-address/deref)
+  (expanded-children:integer/space:1 <- copy len:integer)
+  { begin
+;?     ($print (("i: " literal))) ;? 1
+;?     ($print i:integer) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    ; until done with trace
+    (done?:boolean <- greater-or-equal i:integer len:integer)
+    (break-if done?:boolean)
+    ; or screen ends
+    (screen-done?:boolean <- greater-or-equal cursor-row:integer/space:1 screen-height:integer/space:1)
+    (break-if screen-done?:boolean)
+    (t:trace-address <- index ch:trace-address-array-address/deref i:integer)
+    (print-character screen:terminal-address ((#\space literal)))
+    (print-character screen:terminal-address ((#\space literal)))
+    (print-character screen:terminal-address ((#\space literal)))
+    (print-trace screen:terminal-address t:trace-address)
+    (add-line 0:space-address/browser-state screen:terminal-address)
+    (last-subindex-on-page:integer/space:1 <- copy i:integer)
+;?     ($print (("subindex: " literal))) ;? 1
+;?     ($print last-subindex-on-page:integer/space:1) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    (i:integer <- add i:integer 1:literal)
+    (loop)
+  }
+])
+
+(function print-instruction-trace-collapsed [
+  (default-space:space-address <- new space:literal 30:literal)
+  (screen:terminal <- next-input)
+  (x:instruction-trace-address <- next-input)
+  (browser-state:space-address <- next-input)
+  (clear-line screen:terminal-address)
+  (print-character screen:terminal-address ((#\+ literal)))
+  (print-character screen:terminal-address ((#\space literal)))
+  ; print call stack
+  (c:string-address-array-address <- get x:instruction-trace-address/deref call-stack:offset)
+  (i:integer <- copy 0:literal)
+  (len:integer <- length c:string-address-array-address/deref)
+  { begin
+    (done?:boolean <- greater-or-equal i:integer len:integer)
+    (break-if done?:boolean)
+    (s:string-address <- index c:string-address-array-address/deref i:integer)
+    (print-string screen:terminal-address s:string-address)
+;?     (print-character screen:terminal-address ((#\space literal)))
+    (print-character screen:terminal-address ((#\/ literal)))
+;?     (print-character screen:terminal-address ((#\space literal)))
+    (i:integer <- add i:integer 1:literal)
+    (loop)
+  }
+  ; print pc
+  (print-character screen:terminal-address ((#\space literal)))
+  (p:string-address <- get x:instruction-trace-address/deref pc:offset)
+  (print-string screen:terminal-address p:string-address)
+  ; print instruction
+  (print-character screen:terminal-address ((#\space literal)))
+  (print-character screen:terminal-address ((#\: literal)))
+  (print-character screen:terminal-address ((#\space literal)))
+  (i:string-address <- get x:instruction-trace-address/deref instruction:offset)
+  (print-string screen:terminal-address i:string-address)
+  (add-line browser-state:space-address screen:terminal-address)
+])
+
+(function instruction-trace-num-children [
+  (default-space:space-address <- new space:literal 30:literal)
+  (traces:instruction-trace-address-array-address <- next-input)
+  (index:integer <- next-input)
+  (tr:instruction-trace-address <- index traces:instruction-trace-address-array-address/deref index:integer)
+  (tr-children:trace-address-array-address <- get tr:instruction-trace-address/deref children:offset)
+  (n:integer <- length tr-children:instruction-trace-address-array-address/deref)
+  (reply n:integer)
+])
+
+;; data structure
+(function browser-state [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ; trace state
+  (traces:instruction-trace-address-array-address <- next-input)  ; the ground truth being rendered
+  (expanded-index:integer <- copy -1:literal)  ; currently trace browser only ever shows one item expanded
+  (expanded-children:integer <- copy -1:literal)
+  (first-index-on-page:integer <- copy 0:literal)  ; 'outer' line with label 'run'
+  (first-subindex-on-page:integer <- copy -2:literal)  ; 'inner' line with other labels; -2 or lower => not expanded; -1 => expanded and include parent; 0 => expanded and start at first child
+  (last-index-on-page:integer <- copy 0:literal)
+  (last-subindex-on-page:integer <- copy -2:literal)
+  ; screen state
+  (screen-height:integer <- next-input)  ; 'hardware' limitation
+  (app-height:integer <- copy 0:literal)  ; area of the screen we're responsible for; can't be larger than screen-height
+  (printed-height:integer <- copy 0:literal)  ; part of screen that currently has text; can't be larger than app-height
+  (cursor-row:integer <- copy 0:literal)  ; position of cursor on screen; can't be larger than printed-height + 1
+  (reply default-space:space-address)
+])
+
+(function $dump-browser-state [
+  (default-space:space-address/names:browser-state <- next-input)
+  ($print expanded-index:integer)
+  ($print (("*" literal)))
+  ($print expanded-children:integer)
+  ($print ((": " literal)))
+  ($print first-index-on-page:integer)
+  ($print (("/" literal)))
+  ($print first-subindex-on-page:integer)
+  ($print ((" => " literal)))
+  ($print last-index-on-page:integer)
+  ($print (("/" literal)))
+  ($print last-subindex-on-page:integer)
+  ($print (("\n" literal)))
+  ($print cursor-row:integer)
+  ($print ((" " literal)))
+  ($print printed-height:integer)
+  ($print ((" " literal)))
+  ($print app-height:integer)
+  ($print ((" " literal)))
+  ($print screen-height:integer)
+  ($print (("\n" literal)))
+])
+
+(function down [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  ; if at expanded, skip past nested lines
+  { begin
+    (no-expanded?:boolean <- less-than expanded-index:integer/space:1 0:literal)
+    (break-if no-expanded?:boolean)
+    (at-expanded?:boolean <- equal cursor-row:integer/space:1 expanded-index:integer/space:1)
+    (break-unless at-expanded?:boolean)
+;?     ($print (("down: at expanded index\n" literal))) ;? 1
+    (n:integer <- instruction-trace-num-children traces:instruction-trace-address-array-address/space:1 expanded-index:integer/space:1)
+    (n:integer <- add n:integer 1:literal)
+    (i:integer <- copy 0:literal)
+    { begin
+      (done?:boolean <- greater-or-equal i:integer n:integer)
+      (break-if done?:boolean)
+      (at-bottom?:boolean <- greater-or-equal cursor-row:integer/space:1 printed-height:integer/space:1)
+      (break-if at-bottom?:boolean)
+;?       ($print (("down: incrementing\n" literal))) ;? 1
+      (cursor-row:integer/space:1 <- add cursor-row:integer/space:1 1:literal)
+      (cursor-down screen:terminal-address)
+      (i:integer <- add i:integer 1:literal)
+      (loop)
+    }
+    (reply)
+  }
+  ; if not at bottom, move cursor down
+  { begin
+    (at-bottom?:boolean <- greater-or-equal cursor-row:integer/space:1 printed-height:integer/space:1)
+    (break-if at-bottom?:boolean)
+    (cursor-row:integer/space:1 <- add cursor-row:integer/space:1 1:literal)
+    (cursor-down screen:terminal-address)
+  }
+])
+
+(function up [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  ; if at expanded, skip past nested lines
+  { begin
+    (no-expanded?:boolean <- less-than expanded-index:integer/space:1 0:literal)
+    (break-if no-expanded?:boolean)
+    (n:integer <- instruction-trace-num-children traces:instruction-trace-address-array-address/space:1 expanded-index:integer/space:1)
+    (n:integer <- add n:integer 1:literal)
+    (cursor-row-below-expanded:integer <- add expanded-index:integer/space:1 n:integer)
+    (just-below-expanded?:boolean <- equal cursor-row:integer/space:1 cursor-row-below-expanded:integer)
+    (break-unless just-below-expanded?:boolean)
+    (i:integer <- copy 0:literal)
+    { begin
+      (done?:boolean <- greater-or-equal i:integer n:integer)
+      (break-if done?:boolean)
+      (at-top?:boolean <- lesser-or-equal cursor-row:integer/space:1 0:literal)
+      (break-if at-top?:boolean)
+      (cursor-row:integer/space:1 <- subtract cursor-row:integer/space:1 1:literal)
+      (cursor-up screen:terminal-address)
+      (i:integer <- add i:integer 1:literal)
+      (loop)
+    }
+    (reply)
+  }
+  ; if not at top, move cursor up
+  { begin
+    (at-top?:boolean <- lesser-or-equal cursor-row:integer/space:1 0:literal)
+    (break-if at-top?:boolean)
+    (cursor-row:integer/space:1 <- subtract cursor-row:integer/space:1 1:literal)
+    (cursor-up screen:terminal-address)
+  }
+])
+
+(function to-bottom [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  { begin
+    (at-bottom?:boolean <- greater-or-equal cursor-row:integer/space:1 printed-height:integer/space:1)
+    (break-if at-bottom?:boolean)
+    (down 0:space-address screen:terminal-address)
+    (loop)
+  }
+])
+
+(function to-top [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  { begin
+    (at-top?:boolean <- lesser-or-equal cursor-row:integer/space:1 0:literal)
+    (break-if at-top?:boolean)
+    (up 0:space-address screen:terminal-address)
+    (loop)
+  }
+])
+
+(function back-to [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  (target-row:integer <- next-input)
+;?   ($print (("before back-to: " literal))) ;? 1
+;?   ($print cursor-row:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  { begin
+    (below-target?:boolean <- greater-than cursor-row:integer/space:1 target-row:integer)
+    (break-unless below-target?:boolean)
+;?     ($print (("below target\n" literal))) ;? 1
+    (up 0:space-address screen:terminal-address)
+    (loop)
+  }
+  { begin
+    (above-target?:boolean <- less-than cursor-row:integer/space:1 target-row:integer)
+    (break-unless above-target?:boolean)
+;?     ($print (("above target\n" literal))) ;? 1
+    (down 0:space-address screen:terminal-address)
+    (loop)
+  }
+;?   ($print (("after back-to: " literal))) ;? 1
+;?   ($print cursor-row:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+])
+
+(function add-line [  ; move down, adding line if necessary
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  { begin
+    (at-bottom?:boolean <- greater-or-equal cursor-row:integer/space:1 printed-height:integer/space:1)
+    (break-unless at-bottom?:boolean)
+    { begin
+      (screen-full?:boolean <- greater-or-equal app-height:integer/space:1 screen-height:integer/space:1)
+      (break-unless screen-full?:boolean)
+      (cursor-to-next-line screen:terminal-address)
+      (cursor-up screen:terminal-address)
+      (reply)
+    }
+    (printed-height:integer/space:1 <- add printed-height:integer/space:1 1:literal)
+    ; update app-height if necessary
+    { begin
+      (grow-max?:boolean <- greater-than printed-height:integer/space:1 app-height:integer/space:1)
+      (break-unless grow-max?:boolean)
+      (app-height:integer/space:1 <- copy printed-height:integer/space:1)
+    }
+  }
+  (cursor-row:integer/space:1 <- add cursor-row:integer/space:1 1:literal)
+  (cursor-to-next-line screen:terminal-address)
+])
+
+;; initial screen state
+(function print-traces-collapsed [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+;?   ($print (("print traces collapsed\n" literal))) ;? 1
+  (print-traces-collapsed-from 0:space-address/browser-state screen:terminal-address 0:literal/from)
+  (clear-rest-of-page 0:space-address/browser-state screen:terminal-address)
+])
+
+(function print-traces-collapsed-from [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  (trace-index:integer <- next-input)
+  (limit-index:integer <- next-input)  ; print until this index (exclusive)
+  ; compute bound
+  (max:integer <- length traces:instruction-trace-address-array-address/space:1/deref)
+  { begin
+    (break-unless limit-index:integer)
+    (max:integer <- min max:integer limit-index:integer)
+  }
+  ; print remaining traces collapsed
+  { begin
+    ; until trace ends
+    (trace-done?:boolean <- greater-or-equal trace-index:integer max:integer)
+    (break-if trace-done?:boolean)
+    ; or screen ends
+    (screen-done?:boolean <- greater-or-equal cursor-row:integer/space:1 screen-height:integer/space:1)
+    (break-if screen-done?:boolean)
+;?     ($print (("screen not done\n" literal))) ;? 1
+    ; continue printing trace lines
+    (tr:instruction-trace-address <- index traces:instruction-trace-address-array-address/space:1/deref trace-index:integer)
+    (last-index-on-page:integer/space:1 <- copy trace-index:integer)
+;?     ($print (("setting last index: " literal))) ;? 1
+;?     ($print last-index-on-page:integer/space:1) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    (last-subindex-on-page:integer/space:1 <- copy -2:literal)
+    (print-instruction-trace-collapsed screen:terminal-address tr:instruction-trace-address 0:space-address/browser-state)
+    (trace-index:integer <- add trace-index:integer 1:literal)
+    (loop)
+  }
+])
+
+(function clear-rest-of-page [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  { begin
+    (done?:boolean <- greater-or-equal cursor-row:integer/space:1 app-height:integer/space:1)
+    (break-if done?:boolean)
+    (clear-line screen:terminal-address)
+    (down 0:space-address/browser-state screen:terminal-address)
+    (loop)
+  }
+])
+
+(function print-page [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+;?   ($dump-browser-state 0:space-address/browser-state) ;? 3
+  ; if top inside expanded index, complete existing trace
+  (first-full-index:integer <- copy first-index-on-page:integer/space:1)
+  { begin
+    (screen-done?:boolean <- greater-or-equal cursor-row:integer/space:1 screen-height:integer/space:1)
+    (break-unless screen-done?:boolean)
+    (reply)
+  }
+;?   ($print (("\nAAA\n" literal))) ;? 4
+  { begin
+    (partial-trace?:boolean <- equal first-index-on-page:integer/space:1 expanded-index:integer/space:1)
+    (break-unless partial-trace?:boolean)
+;?   ($print (("AAA: partial\n" literal))) ;? 4
+    (first-full-index:integer <- add first-full-index:integer 1:literal)
+    (tr:instruction-trace-address <- index traces:instruction-trace-address-array-address/space:1/deref first-index-on-page:integer/space:1)
+    { begin
+      (print-parent?:boolean <- equal first-subindex-on-page:integer/space:1 -1:literal)
+      (break-unless print-parent?:boolean)
+      (print-instruction-trace-parent screen:terminal-address tr:instruction-trace-address 0:space-address/browser-state)
+    }
+    (ch:trace-address-array-address <- get tr:instruction-trace-address/deref children:offset)
+    (i:integer <- max first-subindex-on-page:integer/space:1 0:literal)
+    ; print any remaining data in the currently expanded trace
+    { begin
+      ; until done with trace
+      (done?:boolean <- greater-or-equal i:integer expanded-children:integer/space:1)
+      (break-if done?:boolean)
+      ; or screen ends
+      (screen-done?:boolean <- greater-or-equal cursor-row:integer/space:1 screen-height:integer/space:1)
+      (break-if screen-done?:boolean)
+;?       ($print (("AAA printing subtrace\n" literal))) ;? 3
+      (t:trace-address <- index ch:trace-address-array-address/deref i:integer)
+      (print-character screen:terminal-address ((#\space literal)))
+      (print-character screen:terminal-address ((#\space literal)))
+      (print-character screen:terminal-address ((#\space literal)))
+      (print-trace screen:terminal-address t:trace-address)
+      (add-line 0:space-address/browser-state screen:terminal-address)
+      (last-subindex-on-page:integer/space:1 <- copy i:integer)
+      (i:integer <- add i:integer 1:literal)
+      (loop)
+    }
+  }
+;?   ($print (("AAA 3: " literal))) ;? 5
+;?   ($print cursor-row:integer/space:1) ;? 4
+;?   ($print (("\n" literal))) ;? 4
+  { begin
+    (screen-done?:boolean <- greater-or-equal cursor-row:integer/space:1 screen-height:integer/space:1)
+    (break-unless screen-done?:boolean)
+    (reply)
+  }
+;?   ($print (("AAA 4\n" literal))) ;? 5
+  { begin
+    (has-expanded?:boolean <- greater-or-equal expanded-index:integer/space:1 0:literal)
+    (break-if has-expanded?:boolean)
+;?     ($print (("AAA 5a\n" literal))) ;? 4
+    (print-traces-collapsed-from 0:space-address/browser-state screen:terminal-address first-full-index:integer)
+    (clear-rest-of-page 0:space-address/browser-state screen:terminal-address)
+    (reply)
+  }
+  { begin
+    (below-expanded?:boolean <- greater-than first-full-index:integer expanded-index:integer/space:1)
+    (break-unless below-expanded?:boolean)
+;?     ($print (("AAA 5b\n" literal))) ;? 4
+    (print-traces-collapsed-from 0:space-address/browser-state screen:terminal-address first-full-index:integer)
+    (clear-rest-of-page 0:space-address/browser-state screen:terminal-address)
+    (reply)
+  }
+  ; trace has an expanded index and it's below first-full-index
+  ; print traces collapsed until expanded index
+;?   ($print (("AAA 5c\n" literal))) ;? 4
+  (print-traces-collapsed-from 0:space-address/browser-state screen:terminal-address first-full-index:integer expanded-index:integer/space:1/until)
+  ; if room, start printing expanded index
+  { begin
+    (done?:boolean <- greater-or-equal cursor-row:integer/space:1 screen-height:integer/space:1)
+    (break-if done?:boolean)
+    (tr:instruction-trace-address <- index traces:instruction-trace-address-array-address/space:1/deref expanded-index:integer/space:1)
+    (print-instruction-trace screen:terminal-address tr:instruction-trace-address 0:space-address/browser-state)
+    (clear-rest-of-page 0:space-address/browser-state screen:terminal-address)
+  }
+])
+
+(function cursor-row-to-trace-index [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (n:integer/screen <- next-input)
+;?   ($print (("to trace index: first subindex " literal))) ;? 1
+;?   ($print first-subindex-on-page:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+;?   ($print (("cursor-to-index: n " literal))) ;? 2
+;?   ($print n:integer) ;? 2
+;?   ($print (("\n" literal))) ;? 2
+;?   ($print (("cursor-to-index: first index " literal))) ;? 2
+;?   ($print first-index-on-page:integer/space:1) ;? 2
+;?   ($print (("\n" literal))) ;? 2
+  (simple-result:integer <- add first-index-on-page:integer/space:1 n:integer)
+;?   ($print (("cursor-to-index: simple result " literal))) ;? 2
+;?   ($print simple-result:integer) ;? 2
+;?   ($print (("\n" literal))) ;? 2
+  ; no row expanded? no munging needed
+  { begin
+    (has-expanded?:boolean <- greater-or-equal expanded-index:integer/space:1 0:literal)
+    (break-if has-expanded?:boolean)
+    (reply simple-result:integer)
+  }
+  ; expanded row above current page? no munging needed
+  { begin
+    (below-expanded?:boolean <- less-than expanded-index:integer/space:1 first-index-on-page:integer/space:1)
+    (break-unless below-expanded?:boolean)
+    (reply simple-result:integer)
+  }
+  ; expanded row at top of current page and partial?
+  { begin
+    (expanded-at-top?:boolean <- equal first-index-on-page:integer/space:1 expanded-index:integer/space:1)
+;?     ($print (("cursor-to-index: first subindex " literal))) ;? 2
+;?     ($print first-subindex-on-page:integer/space:1) ;? 2
+;?     ($print (("\n" literal))) ;? 2
+    (partial-at-top?:boolean <- greater-or-equal first-subindex-on-page:integer/space:1 0:literal)
+;?     ($print (("AAA\n" literal))) ;? 1
+    (partial-expanded-at-top?:boolean <- and expanded-at-top?:boolean partial-at-top?:boolean)
+    (break-unless partial-expanded-at-top?:boolean)
+;?     ($print (("expanded child at top of page\n" literal))) ;? 2
+    (expanded-children-on-page:integer <- subtract expanded-children:integer/space:1 first-subindex-on-page:integer/space:1)
+    (result:integer <- subtract simple-result:integer expanded-children-on-page:integer)
+    (result:integer <- add result:integer 1:literal)
+    (result:integer <- max result:integer first-index-on-page:integer/space:1)
+    (reply result:integer)
+  }
+  ; expanded row is below current page? no munging needed
+  { begin
+    (above-expanded?:boolean <- lesser-or-equal last-index-on-page:integer/space:1 expanded-index:integer/space:1 )
+    (break-unless above-expanded?:boolean)
+    (reply simple-result:integer)
+  }
+  (expanded-index-cursor-row:integer <- subtract expanded-index:integer/space:1 first-index-on-page:integer/space:1)
+  ; cursor is above expanded index? no munging needed
+  { begin
+    (above-expanded?:boolean <- lesser-or-equal cursor-row:integer/space:1 expanded-index-cursor-row:integer)
+    (break-unless above-expanded?:boolean)
+    (reply simple-result:integer)
+  }
+  (result:integer/index <- subtract simple-result:integer expanded-children:integer/space:1)
+  (reply result:integer/index)
+])
+
+(function back-to-index [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (screen:terminal-address <- next-input)
+  (target-index:integer <- next-input)
+;?   ($print (("back-to-index: target " literal))) ;? 3
+;?   ($print target-index:integer) ;? 3
+;?   ($print (("\n" literal))) ;? 3
+;?   ($print (("back-to-index: first subindex " literal))) ;? 1
+;?   ($print first-subindex-on-page:integer/space:1) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  ; scan up until top, or *before* target-index (to skip expanded children)
+  { begin
+;?     ($print cursor-row:integer/space:1) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    (at-top?:boolean <- equal cursor-row:integer/space:1 0:literal)
+    (break-if at-top?:boolean)
+    (index:integer <- cursor-row-to-trace-index 0:space-address/browser-state cursor-row:integer/space:1)
+;?     ($print cursor-row:integer/space:1) ;? 3
+;?     ($print ((" " literal))) ;? 3
+;?     ($print index:integer) ;? 3
+;?     ($print (("\n" literal))) ;? 3
+    (done?:boolean <- less-than index:integer target-index:integer)
+    (break-if done?:boolean)
+    (up 0:space-address screen:terminal-address)
+    (loop)
+  }
+  ; now if we're before target-index, down 1
+;?   ($print (("final down?\n" literal))) ;? 1
+  (index:integer <- cursor-row-to-trace-index 0:space-address/browser-state cursor-row:integer/space:1)
+;?   ($print (("done scanning; cursor at row " literal))) ;? 2
+;?   ($print cursor-row:integer/space:1) ;? 2
+;?   ($print ((", which is index " literal))) ;? 2
+;?   ($print index:integer) ;? 2
+;?   ($print (("\n" literal))) ;? 2
+  { begin
+    (at-target?:boolean <- greater-or-equal index:integer target-index:integer)
+    (break-if at-target?:boolean)
+;?     ($print (("down 1\n" literal))) ;? 2
+    ; above expanded
+    (down 0:space-address screen:terminal-address)
+  }
+])
+
+;; pagination helpers
+(function at-first-page [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)  ; read-only
+  (result:boolean <- lesser-or-equal first-index-on-page:integer/space:1 0:literal)
+  { begin
+    (break-if result:boolean)
+    (reply nil:literal)
+  }
+  (expanded?:boolean <- equal expanded-index:integer/space:1 0:literal)
+  { begin
+    (break-if expanded?:boolean)
+    (reply t:literal)
+  }
+  ; if first subindex is 0, the top-level line is on a previous page
+  (result:boolean <- less-than first-subindex-on-page:integer/space:1 0:literal)
+  (reply result:boolean)
+])
+
+(function at-final-page [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)  ; read-only
+  (len:integer <- length traces:instruction-trace-address-array-address/space:1/deref)
+  (final-index:integer <- subtract len:integer 1:literal)
+  (result:boolean <- greater-or-equal last-index-on-page:integer/space:1 final-index:integer)
+  { begin
+    (break-if result:boolean)
+    (reply nil:literal)
+  }
+  (last-trace-expanded?:boolean <- equal expanded-index:integer/space:1 len:integer)
+  { begin
+    (break-if last-trace-expanded?:boolean)
+    (reply t:literal)
+  }
+  (result:boolean <- greater-or-equal last-subindex-on-page:integer/space:1 expanded-children:integer/space:1)
+  (reply result:boolean)
+])
+
+(function next-page [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  { begin
+;?     ($print (("expanded: " literal))) ;? 3
+;?     ($print expanded-index:integer/space:1) ;? 3
+;?     ($print ((" last index: " literal))) ;? 3
+;?     ($print last-index-on-page:integer/space:1) ;? 3
+;?     ($print (("\n" literal))) ;? 3
+    (last-index-expanded?:boolean <- equal expanded-index:integer/space:1 last-index-on-page:integer/space:1)
+    (break-unless last-index-expanded?:boolean)
+    ; expanded
+;?     ($print (("last expanded\n" literal))) ;? 3
+    { begin
+      (expanded-index-done?:boolean <- equal expanded-children:integer/space:1 last-subindex-on-page:integer/space:1)
+      (break-if expanded-index-done?:boolean 2:blocks)
+;?       ($print (("children left\n" literal))) ;? 3
+      ; children left to open
+      (first-index-on-page:integer/space:1 <- copy last-index-on-page:integer/space:1)
+      (first-subindex-on-page:integer/space:1 <- add last-subindex-on-page:integer/space:1 1:literal)
+      (reply)
+    }
+  }
+  (first-index-on-page:integer/space:1 <- add last-index-on-page:integer/space:1 1:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+])
+
+(function previous-page [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+;?   ($print (("before: " literal))) ;? 2
+;?   ($print first-index-on-page:integer/space:1) ;? 2
+;?   ($print ((" " literal))) ;? 2
+;?   ($print first-subindex-on-page:integer/space:1) ;? 2
+;?   ($print (("\n" literal))) ;? 2
+  ; easy case: no expanded-index
+  (jump-unless expanded-index:integer/space:1)
+;?   ($print (("b\n" literal))) ;? 4
+  (x:boolean <- less-than expanded-index:integer/space:1 0:literal)
+  (jump-if x:boolean easy-case:offset)
+  ; easy case: expanded-index lies below top of current page
+;?   ($print (("c\n" literal))) ;? 4
+  (x:boolean <- greater-than expanded-index:integer/space:1 first-index-on-page:integer/space:1)
+  (jump-if x:boolean easy-case:offset)
+  ; easy case: expanded-index *starts* at top of current page
+;?   ($print (("d\n" literal))) ;? 5
+  (top-of-screen-inside-expanded:boolean <- equal expanded-index:integer/space:1 first-index-on-page:integer/space:1)
+  (y:boolean <- lesser-or-equal first-subindex-on-page:integer/space:1 -1:literal)
+  (y:boolean <- and top-of-screen-inside-expanded:boolean y:boolean)
+  (jump-if y:boolean easy-case:offset)
+  ; easy case: expanded-index too far up for previous page
+;?   ($print (("e\n" literal))) ;? 5
+  (delta-to-expanded:integer <- subtract first-index-on-page:integer/space:1 expanded-index:integer/space:1)
+;?   ($print delta-to-expanded:integer) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  (x:boolean <- greater-than delta-to-expanded:integer screen-height:integer/space:1)
+  (jump-if x:boolean easy-case:offset)
+;?   ($print (("f\n" literal))) ;? 5
+  ; tough case: expanded index overlaps current and/or previous page
+  (lines-remaining-to-decrement:integer <- copy screen-height:integer/space:1)
+  ; a) scroll to just below expanded-index if necessary
+  (below-expanded-index:integer <- add expanded-index:integer/space:1 1:literal)
+  { begin
+    (done?:boolean <- done-scrolling-up default-space:space-address)
+    (break-if done?:boolean)
+    (done?:boolean <- lesser-or-equal first-index-on-page:integer/space:1 below-expanded-index:integer)
+    (break-if done?:boolean)
+;?     ($print (("g\n" literal))) ;? 2
+    (first-index-on-page:integer/space:1 <- subtract first-index-on-page:integer/space:1 1:literal)
+    (lines-remaining-to-decrement:integer <- subtract lines-remaining-to-decrement:integer 1:literal)
+    (loop)
+  }
+  { begin
+;?     ($print (("h\n" literal))) ;? 2
+    (x:boolean <- equal first-index-on-page:integer/space:1 below-expanded-index:integer)
+    (break-unless x:boolean)
+    (first-index-on-page:integer/space:1 <- copy expanded-index:integer/space:1)
+    (first-subindex-on-page:integer/space:1 <- subtract expanded-children:integer/space:1 1:literal)
+    (lines-remaining-to-decrement:integer <- subtract lines-remaining-to-decrement:integer 1:literal)
+  }
+  ; b) scroll through expanded-children if necessary
+  { begin
+    (done?:boolean <- done-scrolling-up default-space:space-address)
+    (break-if done?:boolean)
+    (done?:boolean <- less-than first-subindex-on-page:integer/space:1 0:literal)
+    (break-if done?:boolean)
+;?     ($print (("i\n" literal))) ;? 2
+    (first-subindex-on-page:integer/space:1 <- subtract first-subindex-on-page:integer/space:1 1:literal)
+    (lines-remaining-to-decrement:integer <- subtract lines-remaining-to-decrement:integer 1:literal)
+    (loop)
+  }
+  ; c) jump past expanded-index parent if necessary
+;?   ($print (("j\n" literal))) ;? 2
+  { begin
+    (done?:boolean <- done-scrolling-up default-space:space-address)
+    (break-if done?:boolean)
+;?     ($print (("k\n" literal))) ;? 2
+    (first-index-on-page:integer/space:1 <- subtract first-index-on-page:integer/space:1 1:literal)
+    (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+    (lines-remaining-to-decrement:integer <- subtract lines-remaining-to-decrement:integer 1:literal)
+  }
+  ; d) scroll up before expanded-index if necessary
+;?   ($print (("l\n" literal))) ;? 2
+  { begin
+    (done?:boolean <- done-scrolling-up default-space:space-address)
+    (break-if done?:boolean)
+;?     ($print (("m\n" literal))) ;? 2
+    (first-index-on-page:integer/space:1 <- subtract first-index-on-page:integer/space:1 1:literal)
+    (lines-remaining-to-decrement:integer <- subtract lines-remaining-to-decrement:integer 1:literal)
+    (loop)
+  }
+  (reply)
+  easy-case
+  (first-index-on-page:integer/space:1 <- subtract first-index-on-page:integer/space:1 screen-height:integer/space:1)
+  (first-index-on-page:integer/space:1 <- max first-index-on-page:integer/space:1 0:literal)
+  (first-subindex-on-page:integer/space:1 <- copy -2:literal)
+])
+
+(function done-scrolling-up [
+  (default-space:space-address/names:previous-page <- next-input)
+  (0:space-address/names:browser-state <- copy 0:space-address)  ; just to wire up names for space/1
+  (at-top-of-screen?:boolean <- lesser-or-equal lines-remaining-to-decrement:integer 0:literal)
+  (jump-if at-top-of-screen?:boolean done:offset)
+  (at-first-index:boolean <- lesser-or-equal first-index-on-page:integer/space:1 0:literal)
+  (at-first-subindex:boolean <- lesser-or-equal first-subindex-on-page:integer/space:1 -1:literal)
+  (trace-done?:boolean <- and at-first-index:boolean at-first-subindex:boolean)
+  (jump-if trace-done?:boolean done:offset)
+  (reply nil:literal)
+  done
+  (reply t:literal)
+])
+
+;; modify screen state in response to a single key
+(function process-key [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  (0:space-address/names:browser-state <- next-input)
+  (k:keyboard-address <- next-input)
+  (screen:terminal-address <- next-input)
+  (c:character <- read-key k:keyboard-address silent:literal/terminal)
+  { begin
+    ; no key yet
+    (break-if c:character)
+    (reply nil:literal)
+  }
+;?   ($print (("key pressed: " literal))) ;? 1
+;?   ($write c:character) ;? 1
+;?   ($print (("\n" literal))) ;? 1
+  { begin
+    ; user quit
+    (q-pressed?:boolean <- equal c:character ((#\q literal)))
+    (end-of-fake-keyboard-input?:boolean <- equal c:character ((#\null literal)))
+    (quit?:boolean <- or q-pressed?:boolean end-of-fake-keyboard-input?:boolean)
+    (break-unless quit?:boolean)
+    (reply t:literal)
+  }
+  ; up/down navigation
+  { begin
+    (up?:boolean <- equal c:character ((up literal)))
+    (k?:boolean <- equal c:character ((#\k literal)))
+    (up?:boolean <- or up?:boolean k?:boolean)
+    (break-unless up?:boolean)
+    (up 0:space-address/browser-state screen:terminal-address)
+    (reply nil:literal)
+  }
+  { begin
+    (down?:boolean <- equal c:character ((down literal)))
+    (j?:boolean <- equal c:character ((#\j literal)))
+    (down?:boolean <- or down?:boolean j?:boolean)
+    (break-unless down?:boolean)
+    (down 0:space-address/browser-state screen:terminal-address)
+    (reply nil:literal)
+  }
+  ; page up/page down
+  { begin
+    ; if page-up pressed
+    (page-up?:boolean <- equal c:character ((pgup literal)))
+    (K?:boolean <- equal c:character ((#\K literal)))
+    (page-up?:boolean <- or page-up?:boolean K?:boolean)
+    (break-unless page-up?:boolean)
+    ; if we're not already at start of trace
+    (first-page?:boolean <- at-first-page 0:space-address/browser-state)
+    (break-if first-page?:boolean)
+    ; move cursor to top of screen
+    (to-top 0:space-address/browser-state screen:terminal-address)
+    ; switch browser state
+    (previous-page 0:space-address/browser-state)
+;?     ($dump-browser-state 0:space-address) ;? 3
+    ; redraw
+    (print-page 0:space-address/browser-state screen:terminal-address)
+    (reply nil:literal)
+  }
+  { begin
+    ; if page-down pressed
+    (page-down?:boolean <- equal c:character ((pgdn literal)))
+    (J?:boolean <- equal c:character ((#\J literal)))
+    (page-down?:boolean <- or page-down?:boolean J?:boolean)
+    (break-unless page-down?:boolean)
+    ; if we're not already at end of trace
+    (final-page?:boolean <- at-final-page 0:space-address/browser-state)
+    (break-if final-page?:boolean)
+    ; move cursor to top of screen
+    (to-top 0:space-address/browser-state screen:terminal-address)
+;?     ($print (("before: " literal))) ;? 1
+;?     ($print first-index-on-page:integer/space:1) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    ; switch browser state
+    (next-page 0:space-address/browser-state)
+;?     ($print (("after: " literal))) ;? 1
+;?     ($print first-index-on-page:integer/space:1) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    ; redraw
+    (print-page 0:space-address/browser-state screen:terminal-address)
+    ; move cursor back to top of screen
+    (to-top 0:space-address/browser-state screen:terminal-address)
+    (reply nil:literal)
+  }
+  ; enter: expand/collapse current row
+  { begin
+    (toggle?:boolean <- equal c:character ((#\newline literal)))
+    (break-unless toggle?:boolean)
+;?     ($print (("expand: first subindex " literal))) ;? 1
+;?     ($print first-subindex-on-page:integer/space:1) ;? 1
+;?     ($print (("\n" literal))) ;? 1
+    (original-cursor-row:integer <- copy cursor-row:integer/space:1)
+;?     ($print (("cursor starts at row " literal))) ;? 6
+;?     ($print original-cursor-row:integer) ;? 7
+;?     ($print (("\n" literal))) ;? 7
+    (original-trace-index:integer <- cursor-row-to-trace-index 0:space-address/browser-state original-cursor-row:integer)
+;?     ($print (("which maps to index " literal))) ;? 7
+;?     ($print original-trace-index:integer) ;? 9
+;?     ($print (("\n" literal))) ;? 9
+    ; is expanded-index already set?
+    { begin
+      (expanded?:boolean <- greater-or-equal expanded-index:integer/space:1 0:literal)
+      (break-unless expanded?:boolean)
+      (too-early?:boolean <- less-than expanded-index:integer/space:1 first-index-on-page:integer/space:1)
+      (break-if too-early?:boolean)
+      (too-late?:boolean <- greater-than expanded-index:integer/space:1 last-index-on-page:integer/space:1)
+      (break-if too-late?:boolean)
+      ; expanded-index is now on this page
+;?       ($print (("expanded index on this page\n" literal))) ;? 6
+      { begin
+        ; are we at the expanded row?
+        (at-expanded?:boolean <- equal original-trace-index:integer expanded-index:integer/space:1)
+        (break-unless at-expanded?:boolean)
+;?         ($print (("at expanded index\n" literal))) ;? 5
+        ; print remaining lines collapsed and return
+        (back-to-index 0:space-address/browser-state screen:terminal-address expanded-index:integer/space:1)
+        (expanded-index:integer/space:1 <- copy -1:literal)
+        (expanded-children:integer/space:1 <- copy -1:literal)
+        (print-traces-collapsed-from 0:space-address/browser-state screen:terminal-address original-trace-index:integer)
+        (clear-rest-of-page 0:space-address/browser-state screen:terminal-address)
+        (back-to 0:space-address/browser-state screen:terminal-address original-cursor-row:integer)
+        (reply nil:literal)
+      }
+      ; are we below the expanded row?
+      { begin
+        (below-expanded?:boolean <- greater-than original-trace-index:integer expanded-index:integer/space:1)
+        (break-unless below-expanded?:boolean)
+;?         ($print (("below expanded index\n" literal))) ;? 6
+        (back-to-index 0:space-address/browser-state screen:terminal-address expanded-index:integer/space:1)
+;?         ($print (("scanning up to row " literal))) ;? 3
+;?         ($print cursor-row:integer/space:1) ;? 3
+;?         ($print (("\n" literal))) ;? 3
+        ; print traces collapsed until just before original row
+        (print-traces-collapsed-from 0:space-address/browser-state screen:terminal-address expanded-index:integer/space:1 original-trace-index:integer/until)
+        ; fall through
+      }
+    }
+    ; expand original row and print traces below it
+;?     ($print (("done collapsing previously expanded index\n" literal))) ;? 6
+    (expanded-index:integer/space:1 <- copy original-trace-index:integer)
+    (last-index-on-page:integer/space:1 <- copy original-trace-index:integer)
+    (tr:instruction-trace-address <- index traces:instruction-trace-address-array-address/space:1/deref original-trace-index:integer)
+;?     ($print (("expanded\n" literal))) ;? 6
+    (print-instruction-trace screen:terminal-address tr:instruction-trace-address 0:space-address/browser-state)
+    (next-index:integer <- add original-trace-index:integer 1:literal)
+;?     ($print (("printing collapsed lines from " literal))) ;? 7
+;?     ($print next-index:integer) ;? 8
+;?     ($print (("\n" literal))) ;? 8
+    (print-traces-collapsed-from 0:space-address/browser-state screen:terminal-address next-index:integer)
+;?     ($print (("clearing rest of page\n" literal))) ;? 6
+    (clear-rest-of-page 0:space-address/browser-state screen:terminal-address)
+;?     ($print (("moving cursor back up\n" literal))) ;? 6
+    (back-to-index 0:space-address/browser-state screen:terminal-address original-trace-index:integer)
+;?     ($print (("returning\n" literal))) ;? 5
+    (reply nil:literal)
+  }
+  (reply nil:literal)
+])
+
+(function browse-trace [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ($print (("parsing trace... (might take a while, depending on how long the trace is)\n" literal)))
+  (x:string-address <- next-input)
+  (screen-height:integer <- next-input)
+;?   (print-string nil:literal/terminal x:string-address) ;? 1
+  (s:stream-address <- init-stream x:string-address)
+  (traces:instruction-trace-address-array-address <- parse-traces s:stream-address)
+  (0:space-address/names:browser-state <- browser-state traces:instruction-trace-address-array-address screen-height:integer)
+  (cursor-mode)
+  (print-traces-collapsed 0:space-address/browser-state nil:literal/terminal)
+  { begin
+    (quit?:boolean <- process-key 0:space-address/browser-state nil:literal/keyboard nil:literal/terminal)
+    (break-if quit?:boolean)
+    (loop)
+  }
+  ; move cursor to bottom before exiting
+  (to-bottom 0:space-address/browser-state nil:literal/terminal)
+  (retro-mode)
+])
+
+(function main [
+  (default-space:space-address <- new space:literal 30:literal/capacity)
+  ($print (("loading trace.. (takes ~10s)\n" literal)))
+  (x:string-address <- new
+"run: main 0: a b c
+mem: 0
+run: main 1: d e f
+mem: 1
+mem: 1
+mem: 1
+mem: 1
+mem: 1
+run: main 2: g hi
+run: main 3: j
+mem: 3
+run: main 4: k
+run: main 5: l
+run: main 6: m
+run: main 7: n
+run: main 8: o")
+  (browse-trace x:string-address 3:literal/screen-height)
+])
diff --git a/archive/1.vm.arc/vimrc.vim b/archive/1.vm.arc/vimrc.vim
new file mode 100644
index 00000000..d2a65146
--- /dev/null
+++ b/archive/1.vm.arc/vimrc.vim
@@ -0,0 +1,8 @@
+syntax sync minlines=999
+
+function! HighlightMuInArc()
+  set ft=mu
+  syntax keyword muHack begin | highlight link muHack CommentedCode
+  syntax match muHack "[()]" | highlight link muHack CommentedCode
+endfunction
+autocmd BufRead,BufNewFile *.mu call HighlightMuInArc()
diff --git a/archive/1.vm.arc/x.mu b/archive/1.vm.arc/x.mu
new file mode 100644
index 00000000..778298a8
--- /dev/null
+++ b/archive/1.vm.arc/x.mu
@@ -0,0 +1,6 @@
+(function main [
+  (x:integer <- copy 1:literal)
+  (y:integer <- copy 3:literal)
+  (z:integer <- add x:integer y:integer)
+  ($dump-memory)
+])
diff --git a/archive/2.vm/000organization.cc b/archive/2.vm/000organization.cc
new file mode 100644
index 00000000..9a1938ff
--- /dev/null
+++ b/archive/2.vm/000organization.cc
@@ -0,0 +1,136 @@
+//: 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/archive/2.vm/001help.cc b/archive/2.vm/001help.cc
new file mode 100644
index 00000000..78877561
--- /dev/null
+++ b/archive/2.vm/001help.cc
@@ -0,0 +1,264 @@
+//: 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"
+       << "  To print out the trace to stderr:\n"
+       << "    mu --dump file1.mu file2.mu ...\n"
+       << "  this is handy when you want to see sandboxed traces alongside the main one\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/archive/2.vm/002test.cc b/archive/2.vm/002test.cc
new file mode 100644
index 00000000..817b0d47
--- /dev/null
+++ b/archive/2.vm/002test.cc
@@ -0,0 +1,104 @@
+//: 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/archive/2.vm/003trace.cc b/archive/2.vm/003trace.cc
new file mode 100644
index 00000000..18f15347
--- /dev/null
+++ b/archive/2.vm/003trace.cc
@@ -0,0 +1,501 @@
+//: 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 *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 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 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 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 focusing on 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 all
+//: 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 tests
+//: 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)
+
+//:: == Core data structures
+
+:(before "End Globals")
+trace_stream* Trace_stream = NULL;
+
+:(before "End Types")
+struct trace_stream {
+  vector<trace_line> past_lines;
+  // End trace_stream Fields
+
+  trace_stream() {
+    // End trace_stream Constructor
+  }
+  ~trace_stream() {
+    // End trace_stream Destructor
+  }
+  // End trace_stream Methods
+};
+
+//:: == Adding to the trace
+
+//: Top-level method is trace() which can be used like an ostream. Usage:
+//:   trace(depth, label) << ... << end();
+//: Don't forget the 'end()' to actually append to the trace.
+:(before "End Includes")
+// No brackets around the expansion so that it prints nothing if Trace_stream
+// isn't initialized.
+#define trace(...)  !Trace_stream ? cerr : Trace_stream->stream(__VA_ARGS__)
+
+:(before "End trace_stream Fields")
+// accumulator for current trace_line
+ostringstream* curr_stream;
+string curr_label;
+int curr_depth;
+// other stuff
+int collect_depth;  // avoid tracing lower levels for speed
+ofstream null_stream;  // never opened, so writes to it silently fail
+
+//: Some constants.
+:(before "struct trace_stream")  // include constants in all cleaved compilation units
+const int Max_depth = 9999;
+:(before "End trace_stream Constructor")
+curr_stream = NULL;
+curr_depth = Max_depth;
+collect_depth = Max_depth;
+
+:(before "struct trace_stream")
+struct trace_line {
+  string contents;
+  string label;
+  int depth;  // 0 is 'sea level'; positive integers are progressively 'deeper' and lower level
+  trace_line(string c, string l) {
+    contents = c;
+    label = l;
+    depth = 0;
+  }
+  trace_line(string c, string l, int d) {
+    contents = c;
+    label = l;
+    depth = d;
+  }
+};
+
+//: Starting a new trace line.
+:(before "End trace_stream Methods")
+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;
+  return *curr_stream;
+}
+
+//: End of a trace line; append it to the trace.
+:(before "End Types")
+struct end {};
+:(code)
+ostream& operator<<(ostream& os, end /*unused*/) {
+  if (Trace_stream) Trace_stream->newline();
+  return os;
+}
+
+:(before "End trace_stream Methods")
+void newline();
+:(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_contents, trim(curr_label), curr_depth));  // preserve indent in contents
+    // maybe incrementally dump trace
+    trace_line& t = past_lines.back();
+    if (should_incrementally_print_trace()) {
+      cerr       << std::setw(4) << t.depth << ' ' << t.label << ": " << t.contents << '\n';
+    }
+    // End trace Commit
+  }
+
+  // clean up
+  delete curr_stream;
+  curr_stream = NULL;
+  curr_label.clear();
+  curr_depth = Max_depth;
+}
+
+//:: == Initializing the trace in tests
+
+:(before "End Includes")
+#define START_TRACING_UNTIL_END_OF_SCOPE  lease_tracer leased_tracer;
+:(before "End Test Setup")
+START_TRACING_UNTIL_END_OF_SCOPE
+
+//: 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() {
+  delete Trace_stream;
+  Trace_stream = NULL;
+}
+
+//:: == Errors using traces
+
+:(before "End Includes")
+#define raise  (!Trace_stream ? (scroll_to_bottom_and_close_console(),++Trace_errors,cerr) /*do print*/ : Trace_stream->stream(Error_depth, "error"))
+
+//: Print errors to the screen by default.
+:(before "struct trace_stream")  // include constants in all cleaved compilation units
+const int Error_depth = 0;
+:(before "End Globals")
+int Hide_errors = false;  // if set, don't print errors to screen
+:(before "End Reset")
+Hide_errors = false;
+:(code)
+bool trace_stream::should_incrementally_print_trace() {
+  if (!Hide_errors && curr_depth == Error_depth) return true;
+  // End Incremental Trace Print Conditions
+  return false;
+}
+:(before "End trace_stream Methods")
+bool should_incrementally_print_trace();
+
+:(before "End Globals")
+int Trace_errors = 0;  // used only when Trace_stream is NULL
+
+// 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()) {
+  Passed = false;
+}
+:(code)
+bool trace_contains_errors() {
+  return Trace_errors > 0 || trace_count("error") > 0;
+}
+
+:(before "End Includes")
+// 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
+#define raise_for_now raise
+
+//: Automatically close the console in some situations.
+:(before "End One-time Setup")
+atexit(scroll_to_bottom_and_close_console);
+:(code)
+void scroll_to_bottom_and_close_console() {
+  if (!tb_is_active()) return;
+  // leave the screen in a relatively clean state
+  tb_set_cursor(tb_width()-1, tb_height()-1);
+  cout << "\r\n";
+  tb_shutdown();
+}
+:(before "End Includes")
+#include "termbox/termbox.h"
+
+//:: == Other assertions on traces
+//: Primitives:
+//:   - CHECK_TRACE_CONTENTS(lines)
+//:     Assert that the trace contains the given lines (separated by newlines)
+//:     in order. There can be other intervening lines between them.
+//:   - CHECK_TRACE_DOESNT_CONTAIN(line)
+//:   - CHECK_TRACE_DOESNT_CONTAIN(label, contents)
+//:     Assert that the trace doesn't contain the given (single) line.
+//:   - CHECK_TRACE_COUNT(label, count)
+//:     Assert that the trace contains exactly 'count' lines with the given
+//:     'label'.
+//:   - CHECK_TRACE_CONTAINS_ERRORS()
+//:   - CHECK_TRACE_DOESNT_CONTAIN_ERRORS()
+//:   - trace_count_prefix(label, prefix)
+//:     Count the number of trace lines with the given 'label' that start with
+//:     the given 'prefix'.
+
+:(before "End Includes")
+#define CHECK_TRACE_CONTENTS(...)  check_trace_contents(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__)
+
+#define CHECK_TRACE_DOESNT_CONTAIN(...)  CHECK(trace_doesnt_contain(__VA_ARGS__))
+
+#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_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; \
+  }
+
+// 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, "\n");
+  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;
+}
+
+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_count(tmp.at(0), tmp.at(1)) == 0;
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+//:: == Helpers for debugging using traces
+
+:(before "End Includes")
+// 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'.
+// `git log` should never show any calls to 'dbg'.
+#define dbg trace(0, "a")
+
+//: Dump the entire trace to file where it can be browsed offline.
+//: Dump the trace as it happens; that way you get something even if the
+//: program crashes.
+
+:(before "End Globals")
+ofstream Trace_file;
+:(before "End Commandline Options(*arg)")
+else if (is_equal(*arg, "--trace")) {
+  Trace_stream = new trace_stream;
+  cerr << "saving trace to 'last_run'\n";
+  Trace_file.open("last_run");
+  // Add a dummy line up top; otherwise the `browse_trace` tool currently has
+  // no way to expand any lines above an error.
+  Trace_file << "   0 dummy: start\n";
+}
+:(before "End trace Commit")
+if (Trace_file) {
+  Trace_file << std::setw(4) << t.depth << ' ' << t.label << ": " << t.contents << '\n';
+}
+:(before "End One-time Setup")
+atexit(cleanup_main);
+:(code)
+void cleanup_main() {
+  if (Trace_file) Trace_file.close();
+  // End cleanup_main
+}
+
+:(before "End trace_stream Methods")
+string readable_contents(string label) {
+  string trim(const string& s);  // prototype
+  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();
+}
+
+//: Print traces to the screen as they happen.
+//: Particularly useful when juggling multiple trace streams, like when
+//: debugging sandboxes.
+:(before "End Globals")
+bool Dump_trace = false;
+:(before "End Commandline Options(*arg)")
+else if (is_equal(*arg, "--dump")) {
+  Dump_trace = true;
+}
+:(before "End Incremental Trace Print Conditions")
+if (Dump_trace) return true;
+
+//: Miscellaneous helpers.
+
+:(code)
+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/archive/2.vm/003trace.test.cc b/archive/2.vm/003trace.test.cc
new file mode 100644
index 00000000..30f61b5a
--- /dev/null
+++ b/archive/2.vm/003trace.test.cc
@@ -0,0 +1,126 @@
+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: foo\n"
+                       "test layer 2: bar\n"
+                       "test layer 1: qux\n");
+}
+
+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/archive/2.vm/010vm.cc b/archive/2.vm/010vm.cc
new file mode 100644
index 00000000..42b867fc
--- /dev/null
+++ b/archive/2.vm/010vm.cc
@@ -0,0 +1,900 @@
+//: A Mu program is a book of 'recipes' (functions)
+:(before "End Globals")
+//: Each recipe is stored at a specific page number, or ordinal.
+map<recipe_ordinal, recipe> Recipe;
+//: You can also refer to each recipe by its name.
+map<string, recipe_ordinal> Recipe_ordinal;
+recipe_ordinal Next_recipe_ordinal = 1;
+
+//: Ordinals are like numbers, except you can't do arithmetic on them. Ordinal
+//: 1 is not less than 2, it's just different. Phone numbers are ordinals;
+//: adding two phone numbers is meaningless. Here each recipe does something
+//: incommensurable with any other recipe.
+:(after "Types")
+typedef int recipe_ordinal;
+
+:(before "End Types")
+// Recipes are lists of instructions. To perform or 'run' a recipe, the
+// computer runs its instructions.
+struct recipe {
+  recipe_ordinal ordinal;
+  string name;
+  vector<instruction> steps;
+  // End recipe Fields
+  recipe();
+};
+
+:(before "struct recipe")
+// Each instruction is either of the form:
+//   product1, product2, product3, ... <- operation ingredient1, ingredient2, ingredient3, ...
+// or just a single 'label' starting with a non-alphanumeric character
+//   +label
+// Labels don't do anything, they're just named locations in a recipe.
+struct instruction {
+  bool is_label;
+  string label;  // only if is_label
+  string name;  // only if !is_label
+  string original_string;  // for error messages
+  recipe_ordinal operation;  // get(Recipe_ordinal, name)
+  vector<reagent> ingredients;  // only if !is_label
+  vector<reagent> products;  // only if !is_label
+  // End instruction Fields
+  instruction();
+  void clear();
+  bool is_empty();
+};
+
+:(before "struct instruction")
+// Ingredients and products are a single species -- a reagent. Reagents refer
+// either to numbers or to locations in memory along with 'type' tags telling
+// us how to interpret them. They also can contain arbitrary other lists of
+// properties besides types, but we're getting ahead of ourselves.
+struct reagent {
+  string original_string;
+  string name;
+  type_tree* type;
+  vector<pair<string, string_tree*> > properties;  // can't be a map because the string_tree sometimes needs to be NULL, which can be confusing
+  double value;
+  bool initialized;
+  // End reagent Fields
+  reagent(const string& s);
+  reagent() :type(NULL), value(0), initialized(false) {}
+  reagent(type_tree* t) :type(t), value(0), initialized(false) {}
+  ~reagent();
+  void clear();
+  reagent(const reagent& original);
+  reagent& operator=(const reagent& original);
+  void set_value(double v) { value = v;  initialized = true; }
+};
+
+:(before "struct reagent")
+// Types can range from a simple type ordinal, to arbitrarily complex trees of
+// type parameters, like (map (address array character) (list number))
+struct type_tree {
+  bool atom;
+  string name;  // only if atom
+  type_ordinal value;  // only if atom
+  type_tree* left;  // only if !atom
+  type_tree* right;  // only if !atom
+  ~type_tree();
+  type_tree(const type_tree& original);
+  // atomic type ordinal
+  explicit type_tree(string name);
+  type_tree(string name, type_ordinal v) :atom(true), name(name), value(v), left(NULL), right(NULL) {}
+  // tree of type ordinals
+  type_tree(type_tree* l, type_tree* r) :atom(false), value(0), left(l), right(r) {}
+  type_tree& operator=(const type_tree& original);
+  bool operator==(const type_tree& other) const;
+  bool operator!=(const type_tree& other) const { return !operator==(other); }
+  bool operator<(const type_tree& other) const;
+  bool operator>(const type_tree& other) const { return other.operator<(*this); }
+};
+
+struct string_tree {
+  bool atom;
+  string value;  // only if atom
+  string_tree* left;  // only if !atom
+  string_tree* right;  // only if !atom
+  ~string_tree();
+  string_tree(const string_tree& original);
+  // atomic string
+  explicit string_tree(string v) :atom(true), value(v), left(NULL), right(NULL) {}
+  // tree of strings
+  string_tree(string_tree* l, string_tree* r) :atom(false), left(l), right(r) {}
+};
+
+// End type_tree Definition
+:(code)
+type_tree::type_tree(string name) :atom(true), name(name), value(get(Type_ordinal, name)), left(NULL), right(NULL) {}
+
+:(before "End Globals")
+// Locations refer to a common 'memory'. Each location can store a number.
+map<int, double> Memory;
+:(before "End Reset")
+Memory.clear();
+
+:(after "Types")
+// Mu types encode how the numbers stored in different parts of memory are
+// interpreted. A location tagged as a 'character' type will interpret the
+// value 97 as the letter 'a', while a different location of type 'number'
+// would not.
+//
+// Unlike most computers today, Mu stores types in a single big table, shared
+// by all the Mu programs on the computer. This is useful in providing a
+// seamless experience to help understand arbitrary Mu programs.
+typedef int type_ordinal;
+:(before "End Globals")
+map<string, type_ordinal> Type_ordinal;
+map<type_ordinal, type_info> Type;
+type_ordinal Next_type_ordinal = 1;
+type_ordinal Number_type_ordinal = 0;
+type_ordinal Boolean_type_ordinal = 0;
+type_ordinal Character_type_ordinal = 0;
+type_ordinal Address_type_ordinal = 0;
+type_ordinal Array_type_ordinal = 0;
+:(code)
+void setup_types() {
+  Type.clear();  Type_ordinal.clear();
+  put(Type_ordinal, "literal", 0);
+  Next_type_ordinal = 1;
+  // Mu Types Initialization
+  Number_type_ordinal = put(Type_ordinal, "number", Next_type_ordinal++);
+  get_or_insert(Type, Number_type_ordinal).name = "number";
+  put(Type_ordinal, "location", Number_type_ordinal);  // synonym of number for addresses we'll never look up
+  Address_type_ordinal = put(Type_ordinal, "address", Next_type_ordinal++);
+  get_or_insert(Type, Address_type_ordinal).name = "address";
+  Boolean_type_ordinal = put(Type_ordinal, "boolean", Next_type_ordinal++);
+  get_or_insert(Type, Boolean_type_ordinal).name = "boolean";
+  Character_type_ordinal = put(Type_ordinal, "character", Next_type_ordinal++);
+  get_or_insert(Type, Character_type_ordinal).name = "character";
+  // Array types are a special modifier to any other type. For example,
+  // array:number or array:address:boolean.
+  Array_type_ordinal = put(Type_ordinal, "array", Next_type_ordinal++);
+  get_or_insert(Type, Array_type_ordinal).name = "array";
+  // End Mu Types Initialization
+}
+void teardown_types() {
+  for (map<type_ordinal, type_info>::iterator p = Type.begin();  p != Type.end();  ++p) {
+    for (int i = 0;  i < SIZE(p->second.elements);  ++i)
+      p->second.elements.clear();
+  }
+  Type_ordinal.clear();
+}
+:(before "End One-time Setup")
+setup_types();
+atexit(teardown_types);
+
+:(before "End Types")
+// You can construct arbitrary new types. New types are either 'containers'
+// with multiple 'elements' of other types, or 'exclusive containers' containing
+// one of multiple 'variants'. (These are similar to C structs and unions,
+// respectively, though exclusive containers implicitly include a tag element
+// recording which variant they should be interpreted as.)
+//
+// For example, storing bank balance and name for an account might require a
+// container, but if bank accounts may be either for individuals or groups,
+// with different properties for each, that may require an exclusive container
+// whose variants are individual-account and joint-account containers.
+enum kind_of_type {
+  PRIMITIVE,
+  CONTAINER,
+  EXCLUSIVE_CONTAINER
+};
+
+struct type_info {
+  string name;
+  kind_of_type kind;
+  vector<reagent> elements;
+  // End type_info Fields
+  type_info() :kind(PRIMITIVE) {
+    // End type_info Constructor
+  }
+};
+
+enum primitive_recipes {
+  IDLE = 0,
+  COPY,
+  // End Primitive Recipe Declarations
+  MAX_PRIMITIVE_RECIPES,
+};
+:(code)
+//: It's all very well to construct recipes out of other recipes, but we need
+//: to know how to do *something* out of the box. For the following
+//: recipes there are only codes, no entries in the book, because Mu just knows
+//: what to do for them.
+void setup_recipes() {
+  Recipe.clear();  Recipe_ordinal.clear();
+  put(Recipe_ordinal, "idle", IDLE);
+  // Primitive Recipe Numbers
+  put(Recipe_ordinal, "copy", COPY);
+  // End Primitive Recipe Numbers
+}
+//: We could just reset the recipe table after every test, but that gets slow
+//: all too quickly. Instead, initialize the common stuff just once at
+//: startup. Later layers will carefully undo each test's additions after
+//: itself.
+:(before "End One-time Setup")
+setup_recipes();
+assert(MAX_PRIMITIVE_RECIPES < 200);  // level 0 is primitives; until 199
+Next_recipe_ordinal = 200;
+put(Recipe_ordinal, "main", Next_recipe_ordinal++);
+// Load Mu Prelude
+// End Mu Prelude
+:(before "End Commandline Parsing")
+assert(Next_recipe_ordinal < 1000);  // recipes being tested didn't overflow into test space
+:(before "End Reset")
+Next_recipe_ordinal = 1000;  // consistent new numbers for each test
+
+//: One final detail: tests can modify our global tables of recipes and types,
+//: so we need some way to clean up after each test is done so it doesn't
+//: influence later ones.
+:(before "End Globals")
+map<string, recipe_ordinal> Recipe_ordinal_snapshot;
+map<recipe_ordinal, recipe> Recipe_snapshot;
+map<string, type_ordinal> Type_ordinal_snapshot;
+map<type_ordinal, type_info> Type_snapshot;
+:(before "End One-time Setup")
+save_snapshots();
+:(before "End Reset")
+restore_snapshots();
+
+:(code)
+void save_snapshots() {
+  Recipe_ordinal_snapshot = Recipe_ordinal;
+  Recipe_snapshot = Recipe;
+  Type_ordinal_snapshot = Type_ordinal;
+  Type_snapshot = Type;
+  // End save_snapshots
+}
+
+void restore_snapshots() {
+  Recipe = Recipe_snapshot;
+  Recipe_ordinal = Recipe_ordinal_snapshot;
+  restore_non_recipe_snapshots();
+}
+// when running sandboxes in the edit/ app we'll want to restore everything except recipes defined in the app
+void restore_non_recipe_snapshots() {
+  Type_ordinal = Type_ordinal_snapshot;
+  Type = Type_snapshot;
+  // End restore_snapshots
+}
+
+//:: Helpers
+
+:(code)
+recipe::recipe() {
+  ordinal = -1;
+  // End recipe Constructor
+}
+
+instruction::instruction() :is_label(false), operation(IDLE) {
+  // End instruction Constructor
+}
+void instruction::clear() {
+  is_label=false;
+  label.clear();
+  name.clear();
+  operation=IDLE;
+  ingredients.clear();
+  products.clear();
+  original_string.clear();
+  // End instruction Clear
+}
+bool instruction::is_empty() { return !is_label && name.empty(); }
+
+// Reagents have the form <name>:<type>:<type>:.../<property>/<property>/...
+reagent::reagent(const string& s) :original_string(s), type(NULL), value(0), initialized(false) {
+  // Parsing reagent(string s)
+  istringstream in(s);
+  in >> std::noskipws;
+  // name and type
+  istringstream first_row(slurp_until(in, '/'));
+  first_row >> std::noskipws;
+  name = slurp_until(first_row, ':');
+  string_tree* type_names = parse_property_list(first_row);
+  // End Parsing Reagent Type Property(type_names)
+  type = new_type_tree(type_names);
+  delete type_names;
+  // special cases
+  if (is_integer(name) && type == NULL)
+    type = new type_tree("literal");
+  if (name == "_" && type == NULL)
+    type = new type_tree("literal");
+  // other properties
+  slurp_properties(in, properties);
+  // End Parsing reagent
+}
+
+void slurp_properties(istream& in, vector<pair<string, string_tree*> >& out) {
+  while (has_data(in)) {
+    istringstream row(slurp_until(in, '/'));
+    row >> std::noskipws;
+    string key = slurp_until(row, ':');
+    string_tree* value = parse_property_list(row);
+    out.push_back(pair<string, string_tree*>(key, value));
+  }
+}
+
+string_tree* parse_property_list(istream& in) {
+  skip_whitespace_but_not_newline(in);
+  if (!has_data(in)) return NULL;
+  string_tree* first = new string_tree(slurp_until(in, ':'));
+  if (!has_data(in)) return first;
+  string_tree* rest = parse_property_list(in);
+  if (!has_data(in) && rest->atom)
+    return new string_tree(first, new string_tree(rest, NULL));
+  return new string_tree(first, rest);
+}
+:(before "End Unit Tests")
+void test_parse_property_list_atom() {
+  istringstream in("a");
+  string_tree* x = parse_property_list(in);
+  CHECK(x->atom);
+  delete x;
+}
+void test_parse_property_list_list() {
+  istringstream in("a:b");
+  string_tree* x = parse_property_list(in);
+  CHECK(!x->atom);
+  CHECK(x->left->atom);
+  CHECK_EQ(x->left->value, "a");
+  CHECK(!x->right->atom);
+  CHECK(x->right->left->atom);
+  CHECK_EQ(x->right->left->value, "b");
+  CHECK(x->right->right == NULL);
+  delete x;
+}
+
+:(code)
+type_tree* new_type_tree(const string_tree* properties) {
+  if (!properties) return NULL;
+  if (properties->atom) {
+    const string& type_name = properties->value;
+    int value = 0;
+    if (contains_key(Type_ordinal, type_name))
+      value = get(Type_ordinal, type_name);
+    else if (is_integer(type_name))  // sometimes types will contain literal integers, like for the size of an array
+      value = 0;
+    else if (properties->value == "->")  // used in recipe types
+      value = 0;
+    else
+      value = -1;  // should never happen; will trigger errors later
+    return new type_tree(type_name, value);
+  }
+  return new type_tree(new_type_tree(properties->left),
+                       new_type_tree(properties->right));
+}
+
+//: avoid memory leaks for the type tree
+
+reagent::reagent(const reagent& other) {
+  original_string = other.original_string;
+  name = other.name;
+  value = other.value;
+  initialized = other.initialized;
+  for (int i = 0;  i < SIZE(other.properties);  ++i) {
+    properties.push_back(pair<string, string_tree*>(other.properties.at(i).first, copy(other.properties.at(i).second)));
+  }
+  type = copy(other.type);
+  // End reagent Copy Constructor
+}
+
+type_tree::type_tree(const type_tree& original) {
+  atom = original.atom;
+  name = original.name;
+  value = original.value;
+  left = copy(original.left);
+  right = copy(original.right);
+}
+
+type_tree& type_tree::operator=(const type_tree& original) {
+  atom = original.atom;
+  name = original.name;
+  value = original.value;
+  if (left) delete left;
+  left = copy(original.left);
+  if (right) delete right;
+  right = copy(original.right);
+  return *this;
+}
+
+bool type_tree::operator==(const type_tree& other) const {
+  if (atom != other.atom) return false;
+  if (atom)
+    return name == other.name && value == other.value;
+  return (left == other.left || *left == *other.left)
+      && (right == other.right || *right == *other.right);
+}
+
+// only constraint we care about: if a < b then !(b < a)
+bool type_tree::operator<(const type_tree& other) const {
+  if (atom != other.atom) return atom > other.atom;  // atoms before non-atoms
+  if (atom) return value < other.value;
+  // first location in one that's missing in the other makes that side 'smaller'
+  if (left && !other.left) return false;
+  if (!left && other.left) return true;
+  if (right && !other.right) return false;
+  if (!right && other.right) return true;
+  // now if either pointer is unequal neither side can be null
+  // if one side is equal that's easy
+  if (left == other.left || *left == *other.left) return right && *right < *other.right;
+  if (right == other.right || *right == *other.right) return left && *left < *other.left;
+  // if the two sides criss-cross, pick the side with the smaller lhs
+  if ((left == other.right || *left == *other.right)
+      && (right == other.left || *right == *other.left))
+    return *left < *other.left;
+  // now the hard case: both sides are not equal
+  // make sure we stay consistent between (a < b) and (b < a)
+  // just return the side with the smallest of the 4 branches
+  if (*left < *other.left && *left < *other.right) return true;
+  if (*right < *other.left && *right < *other.right) return true;
+  return false;
+}
+:(before "End Unit Tests")
+// These unit tests don't always use valid types.
+void test_compare_atom_types() {
+  reagent a("a:address"), b("b:boolean");
+  CHECK(*a.type < *b.type);
+  CHECK(!(*b.type < *a.type));
+}
+void test_compare_equal_atom_types() {
+  reagent a("a:address"), b("b:address");
+  CHECK(!(*a.type < *b.type));
+  CHECK(!(*b.type < *a.type));
+}
+void test_compare_atom_with_non_atom() {
+  reagent a("a:address:number"), b("b:boolean");
+  CHECK(!(*a.type < *b.type));
+  CHECK(*b.type < *a.type);
+}
+void test_compare_lists_with_identical_structure() {
+  reagent a("a:address:address"), b("b:address:boolean");
+  CHECK(*a.type < *b.type);
+  CHECK(!(*b.type < *a.type));
+}
+void test_compare_identical_lists() {
+  reagent a("a:address:boolean"), b("b:address:boolean");
+  CHECK(!(*a.type < *b.type));
+  CHECK(!(*b.type < *a.type));
+}
+void test_compare_list_with_extra_element() {
+  reagent a("a:address:address"), b("b:address:address:number");
+  CHECK(*a.type < *b.type);
+  CHECK(!(*b.type < *a.type));
+}
+void test_compare_list_with_smaller_left_but_larger_right() {
+  reagent a("a:number:character"), b("b:boolean:array");
+  CHECK(*a.type < *b.type);
+  CHECK(!(*b.type < *a.type));
+}
+void test_compare_list_with_smaller_left_but_larger_right_identical_types() {
+  reagent a("a:address:boolean"), b("b:boolean:address");
+  CHECK(*a.type < *b.type);
+  CHECK(!(*b.type < *a.type));
+}
+
+:(code)
+string_tree::string_tree(const string_tree& original) {
+  atom = original.atom;
+  value = original.value;
+  left = copy(original.left);
+  right = copy(original.right);
+}
+
+reagent& reagent::operator=(const reagent& other) {
+  original_string = other.original_string;
+  for (int i = 0;  i < SIZE(properties);  ++i)
+    if (properties.at(i).second) delete properties.at(i).second;
+  properties.clear();
+  for (int i = 0;  i < SIZE(other.properties);  ++i)
+    properties.push_back(pair<string, string_tree*>(other.properties.at(i).first, copy(other.properties.at(i).second)));
+  name = other.name;
+  value = other.value;
+  initialized = other.initialized;
+  if (type) delete type;
+  type = copy(other.type);
+  // End reagent Copy Operator
+  return *this;
+}
+
+reagent::~reagent() {
+  clear();
+}
+
+void reagent::clear() {
+  for (int i = 0;  i < SIZE(properties);  ++i) {
+    if (properties.at(i).second) {
+      delete properties.at(i).second;
+      properties.at(i).second = NULL;
+    }
+  }
+  delete type;
+  type = NULL;
+}
+type_tree::~type_tree() {
+  delete left;
+  delete right;
+}
+string_tree::~string_tree() {
+  delete left;
+  delete right;
+}
+
+void append(type_tree*& base, type_tree* extra) {
+  if (!base) {
+    base = extra;
+    return;
+  }
+  type_tree* curr = base;
+  while (curr->right) curr = curr->right;
+  curr->right = extra;
+}
+
+void append(string_tree*& base, string_tree* extra) {
+  if (!base) {
+    base = extra;
+    return;
+  }
+  string_tree* curr = base;
+  while (curr->right) curr = curr->right;
+  curr->right = extra;
+}
+
+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 has_property(const reagent& x, const string& name) {
+  for (int i = 0;  i < SIZE(x.properties);  ++i) {
+    if (x.properties.at(i).first == name) return true;
+  }
+  return false;
+}
+
+string_tree* property(const reagent& r, const string& name) {
+  for (int p = 0;  p != SIZE(r.properties);  ++p) {
+    if (r.properties.at(p).first == name)
+      return r.properties.at(p).second;
+  }
+  return NULL;
+}
+
+string_tree* copy(const string_tree* x) {
+  if (x == NULL) return NULL;
+  return new string_tree(*x);
+}
+
+type_tree* copy(const type_tree* x) {
+  if (x == NULL) return NULL;
+  return new type_tree(*x);
+}
+
+:(before "End Globals")
+extern const string Ignore(",");  // commas are ignored in Mu except within [] strings
+:(code)
+void skip_whitespace_but_not_newline(istream& in) {
+  while (true) {
+    if (!has_data(in)) break;
+    else if (in.peek() == '\n') break;
+    else if (isspace(in.peek())) in.get();
+    else if (Ignore.find(in.peek()) != string::npos) in.get();
+    else break;
+  }
+}
+
+void dump_memory() {
+  for (map<int, double>::iterator p = Memory.begin();  p != Memory.end();  ++p) {
+    cerr << p->first << ": " << no_scientific(p->second) << '\n';
+  }
+}
+
+//:: Helpers for converting various values to string
+//: Use to_string() in trace(), and try to keep it stable from run to run.
+//: Use debug_string() while debugging, and throw everything into it.
+//: Use inspect() only for emitting a canonical format that can be parsed back
+//: into the value.
+
+string to_string(const recipe& r) {
+  ostringstream out;
+  out << "recipe " << r.name << " [\n";
+  for (int i = 0;  i < SIZE(r.steps);  ++i)
+    out << "  " << to_string(r.steps.at(i)) << '\n';
+  out << "]\n";
+  return out.str();
+}
+
+string to_original_string(const recipe& r) {
+  ostringstream out;
+  out << "recipe " << r.name << " [\n";
+  for (int i = 0;  i < SIZE(r.steps);  ++i)
+    out << "  " << to_original_string(r.steps.at(i)) << '\n';
+  out << "]\n";
+  return out.str();
+}
+
+string debug_string(const recipe& x) {
+  ostringstream out;
+  out << "- recipe " << x.name << '\n';
+  // Begin debug_string(recipe x)
+  for (int index = 0;  index < SIZE(x.steps);  ++index) {
+    const instruction& inst = x.steps.at(index);
+    out << "inst: " << to_string(inst) << '\n';
+    out << "  ingredients\n";
+    for (int i = 0;  i < SIZE(inst.ingredients);  ++i)
+      out << "    " << debug_string(inst.ingredients.at(i)) << '\n';
+    out << "  products\n";
+    for (int i = 0;  i < SIZE(inst.products);  ++i)
+      out << "    " << debug_string(inst.products.at(i)) << '\n';
+  }
+  return out.str();
+}
+
+string to_original_string(const instruction& inst) {
+  if (inst.is_label) return inst.label;
+  if (!inst.original_string.empty()) return inst.original_string;
+  ostringstream out;
+  for (int i = 0;  i < SIZE(inst.products);  ++i) {
+    if (i > 0) out << ", ";
+    out << inst.products.at(i).original_string;
+  }
+  if (!inst.products.empty()) out << " <- ";
+  out << inst.name;
+  if (!inst.ingredients.empty()) out << ' ';
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (i > 0) out << ", ";
+    out << inst.ingredients.at(i).original_string;
+  }
+  return out.str();
+}
+
+string to_string(const instruction& inst) {
+  if (inst.is_label) return inst.label;
+  ostringstream out;
+  for (int i = 0;  i < SIZE(inst.products);  ++i) {
+    if (i > 0) out << ", ";
+    out << to_string(inst.products.at(i));
+  }
+  if (!inst.products.empty()) out << " <- ";
+  out << inst.name << ' ';
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (i > 0) out << ", ";
+    out << to_string(inst.ingredients.at(i));
+  }
+  return out.str();
+}
+
+string to_string(const reagent& r) {
+  if (is_dummy(r)) return "_";
+  ostringstream out;
+  out << "{";
+  out << r.name << ": " << names_to_string(r.type);
+  if (!r.properties.empty()) {
+    for (int i = 0;  i < SIZE(r.properties);  ++i)
+      out << ", \"" << r.properties.at(i).first << "\": " << to_string(r.properties.at(i).second);
+  }
+  out << "}";
+  return out.str();
+}
+
+// special name for ignoring some products
+bool is_dummy(const reagent& x) {
+  return x.name == "_";
+}
+
+string debug_string(const reagent& x) {
+  ostringstream out;
+  out << x.name << ": " << x.value << ' ' << to_string(x.type) << " -- " << to_string(x);
+  return out.str();
+}
+
+string to_string(const string_tree* property) {
+  if (!property) return "()";
+  ostringstream out;
+  dump(property, out);
+  return out.str();
+}
+
+void dump(const string_tree* x, ostream& out) {
+  if (!x) return;
+  if (x->atom) {
+    out << '"' << x->value << '"';
+    return;
+  }
+  out << '(';
+  const string_tree* curr = x;
+  while (curr && !curr->atom) {
+    dump(curr->left, out);
+    if (curr->right) out << ' ';
+    curr = curr->right;
+  }
+  // check for dotted list; should never happen
+  if (curr) {
+    out << ". ";
+    dump(curr, out);
+  }
+  out << ')';
+}
+
+string to_string(const type_tree* type) {
+  if (type == NULL) return "()";
+  ostringstream out;
+  dump(type, out);
+  return out.str();
+}
+
+void dump(const type_tree* x, ostream& out) {
+  if (!x) return;
+  if (x->atom) {
+    dump(x->value, out);
+    return;
+  }
+  out << '(';
+  const type_tree* curr = x;
+  while (curr && !curr->atom) {
+    dump(curr->left, out);
+    if (curr->right) out << ' ';
+    curr = curr->right;
+  }
+  // check for dotted list; should never happen
+  if (curr) {
+    out << ". ";
+    dump(curr, out);
+  }
+  out << ')';
+}
+
+void dump(type_ordinal type, ostream& out) {
+  if (contains_key(Type, type))
+    out << get(Type, type).name;
+  else
+    out << "?" << type;
+}
+
+string names_to_string(const type_tree* type) {
+  if (type == NULL) return "()";  // should never happen
+  ostringstream out;
+  dump_names(type, out);
+  return out.str();
+}
+
+void dump_names(const type_tree* x, ostream& out) {
+  if (!x) return;
+  if (x->atom) {
+    out << '"' << x->name << '"';
+    return;
+  }
+  out << '(';
+  const type_tree* curr = x;
+  while (curr && !curr->atom) {
+    dump_names(curr->left, out);
+    if (curr->right) out << ' ';
+    curr = curr->right;
+  }
+  // check for dotted list; should never happen
+  if (curr) {
+    out << ". ";
+    dump_names(curr, out);
+  }
+  out << ')';
+}
+
+string names_to_string_without_quotes(const type_tree* type) {
+  if (type == NULL) return "()";
+  ostringstream out;
+  dump_names_without_quotes(type, out);
+  return out.str();
+}
+
+void dump_names_without_quotes(const type_tree* x, ostream& out) {
+  if (!x) return;
+  if (x->atom) {
+    out << x->name;
+    return;
+  }
+  out << '(';
+  const type_tree* curr = x;
+  while (curr && !curr->atom) {
+    dump_names_without_quotes(curr->left, out);
+    if (curr->right) out << ' ';
+    curr = curr->right;
+  }
+  // check for dotted list; should never happen
+  if (curr) {
+    out << ". ";
+    dump_names_without_quotes(curr, out);
+  }
+  out << ')';
+}
+
+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
+}
+
+//: helper to print numbers without excessive precision
+
+:(before "End Types")
+struct no_scientific {
+  double x;
+  explicit no_scientific(double y) :x(y) {}
+};
+
+:(code)
+ostream& operator<<(ostream& os, no_scientific x) {
+  if (!isfinite(x.x)) {
+    // Infinity or NaN
+    os << x.x;
+    return os;
+  }
+  ostringstream tmp;
+  // more accurate, but too slow
+//?   tmp.precision(308);  // for 64-bit numbers
+  tmp << std::fixed << x.x;
+  os << trim_floating_point(tmp.str());
+  return os;
+}
+
+string trim_floating_point(const string& in) {
+  if (in.empty()) return "";
+  if (in.find('.') == string::npos) return in;
+  int length = SIZE(in);
+  while (length > 1) {
+    if (in.at(length-1) != '0') break;
+    --length;
+  }
+  if (in.at(length-1) == '.') --length;
+  if (length == 0) return "0";
+  return in.substr(0, length);
+}
+
+void test_trim_floating_point() {
+  CHECK_EQ(trim_floating_point(""), "");
+  CHECK_EQ(trim_floating_point(".0"), "0");
+  CHECK_EQ(trim_floating_point("1.5000"), "1.5");
+  CHECK_EQ(trim_floating_point("1.000001"), "1.000001");
+  CHECK_EQ(trim_floating_point("23.000000"), "23");
+  CHECK_EQ(trim_floating_point("23.0"), "23");
+  CHECK_EQ(trim_floating_point("23."), "23");
+  CHECK_EQ(trim_floating_point("23"), "23");
+  CHECK_EQ(trim_floating_point("230"), "230");
+  CHECK_EQ(trim_floating_point("3.000000"), "3");
+  CHECK_EQ(trim_floating_point("3.0"), "3");
+  CHECK_EQ(trim_floating_point("3."), "3");
+  CHECK_EQ(trim_floating_point("3"), "3");
+}
+
+:(before "End Includes")
+#include <map>
+using std::map;
+#include <utility>
+using std::pair;
+#include <math.h>
diff --git a/archive/2.vm/011load.cc b/archive/2.vm/011load.cc
new file mode 100644
index 00000000..4bb06c36
--- /dev/null
+++ b/archive/2.vm/011load.cc
@@ -0,0 +1,489 @@
+//: Phase 1 of running Mu code: load it from a textual representation.
+//:
+//: The process of running Mu code:
+//:   load -> transform -> run
+
+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"
+  );
+}
+
+vector<recipe_ordinal> load(string form) {
+  istringstream in(form);
+  in >> std::noskipws;
+  return load(in);
+}
+
+vector<recipe_ordinal> load(istream& in) {
+  in >> std::noskipws;
+  vector<recipe_ordinal> result;
+  while (has_data(in)) {
+    skip_whitespace_and_comments(in);
+    if (!has_data(in)) break;
+    string command = next_word(in);
+    if (command.empty()) {
+      assert(!has_data(in));
+      break;
+    }
+    // Command Handlers
+    if (command == "recipe" || command == "def") {
+      recipe_ordinal r = slurp_recipe(in);
+      if (r > 0) result.push_back(r);
+    }
+    else if (command == "recipe!" || command == "def!") {
+      Disable_redefine_checks = true;
+      recipe_ordinal r = slurp_recipe(in);
+      if (r > 0) result.push_back(r);
+      Disable_redefine_checks = false;
+    }
+    // End Command Handlers
+    else {
+      raise << "unknown top-level command: " << command << '\n' << end();
+    }
+  }
+  return result;
+}
+
+// return the recipe ordinal slurped, or -1 if it failed
+int slurp_recipe(istream& in) {
+  recipe result;
+  result.name = next_word(in);
+  if (result.name.empty()) {
+    assert(!has_data(in));
+    raise << "file ended with 'recipe'\n" << end();
+    return -1;
+  }
+  // End Load Recipe Name
+  skip_whitespace_but_not_newline(in);
+  // End Recipe Refinements
+  if (result.name.empty())
+    raise << "empty result.name\n" << end();
+  trace(101, "parse") << "--- defining " << result.name << end();
+  if (!contains_key(Recipe_ordinal, result.name))
+    put(Recipe_ordinal, result.name, Next_recipe_ordinal);
+  result.ordinal = get(Recipe_ordinal, result.name);
+  ++Next_recipe_ordinal;
+  if (Recipe.find(get(Recipe_ordinal, result.name)) != Recipe.end()) {
+    trace(101, "parse") << "already exists" << end();
+    if (should_check_for_redefine(result.name))
+      raise << "redefining recipe " << result.name << "\n" << end();
+    Recipe.erase(get(Recipe_ordinal, result.name));
+  }
+  slurp_body(in, result);
+  // End Recipe Body(result)
+  put(Recipe, get(Recipe_ordinal, result.name), result);
+  return get(Recipe_ordinal, result.name);
+}
+
+void slurp_body(istream& in, recipe& result) {
+  in >> std::noskipws;
+  skip_whitespace_but_not_newline(in);
+  if (in.get() != '[')
+    raise << result.name << ": recipe body must begin with '['\n" << end();
+  skip_whitespace_and_comments(in);  // permit trailing comment after '['
+  instruction curr;
+  while (next_instruction(in, &curr)) {
+    curr.original_string = to_original_string(curr);
+    // End Rewrite Instruction(curr, recipe result)
+    trace(102, "load") << "after rewriting: " << to_string(curr) << end();
+    if (!curr.is_empty()) result.steps.push_back(curr);
+  }
+}
+
+bool next_instruction(istream& in, instruction* curr) {
+  curr->clear();
+  skip_whitespace_and_comments(in);
+  if (!has_data(in)) {
+    raise << "incomplete recipe at end of file (0)\n" << end();
+    return false;
+  }
+
+  vector<string> words;
+  while (has_data(in) && in.peek() != '\n') {
+    skip_whitespace_but_not_newline(in);
+    if (!has_data(in)) {
+      raise << "incomplete recipe at end of file (1)\n" << end();
+      return false;
+    }
+    string word = next_word(in);
+    if (word.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete recipe at end of file (2)\n" << end();
+      return false;
+    }
+    words.push_back(word);
+    skip_whitespace_but_not_newline(in);
+  }
+  skip_whitespace_and_comments(in);
+  if (SIZE(words) == 1 && words.at(0) == "]")
+    return false;  // end of recipe
+
+  if (SIZE(words) == 1 && is_label_word(words.at(0))) {
+    curr->is_label = true;
+    curr->label = words.at(0);
+    trace(103, "parse") << "label: " << curr->label << end();
+    if (!has_data(in)) {
+      raise << "incomplete recipe at end of file (3)\n" << end();
+      return false;
+    }
+    return true;
+  }
+
+  vector<string>::iterator p = words.begin();
+  if (find(words.begin(), words.end(), "<-") != words.end()) {
+    for (;  *p != "<-";  ++p)
+      curr->products.push_back(reagent(*p));
+    ++p;  // skip <-
+  }
+
+  if (p == words.end()) {
+    raise << "instruction prematurely ended with '<-'\n" << end();
+    return false;
+  }
+  curr->name = *p;  ++p;
+  // curr->operation will be set at transform time
+
+  for (;  p != words.end();  ++p)
+    curr->ingredients.push_back(reagent(*p));
+
+  trace(103, "parse") << "instruction: " << curr->name << end();
+  trace(103, "parse") << "  number of ingredients: " << SIZE(curr->ingredients) << end();
+  for (vector<reagent>::iterator p = curr->ingredients.begin();  p != curr->ingredients.end();  ++p)
+    trace(103, "parse") << "  ingredient: " << to_string(*p) << end();
+  for (vector<reagent>::iterator p = curr->products.begin();  p != curr->products.end();  ++p)
+    trace(103, "parse") << "  product: " << to_string(*p) << end();
+  if (!has_data(in)) {
+    raise << "9: unbalanced '[' for recipe\n" << end();
+    return false;
+  }
+  // End next_instruction(curr)
+  return true;
+}
+
+// can return empty string -- only if 'in' has no more data
+string next_word(istream& in) {
+  skip_whitespace_but_not_newline(in);
+  // End next_word Special-cases
+  ostringstream out;
+  slurp_word(in, out);
+  skip_whitespace_and_comments_but_not_newline(in);
+  string result = out.str();
+  if (result != "[" && ends_with(result, '['))
+    raise << "insert a space before '[' in '" << result << "'\n" << end();
+  return result;
+}
+
+bool is_label_word(const string& word) {
+  if (word.empty()) return false;  // error raised elsewhere
+  return !isalnum(word.at(0)) && string("$_*@&,=-[]()").find(word.at(0)) == string::npos;
+}
+
+bool ends_with(const string& s, const char c) {
+  if (s.empty()) return false;
+  return *s.rbegin() == c;
+}
+
+:(before "End Globals")
+// word boundaries
+extern const string Terminators("(){}");
+:(code)
+void slurp_word(istream& in, ostream& out) {
+  char c;
+  if (has_data(in) && Terminators.find(in.peek()) != string::npos) {
+    in >> c;
+    out << c;
+    return;
+  }
+  while (in >> c) {
+    if (isspace(c) || Terminators.find(c) != string::npos || Ignore.find(c) != string::npos) {
+      in.putback(c);
+      break;
+    }
+    out << c;
+  }
+}
+
+void skip_whitespace_and_comments(istream& in) {
+  while (true) {
+    if (!has_data(in)) break;
+    if (isspace(in.peek())) in.get();
+    else if (Ignore.find(in.peek()) != string::npos) in.get();
+    else if (in.peek() == '#') skip_comment(in);
+    else break;
+  }
+}
+
+// confusing; move to the next line only to skip a comment, but never otherwise
+void skip_whitespace_and_comments_but_not_newline(istream& in) {
+  while (true) {
+    if (!has_data(in)) break;
+    if (in.peek() == '\n') break;
+    if (isspace(in.peek())) in.get();
+    else if (Ignore.find(in.peek()) != string::npos) in.get();
+    else if (in.peek() == '#') skip_comment(in);
+    else break;
+  }
+}
+
+void skip_comment(istream& in) {
+  if (has_data(in) && in.peek() == '#') {
+    in.get();
+    while (has_data(in) && in.peek() != '\n') in.get();
+  }
+}
+
+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"
+       "]\n"
+       "# abc");  // no newline after comment
+  cerr << ".";  // termination = success
+}
+
+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
+//: we'll want to disable the errors.
+:(before "End Globals")
+bool Disable_redefine_checks = false;
+:(before "End Reset")
+Disable_redefine_checks = false;
+:(code)
+bool should_check_for_redefine(const string& recipe_name) {
+  if (Disable_redefine_checks) return false;
+  return true;
+}
+
+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);
+}
+
+// for debugging
+void show_rest_of_stream(istream& in) {
+  cerr << '^';
+  char c;
+  while (in >> c)
+    cerr << c;
+  cerr << "$\n";
+  exit(0);
+}
diff --git a/archive/2.vm/012transform.cc b/archive/2.vm/012transform.cc
new file mode 100644
index 00000000..758a286c
--- /dev/null
+++ b/archive/2.vm/012transform.cc
@@ -0,0 +1,102 @@
+//: Phase 2: Filter loaded recipes through an extensible list of 'transforms'.
+//:
+//:   The process of running Mu code:
+//:     load -> transform -> run
+//:
+//: The hope is that this framework of transform tools will provide a
+//: deconstructed alternative to conventional compilers.
+//:
+//: We're going to have many transforms in Mu, and getting their order right
+//: (not the same as ordering of layers) is a well-known problem. Some tips:
+//:   a) Design each layer to rely on as few previous layers as possible.
+//:
+//:   b) When positioning transforms, try to find the tightest constraint in
+//:   each transform relative to previous layers.
+//:
+//:   c) Even so you'll periodically need to try adjusting each transform
+//:   relative to those in previous layers to find a better arrangement.
+
+:(before "End recipe Fields")
+int transformed_until;
+:(before "End recipe Constructor")
+transformed_until = -1;
+
+:(before "End Types")
+typedef void (*transform_fn)(const recipe_ordinal);
+
+:(before "End Globals")
+vector<transform_fn> Transform;
+
+:(before "End One-time Setup")
+initialize_transforms();
+:(code)
+void initialize_transforms() {
+  // Begin Transforms
+    // Begin Instruction Inserting/Deleting Transforms
+    // End Instruction Inserting/Deleting Transforms
+
+    // Begin Instruction Modifying Transforms
+    // End Instruction Modifying Transforms
+  // End Transforms
+
+  // Begin Checks
+  // End Checks
+}
+
+void transform_all() {
+  trace(100, "transform") << "=== transform_all()" << end();
+  // Begin transform_all
+  for (int t = 0;  t < SIZE(Transform);  ++t) {
+    for (map<recipe_ordinal, recipe>::iterator p = Recipe.begin();  p != Recipe.end();  ++p) {
+      recipe& r = p->second;
+      if (r.transformed_until != t-1) continue;
+      // End Transform Checks
+      (*Transform.at(t))(/*recipe_ordinal*/p->first);
+      r.transformed_until = t;
+    }
+  }
+  parse_int_reagents();  // do this after all other transforms have run
+  // End transform_all
+}
+
+//: Even though a run will involve many calls to transform_all() for tests,
+//: our logical model is to load all code, then transform all code, then run.
+//: If you load new code that should cause already-transformed recipes to
+//: change, that's not supported. To help detect such situations and raise
+//: helpful errors we track a count of the number of calls made to
+//: transform_all().
+:(before "End Globals")
+int Num_calls_to_transform_all = 0;
+:(after "void transform_all()")
+  ++Num_calls_to_transform_all;
+
+:(code)
+void parse_int_reagents() {
+  trace(101, "transform") << "--- parsing any uninitialized reagents as integers" << end();
+  for (map<recipe_ordinal, recipe>::iterator p = Recipe.begin();  p != Recipe.end();  ++p) {
+    recipe& r = p->second;
+    if (r.steps.empty()) continue;
+    for (int index = 0;  index < SIZE(r.steps);  ++index) {
+      instruction& inst = r.steps.at(index);
+      for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+        populate_value(inst.ingredients.at(i));
+      }
+      for (int i = 0;  i < SIZE(inst.products);  ++i) {
+        populate_value(inst.products.at(i));
+      }
+    }
+  }
+}
+
+void populate_value(reagent& r) {
+  if (r.initialized) return;
+  // End Reagent-parsing Exceptions
+  if (!is_integer(r.name)) return;
+  r.set_value(to_integer(r.name));
+}
+
+// helper for tests -- temporarily suppress run
+void transform(string form) {
+  load(form);
+  transform_all();
+}
diff --git a/archive/2.vm/013update_operation.cc b/archive/2.vm/013update_operation.cc
new file mode 100644
index 00000000..ffe3dbb9
--- /dev/null
+++ b/archive/2.vm/013update_operation.cc
@@ -0,0 +1,40 @@
+//: Once all code is loaded, save operation ids of instructions and check that
+//: nothing's undefined.
+
+:(before "End Instruction Modifying Transforms")
+Transform.push_back(update_instruction_operations);  // idempotent
+
+:(code)
+void update_instruction_operations(const recipe_ordinal r) {
+  trace(101, "transform") << "--- compute instruction operations for recipe " << get(Recipe, r).name << end();
+  recipe& caller = get(Recipe, r);
+//?   cerr << "--- compute instruction operations for recipe " << caller.name << '\n';
+  for (int index = 0;  index < SIZE(caller.steps);  ++index) {
+    instruction& inst = caller.steps.at(index);
+    if (inst.is_label) continue;
+    if (!contains_key(Recipe_ordinal, inst.name)) {
+      raise << maybe(caller.name) << "instruction '" << inst.name << "' has no recipe in '" << to_original_string(inst) << "'\n" << end();
+      continue;
+    }
+    inst.operation = get(Recipe_ordinal, inst.name);
+    // End Instruction Operation Checks
+  }
+}
+
+// hook to suppress inserting recipe name into errors
+string maybe(string recipe_name) {
+  // End maybe(recipe_name) Special-cases
+  return recipe_name + ": ";
+}
+
+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/archive/2.vm/014literal_string.cc b/archive/2.vm/014literal_string.cc
new file mode 100644
index 00000000..84dbe8d0
--- /dev/null
+++ b/archive/2.vm/014literal_string.cc
@@ -0,0 +1,274 @@
+//: For convenience, some instructions will take literal arrays of characters
+//: (text or strings).
+//:
+//: Instead of quotes, we'll use [] to delimit strings. That'll reduce the
+//: need for escaping since we can support nested brackets. And we can also
+//: imagine that 'recipe' might one day itself be defined in Mu, doing its own
+//: parsing.
+
+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);
+
+:(before "End next_word Special-cases")
+if (in.peek() == '[') {
+  string result = slurp_quoted(in);
+  skip_whitespace_and_comments_but_not_newline(in);
+  return result;
+}
+
+:(code)
+string slurp_quoted(istream& in) {
+  ostringstream out;
+  assert(has_data(in));  assert(in.peek() == '[');  out << static_cast<char>(in.get());  // slurp the '['
+  if (is_code_string(in, out))
+    slurp_quoted_comment_aware(in, out);
+  else
+    slurp_quoted_comment_oblivious(in, out);
+  return out.str();
+}
+
+// A string is a code string (ignores comments when scanning for matching
+// brackets) if it contains a newline at the start before any non-whitespace.
+bool is_code_string(istream& in, ostream& out) {
+  while (has_data(in)) {
+    char c = in.get();
+    if (!isspace(c)) {
+      in.putback(c);
+      return false;
+    }
+    out << c;
+    if (c == '\n') {
+      return true;
+    }
+  }
+  return false;
+}
+
+// Read a regular string. Regular strings can only contain other regular
+// strings.
+void slurp_quoted_comment_oblivious(istream& in, ostream& out) {
+  int brace_depth = 1;
+  while (has_data(in)) {
+    char c = in.get();
+    if (c == '\\') {
+      slurp_one_past_backslashes(in, out);
+      continue;
+    }
+    out << c;
+    if (c == '[') ++brace_depth;
+    if (c == ']') --brace_depth;
+    if (brace_depth == 0) break;
+  }
+  if (!has_data(in) && brace_depth > 0) {
+    raise << "unbalanced '['\n" << end();
+    out.clear();
+  }
+}
+
+// Read a code string. Code strings can contain either code or regular strings.
+void slurp_quoted_comment_aware(istream& in, ostream& out) {
+  char c;
+  while (in >> c) {
+    if (c == '\\') {
+      slurp_one_past_backslashes(in, out);
+      continue;
+    }
+    if (c == '#') {
+      out << c;
+      while (has_data(in) && in.peek() != '\n') out << static_cast<char>(in.get());
+      continue;
+    }
+    if (c == '[') {
+      in.putback(c);
+      // recurse
+      out << slurp_quoted(in);
+      continue;
+    }
+    out << c;
+    if (c == ']') return;
+  }
+  raise << "unbalanced '['\n" << end();
+  out.clear();
+}
+
+:(after "Parsing reagent(string s)")
+if (starts_with(s, "[")) {
+  if (*s.rbegin() != ']') return;  // unbalanced bracket; handled elsewhere
+  name = s;
+  // delete [] delimiters
+  name.erase(0, 1);
+  strip_last(name);
+  type = new type_tree("literal-string", 0);
+  return;
+}
+
+//: Unlike other reagents, escape newlines in literal strings to make them
+//: more friendly to trace().
+
+:(after "string to_string(const reagent& r)")
+  if (is_literal_text(r))
+    return emit_literal_string(r.name);
+
+:(code)
+bool is_literal_text(const reagent& x) {
+  return x.type && x.type->name == "literal-string";
+}
+
+string emit_literal_string(string name) {
+  size_t pos = 0;
+  while (pos != string::npos)
+    pos = replace(name, "\n", "\\n", pos);
+  return "{\""+name+"\": \"literal-string\"}";
+}
+
+size_t replace(string& str, const string& from, const string& to, size_t n) {
+  size_t result = str.find(from, n);
+  if (result != string::npos)
+    str.replace(result, from.length(), to);
+  return result;
+}
+
+void strip_last(string& s) {
+  if (!s.empty()) s.erase(SIZE(s)-1);
+}
+
+void slurp_one_past_backslashes(istream& in, ostream& out) {
+  // When you encounter a backslash, strip it out and pass through any
+  // following run of backslashes. If we 'escaped' a single following
+  // character, then the character '\' would be:
+  //   '\\' escaped once
+  //   '\\\\' escaped twice
+  //   '\\\\\\\\' escaped thrice (8 backslashes)
+  // ..and so on. With our approach it'll be:
+  //   '\\' escaped once
+  //   '\\\' escaped twice
+  //   '\\\\' escaped thrice
+  // This only works as long as backslashes aren't also overloaded to create
+  // special characters. So Mu doesn't follow C's approach of overloading
+  // backslashes both to escape quote characters and also as a notation for
+  // unprintable characters like '\n'.
+  while (has_data(in)) {
+    char c = in.get();
+    out << c;
+    if (c != '\\') break;
+  }
+}
+
+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/archive/2.vm/015literal_noninteger.cc b/archive/2.vm/015literal_noninteger.cc
new file mode 100644
index 00000000..12f24586
--- /dev/null
+++ b/archive/2.vm/015literal_noninteger.cc
@@ -0,0 +1,51 @@
+//: Support literal non-integers.
+
+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)) {
+  name = s;
+  type = new type_tree("literal-fractional-number", 0);
+  set_value(to_double(s));
+  return;
+}
+
+:(code)
+bool is_noninteger(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
+      && std::count(s.begin(), s.end(), '.') == 1;  // exactly one decimal point
+}
+
+double to_double(string n) {
+  char* end = NULL;
+  // safe because string.c_str() is guaranteed to be null-terminated
+  double result = strtod(n.c_str(), &end);
+  assert(*end == '\0');
+  return result;
+}
+
+void test_is_noninteger() {
+  CHECK(!is_noninteger("1234"));
+  CHECK(!is_noninteger("1a2"));
+  CHECK(is_noninteger("234.0"));
+  CHECK(!is_noninteger("..."));
+  CHECK(!is_noninteger("."));
+  CHECK(is_noninteger("2."));
+  CHECK(is_noninteger(".2"));
+  CHECK(is_noninteger("-.2"));
+  CHECK(is_noninteger("-2."));
+  CHECK(!is_noninteger("--.2"));
+  CHECK(!is_noninteger(".-2"));
+  CHECK(!is_noninteger("..2"));
+}
diff --git a/archive/2.vm/016dilated_reagent.cc b/archive/2.vm/016dilated_reagent.cc
new file mode 100644
index 00000000..1354ba45
--- /dev/null
+++ b/archive/2.vm/016dilated_reagent.cc
@@ -0,0 +1,166 @@
+//: An alternative syntax for reagents that permits whitespace in properties,
+//: grouped by brackets. We'll use this ability in the next layer, when we
+//: generalize types from lists to trees of properties.
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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);
+}
+
+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.
+
+:(before "End next_word Special-cases")
+if (in.peek() == '(')
+  return slurp_balanced_bracket(in);
+// treat curlies mostly like parens, but don't mess up labels
+if (start_of_dilated_reagent(in))
+  return slurp_balanced_bracket(in);
+
+:(code)
+// A curly is considered a label if it's the last thing on a line. Dilated
+// reagents should remain all on one line.
+bool start_of_dilated_reagent(istream& in) {
+  if (in.peek() != '{') return false;
+  int pos = in.tellg();
+  in.get();  // slurp '{'
+  skip_whitespace_but_not_newline(in);
+  char next = in.peek();
+  in.seekg(pos);
+  return next != '\n';
+}
+
+// Assume the first letter is an open bracket, and read everything until the
+// matching close bracket.
+// We balance {} () and [].
+string slurp_balanced_bracket(istream& in) {
+  ostringstream result;
+  char c;
+  list<char> open_brackets;
+  while (in >> c) {
+    if (c == '(') open_brackets.push_back(c);
+    if (c == ')') {
+      if (open_brackets.empty() || open_brackets.back() != '(') {
+        raise << "unbalanced ')'\n" << end();
+        continue;
+      }
+      assert(open_brackets.back() == '(');
+      open_brackets.pop_back();
+    }
+    if (c == '[') open_brackets.push_back(c);
+    if (c == ']') {
+      if (open_brackets.empty() || open_brackets.back() != '[') {
+        raise << "unbalanced ']'\n" << end();
+        continue;
+      }
+      open_brackets.pop_back();
+    }
+    if (c == '{') open_brackets.push_back(c);
+    if (c == '}') {
+      if (open_brackets.empty() || open_brackets.back() != '{') {
+        raise << "unbalanced '}'\n" << end();
+        continue;
+      }
+      open_brackets.pop_back();
+    }
+    result << c;
+    if (open_brackets.empty()) break;
+  }
+  skip_whitespace_and_comments_but_not_newline(in);
+  return result.str();
+}
+
+:(after "Parsing reagent(string s)")
+if (starts_with(s, "{")) {
+  assert(properties.empty());
+  istringstream in(s);
+  in >> std::noskipws;
+  in.get();  // skip '{'
+  name = slurp_key(in);
+  if (name.empty()) {
+    raise << "invalid reagent '" << s << "' without a name\n" << end();
+    return;
+  }
+  if (name == "}") {
+    raise << "invalid empty reagent '" << s << "'\n" << end();
+    return;
+  }
+  {
+    string s = next_word(in);
+    if (s.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete dilated reagent at end of file (0)\n" << end();
+      return;
+    }
+    string_tree* type_names = new string_tree(s);
+    // End Parsing Dilated Reagent Type Property(type_names)
+    type = new_type_tree(type_names);
+    delete type_names;
+  }
+  while (has_data(in)) {
+    string key = slurp_key(in);
+    if (key.empty()) continue;
+    if (key == "}") continue;
+    string s = next_word(in);
+    if (s.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete dilated reagent at end of file (1)\n" << end();
+      return;
+    }
+    string_tree* value = new string_tree(s);
+    // End Parsing Dilated Reagent Property(value)
+    properties.push_back(pair<string, string_tree*>(key, value));
+  }
+  return;
+}
+
+:(code)
+string slurp_key(istream& in) {
+  string result = next_word(in);
+  if (result.empty()) {
+    assert(!has_data(in));
+    raise << "incomplete dilated reagent at end of file (2)\n" << end();
+    return result;
+  }
+  while (!result.empty() && *result.rbegin() == ':')
+    strip_last(result);
+  while (isspace(in.peek()) || in.peek() == ':')
+    in.get();
+  return result;
+}
diff --git a/archive/2.vm/017parse_tree.cc b/archive/2.vm/017parse_tree.cc
new file mode 100644
index 00000000..02170f6d
--- /dev/null
+++ b/archive/2.vm/017parse_tree.cc
@@ -0,0 +1,124 @@
+// So far instructions can only contain linear lists of properties. Now we add
+// support for more complex trees of properties in dilated reagents. This will
+// come in handy later for expressing complex types, like "a dictionary from
+// (address to array of charaters) to (list of numbers)".
+//
+// Type trees aren't as general as s-expressions even if they look like them:
+// the first element of a type tree is always an atom, and it can never be
+// dotted (right->right->right->...->right is always NULL).
+//
+// For now you can't use the simpler 'colon-based' representation inside type
+// trees. Once you start typing parens, keep on typing parens.
+
+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);
+:(before "End Parsing Dilated Reagent Type Property(type_names)")
+type_names = parse_string_tree(type_names);
+
+:(code)
+string_tree* parse_string_tree(string_tree* s) {
+  assert(s->atom);
+  if (!starts_with(s->value, "(")) return s;
+  string_tree* result = parse_string_tree(s->value);
+  delete s;
+  return result;
+}
+
+string_tree* parse_string_tree(const string& s) {
+  istringstream in(s);
+  in >> std::noskipws;
+  return parse_string_tree(in);
+}
+
+string_tree* parse_string_tree(istream& in) {
+  skip_whitespace_but_not_newline(in);
+  if (!has_data(in)) return NULL;
+  if (in.peek() == ')') {
+    in.get();
+    return NULL;
+  }
+  if (in.peek() != '(') {
+    string s = next_word(in);
+    if (s.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete string tree at end of file (0)\n" << end();
+      return NULL;
+    }
+    string_tree* result = new string_tree(s);
+    return result;
+  }
+  in.get();  // skip '('
+  string_tree* result = NULL;
+  string_tree** curr = &result;
+  while (true) {
+    skip_whitespace_but_not_newline(in);
+    assert(has_data(in));
+    if (in.peek() == ')') break;
+    *curr = new string_tree(NULL, NULL);
+    if (in.peek() == '(') {
+      (*curr)->left = parse_string_tree(in);
+    }
+    else {
+      string s = next_word(in);
+      if (s.empty()) {
+        assert(!has_data(in));
+        raise << "incomplete string tree at end of file (1)\n" << end();
+        return NULL;
+      }
+      (*curr)->left = new string_tree(s);
+    }
+    curr = &(*curr)->right;
+  }
+  in.get();  // skip ')'
+  assert(*curr == NULL);
+  return result;
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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/archive/2.vm/018constant.cc b/archive/2.vm/018constant.cc
new file mode 100644
index 00000000..bbf3a412
--- /dev/null
+++ b/archive/2.vm/018constant.cc
@@ -0,0 +1,79 @@
+//: A few literal constants.
+
+:(before "End Mu Types Initialization")
+put(Type_ordinal, "literal-boolean", 0);
+
+//: 'true'
+
+:(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") {
+  if (type != NULL) {
+    raise << "'true' is a literal and can't take a type\n" << end();
+    return;
+  }
+  type = new type_tree("literal-boolean");
+  set_value(1);
+}
+
+//: 'false'
+
+:(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") {
+  if (type != NULL) {
+    raise << "'false' is a literal and can't take a type\n" << end();
+    return;
+  }
+  type = new type_tree("literal-boolean");
+  set_value(0);
+}
+
+//: 'null'
+
+:(before "End Mu Types Initialization")
+put(Type_ordinal, "literal-address", 0);
+
+:(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") {
+  if (type != NULL) {
+    raise << "'null' is a literal and can't take a type\n" << end();
+    return;
+  }
+  type = new type_tree("literal-address");
+  set_value(0);
+}
diff --git a/archive/2.vm/019type_abbreviations.cc b/archive/2.vm/019type_abbreviations.cc
new file mode 100644
index 00000000..35cc5d90
--- /dev/null
+++ b/archive/2.vm/019type_abbreviations.cc
@@ -0,0 +1,236 @@
+//: For convenience, allow Mu types to be abbreviated.
+
+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;
+
+//:: Defining type abbreviations.
+
+:(before "End Command Handlers")
+else if (command == "type") {
+  load_type_abbreviations(in);
+}
+
+:(code)
+void load_type_abbreviations(istream& in) {
+  string new_type_name = next_word(in);
+  assert(has_data(in) || !new_type_name.empty());
+  if (!has_data(in) || new_type_name.empty()) {
+    raise << "incomplete 'type' statement; must be of the form 'type <new type name> = <type expression>'\n" << end();
+    return;
+  }
+  string arrow = next_word(in);
+  assert(has_data(in) || !arrow.empty());
+  if (arrow.empty()) {
+    raise << "incomplete 'type' statement 'type " << new_type_name << "'\n" << end();
+    return;
+  }
+  if (arrow != "=") {
+    raise << "'type' statements must be of the form 'type <new type name> = <type expression>' but got 'type " << new_type_name << ' ' << arrow << "'\n" << end();
+    return;
+  }
+  if (!has_data(in)) {
+    raise << "incomplete 'type' statement 'type " << new_type_name << " ='\n" << end();
+    return;
+  }
+  string old = next_word(in);
+  if (old.empty()) {
+    raise << "incomplete 'type' statement 'type " << new_type_name << " ='\n" << end();
+    raise << "'type' statements must be of the form 'type <new type name> = <type expression>' but got 'type " << new_type_name << ' ' << arrow << "'\n" << end();
+    return;
+  }
+  if (contains_key(Type_abbreviations, new_type_name)) {
+    raise << "'type' conflict: '" << new_type_name << "' defined as both '" << names_to_string_without_quotes(get(Type_abbreviations, new_type_name)) << "' and '" << old << "'\n" << end();
+    return;
+  }
+  trace(100, "type") << "alias " << new_type_name << " = " << old << end();
+  type_tree* old_type = new_type_tree(old);
+  put(Type_abbreviations, new_type_name, old_type);
+}
+
+type_tree* new_type_tree(const string& x) {
+  string_tree* type_names = starts_with(x, "(") ? parse_string_tree(x) : parse_string_list(x);
+  type_tree* result = new_type_tree(type_names);
+  delete type_names;
+  expand_type_abbreviations(result);
+  return result;
+}
+
+string_tree* parse_string_list(const string& s) {
+  istringstream in(s);
+  in >> std::noskipws;
+  return parse_property_list(in);
+}
+
+void test_type_error1() {
+  Hide_errors = true;
+  transform(
+      "type foo\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: incomplete 'type' statement 'type foo'\n"
+  );
+}
+
+void test_type_error2() {
+  Hide_errors = true;
+  transform(
+      "type foo =\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "error: incomplete 'type' statement 'type foo ='\n"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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
+
+:(before "End save_snapshots")
+Type_abbreviations_snapshot = Type_abbreviations;
+:(before "End restore_snapshots")
+restore_type_abbreviations();
+:(before "End One-time Setup")
+atexit(clear_type_abbreviations);
+:(code)
+void restore_type_abbreviations() {
+  for (map<string, type_tree*>::iterator p = Type_abbreviations.begin();  p != Type_abbreviations.end();  ++p) {
+    if (!contains_key(Type_abbreviations_snapshot, p->first))
+      delete p->second;
+  }
+  Type_abbreviations.clear();
+  Type_abbreviations = Type_abbreviations_snapshot;
+}
+void clear_type_abbreviations() {
+  for (map<string, type_tree*>::iterator p = Type_abbreviations.begin();  p != Type_abbreviations.end();  ++p)
+    delete p->second;
+  Type_abbreviations.clear();
+}
+
+//:: A few default abbreviations.
+
+:(before "End Mu Types Initialization")
+put(Type_abbreviations, "&", new_type_tree("address"));
+put(Type_abbreviations, "@", new_type_tree("array"));
+put(Type_abbreviations, "num", new_type_tree("number"));
+put(Type_abbreviations, "bool", new_type_tree("boolean"));
+put(Type_abbreviations, "char", new_type_tree("character"));
+
+:(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.
+
+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
+// Begin Type Modifying Transforms
+// End Type Modifying Transforms
+
+:(code)
+void expand_type_abbreviations(const recipe_ordinal r) {
+  expand_type_abbreviations(get(Recipe, r));
+}
+
+void expand_type_abbreviations(const recipe& caller) {
+  trace(101, "transform") << "--- expand type abbreviations in recipe '" << caller.name << "'" << end();
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    const instruction& inst = caller.steps.at(i);
+    trace(102, "transform") << "instruction '" << to_original_string(inst) << end();
+    for (long int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+      expand_type_abbreviations(inst.ingredients.at(i).type);
+      trace(102, "transform") << "ingredient type after expanding abbreviations: " << names_to_string(inst.ingredients.at(i).type) << end();
+    }
+    for (long int i = 0;  i < SIZE(inst.products);  ++i) {
+      expand_type_abbreviations(inst.products.at(i).type);
+      trace(102, "transform") << "product type after expanding abbreviations: " << names_to_string(inst.products.at(i).type) << end();
+    }
+  }
+  // End Expand Type Abbreviations(caller)
+}
+
+void expand_type_abbreviations(type_tree* type) {
+  if (!type) return;
+  if (!type->atom) {
+    expand_type_abbreviations(type->left);
+    expand_type_abbreviations(type->right);
+    return;
+  }
+  if (contains_key(Type_abbreviations, type->name))
+    *type = type_tree(*get(Type_abbreviations, type->name));
+}
diff --git a/archive/2.vm/020run.cc b/archive/2.vm/020run.cc
new file mode 100644
index 00000000..aa4513e4
--- /dev/null
+++ b/archive/2.vm/020run.cc
@@ -0,0 +1,571 @@
+//: Phase 3: Start running a loaded and transformed recipe.
+//:
+//:   The process of running Mu code:
+//:     load -> transform -> run
+//:
+//: So far we've seen recipes as lists of instructions, and instructions point
+//: at other recipes. To kick things off Mu needs to know how to run certain
+//: 'primitive' recipes. That will then give the ability to run recipes
+//: containing these primitives.
+//:
+//: This layer defines a skeleton with just two primitive recipes: IDLE which
+//: does nothing, and COPY, which can copy numbers from one memory location to
+//: another. Later layers will add more primitives.
+
+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.
+//: Later layers will replace this to support running multiple routines at once.
+struct routine {
+  recipe_ordinal running_recipe;
+  int running_step_index;
+  routine(recipe_ordinal r) :running_recipe(r), running_step_index(0) {}
+  bool completed() const;
+  const vector<instruction>& steps() const;
+};
+
+:(before "End Globals")
+routine* Current_routine = NULL;
+:(before "End Reset")
+Current_routine = NULL;
+
+:(code)
+void run(const recipe_ordinal r) {
+  routine rr(r);
+  Current_routine = &rr;
+  run_current_routine();
+  Current_routine = NULL;
+}
+
+void run_current_routine() {
+  while (should_continue_running(Current_routine)) {  // beware: may modify Current_routine
+    // Running One Instruction
+    if (current_instruction().is_label) { ++current_step_index();  continue; }
+    trace(Callstack_depth, "run") << to_string(current_instruction()) << end();
+//?     if (Foo) cerr << "run: " << to_string(current_instruction()) << '\n';
+    if (get_or_insert(Memory, 0) != 0) {
+      raise << "something wrote to location 0; this should never happen\n" << end();
+      put(Memory, 0, 0);
+    }
+    // read all ingredients from memory, each potentially spanning multiple locations
+    vector<vector<double> > ingredients;
+    if (should_copy_ingredients()) {
+      for (int i = 0;  i < SIZE(current_instruction().ingredients);  ++i)
+        ingredients.push_back(read_memory(current_instruction().ingredients.at(i)));
+    }
+    // instructions below will write to 'products'
+    vector<vector<double> > products;
+    //: This will be a large switch that later layers will often insert cases
+    //: into. Never call 'continue' within it. Instead, we'll explicitly
+    //: control which of the following stages after the switch we run for each
+    //: instruction.
+    bool write_products = true;
+    bool fall_through_to_next_instruction = true;
+    switch (current_instruction().operation) {
+      // Primitive Recipe Implementations
+      case COPY: {
+        copy(ingredients.begin(), ingredients.end(), inserter(products, products.begin()));
+        break;
+      }
+      // End Primitive Recipe Implementations
+      default: {
+        raise << "not a primitive op: " << current_instruction().operation << '\n' << end();
+      }
+    }
+    //: used by a later layer
+    if (write_products) {
+      if (SIZE(products) < SIZE(current_instruction().products)) {
+        raise << SIZE(products) << " vs " << SIZE(current_instruction().products) << ": failed to write to all products in '" << to_original_string(current_instruction()) << "'\n" << end();
+      }
+      else {
+        for (int i = 0;  i < SIZE(current_instruction().products);  ++i) {
+          // Writing Instruction Product(i)
+          write_memory(current_instruction().products.at(i), products.at(i));
+        }
+      }
+    }
+    // End Running One Instruction
+    if (fall_through_to_next_instruction)
+      ++current_step_index();
+  }
+  stop_running_current_routine:;
+}
+
+//: Helpers for managing trace depths
+//:
+//: We're going to use trace depths primarily to segment code running at
+//: different frames of the call stack. This will make it easy for the trace
+//: browser to collapse over entire calls.
+//:
+//: The entire map of possible depths is as follows:
+//:
+//: Errors will be depth 0.
+//: Mu 'applications' will be able to use depths 1-99 as they like.
+//: Primitive statements will occupy 100 and up to Max_depth, organized by
+//: stack frames.
+:(before "End Globals")
+extern const int Initial_callstack_depth = 100;
+int Callstack_depth = Initial_callstack_depth;
+:(before "End Reset")
+Callstack_depth = Initial_callstack_depth;
+
+//: Other helpers for the VM.
+
+:(code)
+//: hook replaced in a later layer
+bool should_continue_running(const routine* current_routine) {
+  assert(current_routine == Current_routine);  // argument passed in just to make caller readable above
+  return !Current_routine->completed();
+}
+
+bool should_copy_ingredients() {
+  // End should_copy_ingredients Special-cases
+  return true;
+}
+
+bool is_mu_scalar(reagent/*copy*/ r) {
+  return is_mu_scalar(r.type);
+}
+bool is_mu_scalar(const type_tree* type) {
+  if (!type) return false;
+  if (is_mu_address(type)) return false;
+  if (!type->atom) return false;
+  if (is_literal(type))
+    return type->name != "literal-string";
+  return size_of(type) == 1;
+}
+
+bool is_mu_address(reagent/*copy*/ r) {
+  // End Preprocess is_mu_address(reagent r)
+  return is_mu_address(r.type);
+}
+bool is_mu_address(const type_tree* type) {
+  if (!type) return false;
+  if (is_literal(type)) return false;
+  if (type->atom) return false;
+  if (!type->left->atom) {
+    raise << "invalid type " << to_string(type) << '\n' << end();
+    return false;
+  }
+  return type->left->value == Address_type_ordinal;
+}
+
+//: Some helpers.
+//: Important that they return references into the current routine.
+
+//: hook replaced in a later layer
+int& current_step_index() {
+  return Current_routine->running_step_index;
+}
+
+//: hook replaced in a later layer
+recipe_ordinal currently_running_recipe() {
+  return Current_routine->running_recipe;
+}
+
+//: hook replaced in a later layer
+const string& current_recipe_name() {
+  return get(Recipe, Current_routine->running_recipe).name;
+}
+
+//: hook replaced in a later layer
+const recipe& current_recipe() {
+  return get(Recipe, Current_routine->running_recipe);
+}
+
+//: hook replaced in a later layer
+const instruction& current_instruction() {
+  return get(Recipe, Current_routine->running_recipe).steps.at(Current_routine->running_step_index);
+}
+
+//: hook replaced in a later layer
+bool routine::completed() const {
+  return running_step_index >= SIZE(get(Recipe, running_recipe).steps);
+}
+
+//: hook replaced in a later layer
+const vector<instruction>& routine::steps() const {
+  return get(Recipe, running_recipe).steps;
+}
+
+//:: Startup flow
+
+:(before "End Mu Prelude")
+load_file_or_directory("core.mu");
+//? DUMP("");
+//? exit(0);
+
+//: Step 2: load any .mu files provided at the commandline
+:(before "End Commandline Parsing")
+// Check For .mu Files
+if (argc > 1) {
+  // skip argv[0]
+  ++argv;
+  --argc;
+  while (argc > 0) {
+    // ignore argv past '--'; that's commandline args for 'main'
+    if (string(*argv) == "--") break;
+    if (starts_with(*argv, "--"))
+      cerr << "treating " << *argv << " as a file rather than an option\n";
+    load_file_or_directory(*argv);
+    --argc;
+    ++argv;
+  }
+  if (Run_tests) Recipe.erase(get(Recipe_ordinal, "main"));
+}
+transform_all();
+//? cerr << to_original_string(get(Type_ordinal, "editor")) << '\n';
+//? cerr << to_original_string(get(Recipe, get(Recipe_ordinal, "event-loop"))) << '\n';
+//? DUMP("");
+//? exit(0);
+if (trace_contains_errors()) return 1;
+if (Trace_stream && Run_tests) {
+  // We'll want a trace per test. Clear the trace.
+  delete Trace_stream;
+  Trace_stream = NULL;
+}
+save_snapshots();
+
+//: Step 3: if we aren't running tests, locate a recipe called 'main' and
+//: start running it.
+:(before "End Main")
+if (!Run_tests && contains_key(Recipe_ordinal, "main") && contains_key(Recipe, get(Recipe_ordinal, "main"))) {
+  // Running Main
+  reset();
+  trace(2, "run") << "=== Starting to run" << end();
+  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);
+}
+
+void load_file_or_directory(string filename) {
+  if (is_directory(filename)) {
+    load_all(filename);
+    return;
+  }
+  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 test to run.
+    return;
+  }
+  trace(2, "load") << "=== " << filename << end();
+  load(fin);
+  fin.close();
+}
+
+bool is_directory(string path) {
+  struct stat info;
+  if (stat(path.c_str(), &info)) return false;  // error
+  return info.st_mode & S_IFDIR;
+}
+
+void load_all(string dir) {
+  dirent** files;
+  int num_files = scandir(dir.c_str(), &files, NULL, alphasort);
+  for (int i = 0;  i < num_files;  ++i) {
+    string curr_file = files[i]->d_name;
+    if (isdigit(curr_file.at(0)) && ends_with(curr_file, ".mu"))
+      load_file_or_directory(dir+'/'+curr_file);
+    free(files[i]);
+    files[i] = NULL;
+  }
+  free(files);
+}
+
+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 Includes")
+#include <dirent.h>
+#include <sys/stat.h>
+
+//:: Reading from memory, writing to memory.
+
+:(code)
+vector<double> read_memory(reagent/*copy*/ x) {
+  // Begin Preprocess read_memory(x)
+  vector<double> result;
+  if (x.name == "null") result.push_back(/*alloc id*/0);
+  if (is_literal(x)) {
+    result.push_back(x.value);
+    return result;
+  }
+  // End Preprocess read_memory(x)
+  int size = size_of(x);
+  for (int offset = 0;  offset < size;  ++offset) {
+    double val = get_or_insert(Memory, x.value+offset);
+    trace(Callstack_depth+1, "mem") << "location " << x.value+offset << " is " << no_scientific(val) << end();
+    result.push_back(val);
+  }
+  return result;
+}
+
+void write_memory(reagent/*copy*/ x, const vector<double>& data) {
+  assert(Current_routine);  // run-time only
+  // Begin Preprocess write_memory(x, data)
+  if (!x.type) {
+    raise << "can't write to '" << to_string(x) << "'; no type\n" << end();
+    return;
+  }
+  if (is_dummy(x)) return;
+  if (is_literal(x)) return;
+  // End Preprocess write_memory(x, data)
+  if (x.value == 0) {
+    raise << "can't write to location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+    return;
+  }
+  if (size_mismatch(x, data)) {
+    raise << maybe(current_recipe_name()) << "size mismatch in storing to '" << x.original_string << "' (" << size_of(x) << " vs " << SIZE(data) << ") at '" << to_original_string(current_instruction()) << "'\n" << end();
+    return;
+  }
+  // End write_memory(x) Special-cases
+  for (int offset = 0;  offset < SIZE(data);  ++offset) {
+    assert(x.value+offset > 0);
+    trace(Callstack_depth+1, "mem") << "storing " << no_scientific(data.at(offset)) << " in location " << x.value+offset << end();
+//?     if (Foo) cerr << "mem: storing " << no_scientific(data.at(offset)) << " in location " << x.value+offset << '\n';
+    put(Memory, x.value+offset, data.at(offset));
+  }
+}
+
+:(code)
+int size_of(const reagent& r) {
+  if (!r.type) return 0;
+  // End size_of(reagent r) Special-cases
+  return size_of(r.type);
+}
+int size_of(const type_tree* type) {
+  if (!type) return 0;
+  if (type->atom) {
+    if (type->value == -1) return 1;  // error value, but we'll raise it elsewhere
+    if (type->value == 0) return 1;
+    // End size_of(type) Atom Special-cases
+  }
+  else {
+    if (!type->left->atom) {
+      raise << "invalid type " << to_string(type) << '\n' << end();
+      return 0;
+    }
+    if (type->left->value == Address_type_ordinal) return 2;  // address and alloc id
+    // End size_of(type) Non-atom Special-cases
+  }
+  // End size_of(type) Special-cases
+  return 1;
+}
+
+bool size_mismatch(const reagent& x, const vector<double>& data) {
+  if (!x.type) return true;
+  // End size_mismatch(x) Special-cases
+//?   if (size_of(x) != SIZE(data)) cerr << size_of(x) << " vs " << SIZE(data) << '\n';
+  return size_of(x) != SIZE(data);
+}
+
+bool is_literal(const reagent& r) {
+  return is_literal(r.type);
+}
+bool is_literal(const type_tree* type) {
+  if (!type) return false;
+  if (!type->atom) return false;
+  return type->value == 0;
+}
+
+bool scalar(const vector<int>& x) {
+  return SIZE(x) == 1;
+}
+bool scalar(const vector<double>& x) {
+  return SIZE(x) == 1;
+}
+
+// helper for tests
+void run(const string& form) {
+  vector<recipe_ordinal> tmp = load(form);
+  transform_all();
+  if (tmp.empty()) return;
+  if (trace_contains_errors()) return;
+  // if a test defines main, it probably wants to start there regardless of
+  // definition order
+  if (contains_key(Recipe, get(Recipe_ordinal, "main")))
+    run(get(Recipe_ordinal, "main"));
+  else
+    run(tmp.front());
+}
+
+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 '<-'.
+
+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.
+
+:(before "End Globals")
+bool Run_profiler = false;
+// We'll key profile information by recipe_ordinal rather than name because
+// it's more efficient, and because later layers will show more than just the
+// name of a recipe.
+//
+// One drawback: if you're clearing recipes your profile will be inaccurate.
+// So far that happens in tests, and in 'run-sandboxed' in a later layer.
+map<recipe_ordinal, int> Instructions_running;
+:(before "End Commandline Options(*arg)")
+else if (is_equal(*arg, "--profile")) {
+  Run_profiler = true;
+}
+:(after "Running One Instruction")
+if (Run_profiler) Instructions_running[currently_running_recipe()]++;
+:(before "End One-time Setup")
+atexit(dump_profile);
+:(code)
+void dump_profile() {
+  if (!Run_profiler) return;
+  if (Run_tests) {
+    cerr << "It's not a good idea to profile a run with tests, since tests can create conflicting recipes and mislead you. To try it anyway, comment out this check in the code.\n";
+    return;
+  }
+  ofstream fout;
+  fout.open("profile.instructions");
+  if (fout) {
+    for (map<recipe_ordinal, int>::iterator p = Instructions_running.begin();  p != Instructions_running.end();  ++p) {
+      fout << std::setw(9) << p->second << ' ' << header_label(p->first) << '\n';
+    }
+  }
+  fout.close();
+  // End dump_profile
+}
+
+// overridden in a later layer
+string header_label(const recipe_ordinal r) {
+  return get(Recipe, r).name;
+}
diff --git a/archive/2.vm/021check_instruction.cc b/archive/2.vm/021check_instruction.cc
new file mode 100644
index 00000000..5a8e1324
--- /dev/null
+++ b/archive/2.vm/021check_instruction.cc
@@ -0,0 +1,260 @@
+//: Introduce a new transform to perform various checks in instructions before
+//: we start running them. It'll be extensible, so that we can add checks for
+//: new recipes as we extend 'run' to support them.
+//:
+//: Doing checking in a separate part complicates things, because the values
+//: of variables in memory and the processor (current_recipe_name,
+//: current_instruction) aren't available at checking time. If I had a more
+//: sophisticated layer system I'd introduce the simpler version first and
+//: transform it in a separate layer or set of layers.
+
+:(before "End Checks")
+Transform.push_back(check_instruction);  // idempotent
+
+:(code)
+void check_instruction(const recipe_ordinal r) {
+  trace(101, "transform") << "--- perform checks for recipe " << get(Recipe, r).name << end();
+  map<string, vector<type_ordinal> > metadata;
+  for (int i = 0;  i < SIZE(get(Recipe, r).steps);  ++i) {
+    instruction& inst = get(Recipe, r).steps.at(i);
+    if (inst.is_label) continue;
+    switch (inst.operation) {
+      // Primitive Recipe Checks
+      case COPY: {
+        if (SIZE(inst.products) > SIZE(inst.ingredients)) {
+          raise << maybe(get(Recipe, r).name) << "too many products in '" << to_original_string(inst) << "'\n" << end();
+          break;
+        }
+        for (int i = 0;  i < SIZE(inst.products);  ++i) {
+          if (!types_coercible(inst.products.at(i), inst.ingredients.at(i))) {
+            raise << maybe(get(Recipe, r).name) << "can't copy '" << inst.ingredients.at(i).original_string << "' to '" << inst.products.at(i).original_string << "'; types don't match\n" << end();
+            goto finish_checking_instruction;
+          }
+        }
+        break;
+      }
+      // End Primitive Recipe Checks
+      default: {
+        // Defined Recipe Checks
+        // End Defined Recipe Checks
+      }
+    }
+    finish_checking_instruction:;
+  }
+}
+
+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
+bool types_coercible(reagent/*copy*/ to, reagent/*copy*/ from) {
+  // Begin types_coercible(reagent to, reagent from)
+  if (types_match_sub(to, from)) return true;
+  if (is_real_mu_number(from) && is_mu_character(to)) return true;
+  // End types_coercible Special-cases
+  return false;
+}
+
+bool types_match_sub(const reagent& to, const reagent& from) {
+  // to sidestep type-checking, use /unsafe in the source.
+  // this will be highlighted in red inside vim. just for setting up some tests.
+  if (is_unsafe(from)) return true;
+  if (is_literal(from)) {
+    if (is_mu_array(to)) return false;
+    // End Matching Types For Literal(to)
+    if (!to.type) return false;
+    // allow writing null to any address
+    if (is_mu_address(to)) return from.name == "null";
+    return size_of(to) == 1;  // literals are always scalars
+  }
+  return types_strictly_match_sub(to, from);
+}
+// variant for others to call
+bool types_match(reagent/*copy*/ to, reagent/*copy*/ from) {
+  // Begin types_match(reagent to, reagent from)
+  return types_match_sub(to, from);
+}
+
+//: copy arguments for later layers
+bool types_strictly_match_sub(const reagent& to, const reagent& from) {
+  if (to.type == NULL) return false;  // error
+  if (is_literal(from) && to.type->value == Number_type_ordinal) return true;
+  // to sidestep type-checking, use /unsafe in the source.
+  // this will be highlighted in red inside vim. just for setting up some tests.
+  if (is_unsafe(from)) return true;
+  // '_' never raises type error
+  if (is_dummy(to)) return true;
+  if (!to.type) return !from.type;
+  return types_strictly_match(to.type, from.type);
+}
+// variant for others to call
+bool types_strictly_match(reagent/*copy*/ to, reagent/*copy*/ from) {
+  // Begin types_strictly_match(reagent to, reagent from)
+  return types_strictly_match_sub(to, from);
+}
+
+bool types_strictly_match(const type_tree* to, const type_tree* from) {
+  if (from == to) return true;
+  if (!to) return false;
+  if (!from) return to->atom && to->value == 0;
+  if (from->atom != to->atom) return false;
+  if (from->atom) {
+    if (from->value == -1) return from->name == to->name;
+    return from->value == to->value;
+  }
+  if (types_strictly_match(to->left, from->left) && types_strictly_match(to->right, from->right))
+    return true;
+  // fallback: (x) == x
+  if (to->right == NULL && types_strictly_match(to->left, from)) return true;
+  if (from->right == NULL && types_strictly_match(to, from->left)) return true;
+  return false;
+}
+
+void test_unknown_type_does_not_match_unknown_type() {
+  reagent a("a:foo");
+  reagent b("b:bar");
+  CHECK(!types_strictly_match(a, b));
+}
+
+void test_unknown_type_matches_itself() {
+  reagent a("a:foo");
+  reagent b("b:foo");
+  CHECK(types_strictly_match(a, b));
+}
+
+void test_type_abbreviations_match_raw_types() {
+  put(Type_abbreviations, "text", new_type_tree("address:array:character"));
+  // a has type (address buffer (address array character))
+  reagent a("a:address:buffer:text");
+  expand_type_abbreviations(a.type);
+  // b has type (address buffer address array character)
+  reagent b("b:address:buffer:address:array:character");
+  CHECK(types_strictly_match(a, b));
+  delete Type_abbreviations["text"];
+  put(Type_abbreviations, "text", NULL);
+}
+
+//: helpers
+
+bool is_unsafe(const reagent& r) {
+  return has_property(r, "unsafe");
+}
+
+bool is_mu_array(reagent/*copy*/ r) {
+  // End Preprocess is_mu_array(reagent r)
+  return is_mu_array(r.type);
+}
+bool is_mu_array(const type_tree* type) {
+  if (!type) return false;
+  if (is_literal(type)) return false;
+  if (type->atom) return false;
+  if (!type->left->atom) {
+    raise << "invalid type " << to_string(type) << '\n' << end();
+    return false;
+  }
+  return type->left->value == Array_type_ordinal;
+}
+
+bool is_mu_boolean(reagent/*copy*/ r) {
+  // End Preprocess is_mu_boolean(reagent r)
+  if (!r.type) return false;
+  if (is_literal(r)) return false;
+  if (!r.type->atom) return false;
+  return r.type->value == Boolean_type_ordinal;
+}
+
+bool is_mu_number(reagent/*copy*/ r) {
+  if (is_mu_character(r.type)) return true;  // permit arithmetic on unicode code points
+  return is_real_mu_number(r);
+}
+
+bool is_real_mu_number(reagent/*copy*/ r) {
+  // End Preprocess is_mu_number(reagent r)
+  if (!r.type) return false;
+  if (!r.type->atom) return false;
+  if (is_literal(r)) {
+    return r.type->name == "literal-fractional-number"
+        || r.type->name == "literal";
+  }
+  return r.type->value == Number_type_ordinal;
+}
+
+bool is_mu_character(reagent/*copy*/ r) {
+  // End Preprocess is_mu_character(reagent r)
+  return is_mu_character(r.type);
+}
+bool is_mu_character(const type_tree* type) {
+  if (!type) return false;
+  if (!type->atom) return false;
+  if (is_literal(type)) return false;
+  return type->value == Character_type_ordinal;
+}
diff --git a/archive/2.vm/022arithmetic.cc b/archive/2.vm/022arithmetic.cc
new file mode 100644
index 00000000..05e79e3b
--- /dev/null
+++ b/archive/2.vm/022arithmetic.cc
@@ -0,0 +1,1071 @@
+//: Arithmetic primitives
+
+:(before "End Primitive Recipe Declarations")
+ADD,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "add", ADD);
+:(before "End Primitive Recipe Checks")
+case ADD: {
+  // primary goal of these checks is to forbid address arithmetic
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'add' requires number ingredients, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'add' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'add' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case ADD: {
+  double result = 0;
+  for (int i = 0;  i < SIZE(ingredients);  ++i) {
+    result += ingredients.at(i).at(0);
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "subtract", SUBTRACT);
+:(before "End Primitive Recipe Checks")
+case SUBTRACT: {
+  if (inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'subtract' has no ingredients\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'subtract' requires number ingredients, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'subtract' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'subtract' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SUBTRACT: {
+  double result = ingredients.at(0).at(0);
+  for (int i = 1;  i < SIZE(ingredients);  ++i)
+    result -= ingredients.at(i).at(0);
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "multiply", MULTIPLY);
+:(before "End Primitive Recipe Checks")
+case MULTIPLY: {
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'multiply' requires number ingredients, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'multiply' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'multiply' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MULTIPLY: {
+  double result = 1;
+  for (int i = 0;  i < SIZE(ingredients);  ++i) {
+    result *= ingredients.at(i).at(0);
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "divide", DIVIDE);
+:(before "End Primitive Recipe Checks")
+case DIVIDE: {
+  if (inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'divide' has no ingredients\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'divide' requires number ingredients, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'divide' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'divide' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case DIVIDE: {
+  double result = ingredients.at(0).at(0);
+  for (int i = 1;  i < SIZE(ingredients);  ++i)
+    result /= ingredients.at(i).at(0);
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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
+
+:(before "End Primitive Recipe Declarations")
+DIVIDE_WITH_REMAINDER,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "divide-with-remainder", DIVIDE_WITH_REMAINDER);
+:(before "End Primitive Recipe Checks")
+case DIVIDE_WITH_REMAINDER: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'divide-with-remainder' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'divide-with-remainder' requires number ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) > 2) {
+    raise << maybe(get(Recipe, r).name) << "'divide-with-remainder' yields two products in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.products);  ++i) {
+    if (!is_dummy(inst.products.at(i)) && !is_mu_number(inst.products.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'divide-with-remainder' should yield a number, but got '" << inst.products.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case DIVIDE_WITH_REMAINDER: {
+  products.resize(2);
+  // fractions will be dropped; very large numbers will overflow
+  long long int a = static_cast<long long int>(ingredients.at(0).at(0));
+  long long int b = static_cast<long long int>(ingredients.at(1).at(0));
+  if (b == 0) {
+    raise << maybe(current_recipe_name()) << "divide by zero in '" << to_original_string(current_instruction()) << "'\n" << end();
+    products.resize(2);
+    products.at(0).push_back(0);
+    products.at(1).push_back(0);
+    break;
+  }
+  long long int quotient = a / b;
+  long long int remainder = a % b;
+  products.at(0).push_back(static_cast<double>(quotient));
+  products.at(1).push_back(static_cast<double>(remainder));
+  break;
+}
+
+:(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
+
+:(before "End Primitive Recipe Declarations")
+SHIFT_LEFT,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "shift-left", SHIFT_LEFT);
+:(before "End Primitive Recipe Checks")
+case SHIFT_LEFT: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'shift-left' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'shift-left' requires number ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'shift-left' yields one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'shift-left' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    goto finish_checking_instruction;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SHIFT_LEFT: {
+  // ingredients must be integers
+  int a = static_cast<int>(ingredients.at(0).at(0));
+  int b = static_cast<int>(ingredients.at(1).at(0));
+  products.resize(1);
+  if (b < 0) {
+    raise << maybe(current_recipe_name()) << "second ingredient can't be negative in '" << to_original_string(current_instruction()) << "'\n" << end();
+    products.at(0).push_back(0);
+    break;
+  }
+  products.at(0).push_back(a<<b);
+  break;
+}
+
+:(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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "shift-right", SHIFT_RIGHT);
+:(before "End Primitive Recipe Checks")
+case SHIFT_RIGHT: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'shift-right' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'shift-right' requires number ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'shift-right' yields one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'shift-right' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    goto finish_checking_instruction;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SHIFT_RIGHT: {
+  // ingredients must be integers
+  int a = static_cast<int>(ingredients.at(0).at(0));
+  int b = static_cast<int>(ingredients.at(1).at(0));
+  products.resize(1);
+  if (b < 0) {
+    raise << maybe(current_recipe_name()) << "second ingredient can't be negative in '" << to_original_string(current_instruction()) << "'\n" << end();
+    products.at(0).push_back(0);
+    break;
+  }
+  products.at(0).push_back(a>>b);
+  break;
+}
+
+:(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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "and-bits", AND_BITS);
+:(before "End Primitive Recipe Checks")
+case AND_BITS: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'and-bits' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'and-bits' requires number ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'and-bits' yields one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'and-bits' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    goto finish_checking_instruction;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case AND_BITS: {
+  // ingredients must be integers
+  int a = static_cast<int>(ingredients.at(0).at(0));
+  int b = static_cast<int>(ingredients.at(1).at(0));
+  products.resize(1);
+  products.at(0).push_back(a&b);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "or-bits", OR_BITS);
+:(before "End Primitive Recipe Checks")
+case OR_BITS: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'or-bits' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'or-bits' requires number ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'or-bits' yields one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'or-bits' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    goto finish_checking_instruction;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case OR_BITS: {
+  // ingredients must be integers
+  int a = static_cast<int>(ingredients.at(0).at(0));
+  int b = static_cast<int>(ingredients.at(1).at(0));
+  products.resize(1);
+  products.at(0).push_back(a|b);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "xor-bits", XOR_BITS);
+:(before "End Primitive Recipe Checks")
+case XOR_BITS: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'xor-bits' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0)) || !is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'xor-bits' requires number ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'xor-bits' yields one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'xor-bits' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    goto finish_checking_instruction;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case XOR_BITS: {
+  // ingredients must be integers
+  int a = static_cast<int>(ingredients.at(0).at(0));
+  int b = static_cast<int>(ingredients.at(1).at(0));
+  products.resize(1);
+  products.at(0).push_back(a^b);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "flip-bits", FLIP_BITS);
+:(before "End Primitive Recipe Checks")
+case FLIP_BITS: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'flip-bits' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'flip-bits' requires a number ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'flip-bits' yields one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'flip-bits' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    goto finish_checking_instruction;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case FLIP_BITS: {
+  // ingredient must be integer
+  int a = static_cast<int>(ingredients.at(0).at(0));
+  products.resize(1);
+  products.at(0).push_back(~a);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "round", ROUND);
+:(before "End Primitive Recipe Checks")
+case ROUND: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'round' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'round' should be a number, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case ROUND: {
+  products.resize(1);
+  products.at(0).push_back(rint(ingredients.at(0).at(0)));
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "truncate", TRUNCATE);
+:(before "End Primitive Recipe Checks")
+case TRUNCATE: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'truncate' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'truncate' should be a number, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case TRUNCATE: {
+  products.resize(1);
+  products.at(0).push_back(trunc(ingredients.at(0).at(0)));
+  break;
+}
+
+:(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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "square-root", SQUARE_ROOT);
+:(before "End Primitive Recipe Checks")
+case SQUARE_ROOT: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'square-root' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'square-root' should be a number, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SQUARE_ROOT: {
+  products.resize(1);
+  products.at(0).push_back(sqrt(ingredients.at(0).at(0)));
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CHARACTER_TO_CODE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "character-to-code", CHARACTER_TO_CODE);
+:(before "End Primitive Recipe Checks")
+case CHARACTER_TO_CODE: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'character-to-code' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_character(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'character-to-code' should be a character, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'character-to-code' requires exactly one product, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first product of 'character-to-code' should be a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CHARACTER_TO_CODE: {
+  double result = 0;
+  for (int i = 0;  i < SIZE(ingredients);  ++i) {
+    result += ingredients.at(i).at(0);
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(before "End Includes")
+#include <math.h>
diff --git a/archive/2.vm/023boolean.cc b/archive/2.vm/023boolean.cc
new file mode 100644
index 00000000..c6c5cac7
--- /dev/null
+++ b/archive/2.vm/023boolean.cc
@@ -0,0 +1,224 @@
+//: Boolean primitives
+
+:(before "End Primitive Recipe Declarations")
+AND,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "and", AND);
+:(before "End Primitive Recipe Checks")
+case AND: {
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_scalar(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'and' requires boolean ingredients, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'and' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_boolean(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'and' should yield a boolean, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case AND: {
+  bool result = true;
+  for (int i = 0;  i < SIZE(ingredients);  ++i)
+    result = result && scalar_ingredient(ingredients, i);
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+:(code)
+double scalar_ingredient(const vector<vector<double> >& ingredients, int i) {
+  if (is_mu_address(current_instruction().ingredients.at(i)))
+    return ingredients.at(i).at(/*skip alloc id*/1);
+  return ingredients.at(i).at(0);
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "or", OR);
+:(before "End Primitive Recipe Checks")
+case OR: {
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_scalar(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'and' requires boolean ingredients, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'or' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_boolean(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'or' should yield a boolean, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case OR: {
+  bool result = false;
+  for (int i = 0;  i < SIZE(ingredients);  ++i)
+    result = result || scalar_ingredient(ingredients, i);
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "not", NOT);
+:(before "End Primitive Recipe Checks")
+case NOT: {
+  if (SIZE(inst.products) != SIZE(inst.ingredients)) {
+    raise << "ingredients and products should match in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_scalar(inst.ingredients.at(i)) && !is_mu_address(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'not' requires ingredients that can be interpreted as boolean, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  for (int i = 0;  i < SIZE(inst.products);  ++i) {
+    if (is_dummy(inst.products.at(i))) continue;
+    if (!is_mu_boolean(inst.products.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'not' should yield a boolean, but got '" << inst.products.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case NOT: {
+  products.resize(SIZE(ingredients));
+  for (int i = 0;  i < SIZE(ingredients);  ++i) {
+    products.at(i).push_back(!scalar_ingredient(ingredients, i));
+  }
+  break;
+}
+
+:(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"
+  );
+}
+
+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/archive/2.vm/024jump.cc b/archive/2.vm/024jump.cc
new file mode 100644
index 00000000..66e238e6
--- /dev/null
+++ b/archive/2.vm/024jump.cc
@@ -0,0 +1,237 @@
+//: Jump primitives
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "jump", JUMP);
+:(before "End Primitive Recipe Checks")
+case JUMP: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' should get exactly one ingredient\n" << end();
+    break;
+  }
+  if (!is_literal(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '" << to_original_string(inst) << "' should be a label or offset, but '" << inst.ingredients.at(0).name << "' has type '" << names_to_string_without_quotes(inst.ingredients.at(0).type) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'jump' instructions write no products\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case JUMP: {
+  assert(current_instruction().ingredients.at(0).initialized);
+  current_step_index() += ingredients.at(0).at(0)+1;
+  trace(Callstack_depth+1, "run") << "jumping to instruction " << current_step_index() << end();
+  // skip rest of this instruction
+  write_products = false;
+  fall_through_to_next_instruction = false;
+  break;
+}
+
+//: special type to designate jump targets
+:(before "End Mu Types Initialization")
+put(Type_ordinal, "offset", 0);
+
+:(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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "jump-if", JUMP_IF);
+:(before "End Primitive Recipe Checks")
+case JUMP_IF: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' should get exactly two ingredients\n" << end();
+    break;
+  }
+  if (!is_mu_address(inst.ingredients.at(0)) && !is_mu_scalar(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' requires a boolean for its first ingredient, but '" << inst.ingredients.at(0).name << "' has type '" << names_to_string_without_quotes(inst.ingredients.at(0).type) << "'\n" << end();
+    break;
+  }
+  if (!is_literal(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' requires a label or offset for its second ingredient, but '" << inst.ingredients.at(1).name << "' has type '" << names_to_string_without_quotes(inst.ingredients.at(1).type) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'jump-if' instructions write no products\n" << end();
+    break;
+  }
+  // End JUMP_IF Checks
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case JUMP_IF: {
+  assert(current_instruction().ingredients.at(1).initialized);
+  if (!scalar_ingredient(ingredients, 0)) {
+    trace(Callstack_depth+1, "run") << "jump-if fell through" << end();
+    break;
+  }
+  current_step_index() += ingredients.at(1).at(0)+1;
+  trace(Callstack_depth+1, "run") << "jumping to instruction " << current_step_index() << end();
+  // skip rest of this instruction
+  write_products = false;
+  fall_through_to_next_instruction = false;
+  break;
+}
+
+:(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");
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "jump-unless", JUMP_UNLESS);
+:(before "End Primitive Recipe Checks")
+case JUMP_UNLESS: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' should get exactly two ingredients\n" << end();
+    break;
+  }
+  if (!is_mu_address(inst.ingredients.at(0)) && !is_mu_scalar(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' requires a boolean for its first ingredient, but '" << inst.ingredients.at(0).name << "' has type '" << names_to_string_without_quotes(inst.ingredients.at(0).type) << "'\n" << end();
+    break;
+  }
+  if (!is_literal(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' requires a label or offset for its second ingredient, but '" << inst.ingredients.at(1).name << "' has type '" << names_to_string_without_quotes(inst.ingredients.at(1).type) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'jump' instructions write no products\n" << end();
+    break;
+  }
+  // End JUMP_UNLESS Checks
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case JUMP_UNLESS: {
+  assert(current_instruction().ingredients.at(1).initialized);
+  if (scalar_ingredient(ingredients, 0)) {
+    trace(Callstack_depth+1, "run") << "jump-unless fell through" << end();
+    break;
+  }
+  current_step_index() += ingredients.at(1).at(0)+1;
+  trace(Callstack_depth+1, "run") << "jumping to instruction " << current_step_index() << end();
+  // skip rest of this instruction
+  write_products = false;
+  fall_through_to_next_instruction = false;
+  break;
+}
+
+:(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");
+}
+
+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/archive/2.vm/025compare.cc b/archive/2.vm/025compare.cc
new file mode 100644
index 00000000..bade3c9c
--- /dev/null
+++ b/archive/2.vm/025compare.cc
@@ -0,0 +1,624 @@
+//: Comparison primitives
+
+:(before "End Primitive Recipe Declarations")
+EQUAL,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "equal", EQUAL);
+:(before "End Primitive Recipe Checks")
+case EQUAL: {
+  if (SIZE(inst.ingredients) <= 1) {
+    raise << maybe(get(Recipe, r).name) << "'equal' needs at least two ingredients to compare in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  const reagent& exemplar = inst.ingredients.at(0);
+  for (int i = /*skip exemplar*/1;  i < SIZE(inst.ingredients);  ++i) {
+    if (!types_match(inst.ingredients.at(i), exemplar) && !types_match(exemplar, inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'equal' expects ingredients to be all of the same type, but got '" << to_original_string(inst) << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'equal' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_boolean(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'equal' should yield a boolean, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case EQUAL: {
+  vector<double>& exemplar = ingredients.at(0);
+  bool result = true;
+  for (int i = /*skip exemplar*/1;  i < SIZE(ingredients);  ++i) {
+    if (SIZE(ingredients.at(i)) != SIZE(exemplar)) {
+      result = false;
+      break;
+    }
+    if (!equal(ingredients.at(i).begin(), ingredients.at(i).end(), exemplar.begin())) {
+      result = false;
+      break;
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "not-equal", NOT_EQUAL);
+:(before "End Primitive Recipe Checks")
+case NOT_EQUAL: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'equal' needs two ingredients to compare in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  const reagent& exemplar = inst.ingredients.at(0);
+  if (!types_match(inst.ingredients.at(1), exemplar) && !types_match(exemplar, inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'equal' expects ingredients to be all of the same type, but got '" << to_original_string(inst) << "'\n" << end();
+    goto finish_checking_instruction;
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'equal' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_boolean(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'equal' should yield a boolean, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case NOT_EQUAL: {
+  vector<double>& exemplar = ingredients.at(0);
+  products.resize(1);
+  if (SIZE(ingredients.at(1)) != SIZE(exemplar)) {
+    products.at(0).push_back(true);
+    break;
+  }
+  bool equal_ingredients = equal(ingredients.at(1).begin(), ingredients.at(1).end(), exemplar.begin());
+  products.at(0).push_back(!equal_ingredients);
+  break;
+}
+
+:(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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "greater-than", GREATER_THAN);
+:(before "End Primitive Recipe Checks")
+case GREATER_THAN: {
+  if (SIZE(inst.ingredients) <= 1) {
+    raise << maybe(get(Recipe, r).name) << "'greater-than' needs at least two ingredients to compare in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'greater-than' can only compare numbers; got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'greater-than' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_boolean(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'greater-than' should yield a boolean, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case GREATER_THAN: {
+  bool result = true;
+  for (int i = /**/1;  i < SIZE(ingredients);  ++i) {
+    if (ingredients.at(i-1).at(0) <= ingredients.at(i).at(0)) {
+      result = false;
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "lesser-than", LESSER_THAN);
+:(before "End Primitive Recipe Checks")
+case LESSER_THAN: {
+  if (SIZE(inst.ingredients) <= 1) {
+    raise << maybe(get(Recipe, r).name) << "'lesser-than' needs at least two ingredients to compare in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'lesser-than' can only compare numbers; got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'lesser-than' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_boolean(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'lesser-than' should yield a boolean, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case LESSER_THAN: {
+  bool result = true;
+  for (int i = /**/1;  i < SIZE(ingredients);  ++i) {
+    if (ingredients.at(i-1).at(0) >= ingredients.at(i).at(0)) {
+      result = false;
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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")
+put(Recipe_ordinal, "greater-or-equal", GREATER_OR_EQUAL);
+:(before "End Primitive Recipe Checks")
+case GREATER_OR_EQUAL: {
+  if (SIZE(inst.ingredients) <= 1) {
+    raise << maybe(get(Recipe, r).name) << "'greater-or-equal' needs at least two ingredients to compare in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'greater-or-equal' can only compare numbers; got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'greater-or-equal' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_boolean(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'greater-or-equal' should yield a boolean, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case GREATER_OR_EQUAL: {
+  bool result = true;
+  for (int i = /**/1;  i < SIZE(ingredients);  ++i) {
+    if (ingredients.at(i-1).at(0) < ingredients.at(i).at(0)) {
+      result = false;
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "lesser-or-equal", LESSER_OR_EQUAL);
+:(before "End Primitive Recipe Checks")
+case LESSER_OR_EQUAL: {
+  if (SIZE(inst.ingredients) <= 1) {
+    raise << maybe(get(Recipe, r).name) << "'lesser-or-equal' needs at least two ingredients to compare in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'lesser-or-equal' can only compare numbers; got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'greater-or-equal' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_boolean(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'greater-or-equal' should yield a boolean, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case LESSER_OR_EQUAL: {
+  bool result = true;
+  for (int i = /**/1;  i < SIZE(ingredients);  ++i) {
+    if (ingredients.at(i-1).at(0) > ingredients.at(i).at(0)) {
+      result = false;
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "max", MAX);
+:(before "End Primitive Recipe Checks")
+case MAX: {
+  if (SIZE(inst.ingredients) <= 1) {
+    raise << maybe(get(Recipe, r).name) << "'max' needs at least two ingredients to compare in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'max' can only compare numbers; got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'max' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'max' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MAX: {
+  int result = ingredients.at(0).at(0);
+  for (int i = /**/1;  i < SIZE(ingredients);  ++i) {
+    if (ingredients.at(i).at(0) > result) {
+      result = ingredients.at(i).at(0);
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+MIN,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "min", MIN);
+:(before "End Primitive Recipe Checks")
+case MIN: {
+  if (SIZE(inst.ingredients) <= 1) {
+    raise << maybe(get(Recipe, r).name) << "'min' needs at least two ingredients to compare in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_number(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'min' can only compare numbers; got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > 1) {
+    raise << maybe(get(Recipe, r).name) << "'min' yields exactly one product in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty() && !is_dummy(inst.products.at(0)) && !is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'min' should yield a number, but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MIN: {
+  int result = ingredients.at(0).at(0);
+  for (int i = /**/1;  i < SIZE(ingredients);  ++i) {
+    if (ingredients.at(i).at(0) < result) {
+      result = ingredients.at(i).at(0);
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
diff --git a/archive/2.vm/026call.cc b/archive/2.vm/026call.cc
new file mode 100644
index 00000000..faa9455d
--- /dev/null
+++ b/archive/2.vm/026call.cc
@@ -0,0 +1,246 @@
+//: So far the recipes we define can't run each other. Let's fix that.
+
+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
+// recipe. When that finishes, we continue this one where we left off.
+// This requires maintaining a 'stack' of interrupted recipes or 'calls'.
+struct call {
+  recipe_ordinal running_recipe;
+  int running_step_index;
+  // End call Fields
+  call(recipe_ordinal r) { clear(r, 0); }
+  call(recipe_ordinal r, int index) { clear(r, index); }
+  void clear(recipe_ordinal r, int index) {
+    running_recipe = r;
+    running_step_index = index;
+    // End call Constructor
+  }
+  ~call() {
+    // End call Destructor
+  }
+};
+typedef list<call> call_stack;
+
+:(replace{} "struct routine")
+struct routine {
+  call_stack calls;
+  // End routine Fields
+  routine(recipe_ordinal r);
+  bool completed() const;
+  const vector<instruction>& steps() const;
+};
+:(code)
+routine::routine(recipe_ordinal r) {
+  ++Callstack_depth;
+  trace(Callstack_depth+1, "trace") << "new routine; incrementing callstack depth to " << Callstack_depth << end();
+  assert(Callstack_depth < Max_depth);
+  calls.push_front(call(r));
+  // End routine Constructor
+}
+
+//:: now update routine's helpers
+
+//: macro versions for a slight speedup
+
+:(delete{} "int& current_step_index()")
+:(delete{} "recipe_ordinal currently_running_recipe()")
+:(delete{} "const string& current_recipe_name()")
+:(delete{} "const recipe& current_recipe()")
+:(delete{} "const instruction& current_instruction()")
+
+:(before "End Includes")
+#define current_call() Current_routine->calls.front()
+#define current_step_index() current_call().running_step_index
+#define currently_running_recipe() current_call().running_recipe
+#define current_recipe() get(Recipe, currently_running_recipe())
+#define current_recipe_name() current_recipe().name
+#define to_instruction(call) get(Recipe, (call).running_recipe).steps.at((call).running_step_index)
+#define current_instruction() to_instruction(current_call())
+
+//: function versions for debugging
+
+:(code)
+//? :(before "End Globals")
+//? bool Foo2 = false;
+//? :(code)
+//? call& current_call() {
+//?   if (Foo2) cerr << __FUNCTION__ << '\n';
+//?   return Current_routine->calls.front();
+//? }
+//? :(replace{} "int& current_step_index()")
+//? int& current_step_index() {
+//?   assert(!Current_routine->calls.empty());
+//?   if (Foo2) cerr << __FUNCTION__ << '\n';
+//?   return current_call().running_step_index;
+//? }
+//? :(replace{} "recipe_ordinal currently_running_recipe()")
+//? recipe_ordinal currently_running_recipe() {
+//?   assert(!Current_routine->calls.empty());
+//?   if (Foo2) cerr << __FUNCTION__ << '\n';
+//?   return current_call().running_recipe;
+//? }
+//? :(replace{} "const string& current_recipe_name()")
+//? const string& current_recipe_name() {
+//?   assert(!Current_routine->calls.empty());
+//?   if (Foo2) cerr << __FUNCTION__ << '\n';
+//?   return get(Recipe, current_call().running_recipe).name;
+//? }
+//? :(replace{} "const recipe& current_recipe()")
+//? const recipe& current_recipe() {
+//?   assert(!Current_routine->calls.empty());
+//?   if (Foo2) cerr << __FUNCTION__ << '\n';
+//?   return get(Recipe, current_call().running_recipe);
+//? }
+//? :(replace{} "const instruction& current_instruction()")
+//? const instruction& current_instruction() {
+//?   assert(!Current_routine->calls.empty());
+//?   if (Foo2) cerr << __FUNCTION__ << '\n';
+//?   return to_instruction(current_call());
+//? }
+//? :(code)
+//? const instruction& to_instruction(const call& call) {
+//?   return get(Recipe, call.running_recipe).steps.at(call.running_step_index);
+//? }
+
+:(code)
+void dump_callstack() {
+  if (!Current_routine) return;
+  if (Current_routine->calls.size() <= 1) return;
+  for (call_stack::const_iterator p = ++Current_routine->calls.begin();  p != Current_routine->calls.end();  ++p)
+    raise << "  called from " << get(Recipe, p->running_recipe).name << ": " << to_original_string(to_instruction(*p)) << '\n' << end();
+}
+
+:(after "Defined Recipe Checks")
+// not a primitive; check that it's present in the book of recipes
+if (!contains_key(Recipe, inst.operation)) {
+  raise << maybe(get(Recipe, r).name) << "undefined operation in '" << to_original_string(inst) << "'\n" << end();
+  break;
+}
+:(replace{} "default:" following "End Primitive Recipe Implementations")
+default: {
+  if (contains_key(Recipe, current_instruction().operation)) {  // error already raised in Checks above
+    // not a primitive; look up the book of recipes
+    ++Callstack_depth;
+    trace(Callstack_depth+1, "trace") << "incrementing callstack depth to " << Callstack_depth << end();
+    assert(Callstack_depth < Max_depth);
+    const call& caller_frame = current_call();
+    Current_routine->calls.push_front(call(to_instruction(caller_frame).operation));
+    finish_call_housekeeping(to_instruction(caller_frame), ingredients);
+    // not done with caller
+    write_products = false;
+    fall_through_to_next_instruction = false;
+    // End Non-primitive Call(caller_frame)
+  }
+}
+:(code)
+void finish_call_housekeeping(const instruction& call_instruction, const vector<vector<double> >& ingredients) {
+  // End Call Housekeeping
+}
+
+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
+
+:(replace{} "bool routine::completed() const")
+bool routine::completed() const {
+  return calls.empty();
+}
+
+:(replace{} "const vector<instruction>& routine::steps() const")
+const vector<instruction>& routine::steps() const {
+  assert(!calls.empty());
+  return get(Recipe, calls.front().running_recipe).steps;
+}
+
+:(after "Running One Instruction")
+// when we reach the end of one call, we may reach the end of the one below
+// it, and the one below that, and so on
+while (current_step_index() >= SIZE(Current_routine->steps())) {
+  // Falling Through End Of Recipe
+  trace(Callstack_depth+1, "trace") << "fall-through: exiting " << current_recipe_name() << "; decrementing callstack depth from " << Callstack_depth << end();
+  --Callstack_depth;
+  assert(Callstack_depth >= 0);
+  Current_routine->calls.pop_front();
+  if (Current_routine->calls.empty()) goto stop_running_current_routine;
+  // Complete Call Fallthrough
+  // todo: fail if no products returned
+  ++current_step_index();
+}
+
+:(before "End Primitive Recipe Declarations")
+_DUMP_CALL_STACK,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$dump-call-stack", _DUMP_CALL_STACK);
+:(before "End Primitive Recipe Checks")
+case _DUMP_CALL_STACK: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _DUMP_CALL_STACK: {
+  dump(Current_routine->calls);
+  break;
+}
+:(code)
+void dump(const call_stack& calls) {
+  for (call_stack::const_reverse_iterator p = calls.rbegin(); p != calls.rend(); ++p)
+    cerr << get(Recipe, p->running_recipe).name << ":" << p->running_step_index << " -- " << to_string(to_instruction(*p)) << '\n';
+}
diff --git a/archive/2.vm/027call_ingredient.cc b/archive/2.vm/027call_ingredient.cc
new file mode 100644
index 00000000..659f644d
--- /dev/null
+++ b/archive/2.vm/027call_ingredient.cc
@@ -0,0 +1,220 @@
+//: Calls can take ingredients just like primitives. To access a recipe's
+//: ingredients, use 'next-ingredient'.
+
+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"
+  );
+}
+
+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;
+vector<reagent> ingredients;
+int next_ingredient_to_process;
+:(before "End call Constructor")
+next_ingredient_to_process = 0;
+
+:(before "End Call Housekeeping")
+for (int i = 0;  i < SIZE(ingredients);  ++i) {
+  current_call().ingredient_atoms.push_back(ingredients.at(i));
+  reagent/*copy*/ ingredient = call_instruction.ingredients.at(i);
+  // End Compute Call Ingredient
+  current_call().ingredients.push_back(ingredient);
+  // End Populate Call Ingredient
+}
+
+:(before "End Primitive Recipe Declarations")
+NEXT_INGREDIENT,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "next-ingredient", NEXT_INGREDIENT);
+put(Recipe_ordinal, "next-input", NEXT_INGREDIENT);
+:(before "End Primitive Recipe Checks")
+case NEXT_INGREDIENT: {
+  if (!inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'next-ingredient' didn't expect any ingredients in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case NEXT_INGREDIENT: {
+  assert(!Current_routine->calls.empty());
+  if (current_call().next_ingredient_to_process < SIZE(current_call().ingredient_atoms)) {
+    reagent/*copy*/ product = current_instruction().products.at(0);
+    // End Preprocess NEXT_INGREDIENT product
+    if (current_recipe_name() == "main") {
+      // no ingredient types since the call might be implicit; assume ingredients are always strings
+      // todo: how to test this?
+      if (!is_mu_text(product))
+        raise << "main: wrong type for ingredient '" << product.original_string << "'\n" << end();
+    }
+    else if (!types_coercible(product,
+                              current_call().ingredients.at(current_call().next_ingredient_to_process))) {
+      raise << maybe(current_recipe_name()) << "wrong type for ingredient '" << product.original_string << "': " << current_call().ingredients.at(current_call().next_ingredient_to_process).original_string << '\n' << end();
+      // End next-ingredient Type Mismatch Error
+    }
+    products.push_back(
+        current_call().ingredient_atoms.at(current_call().next_ingredient_to_process));
+    assert(SIZE(products) == 1);  products.resize(2);  // push a new vector
+    products.at(1).push_back(1);
+    ++current_call().next_ingredient_to_process;
+  }
+  else {
+    if (SIZE(current_instruction().products) < 2)
+      raise << maybe(current_recipe_name()) << "no ingredient to save in '" << current_instruction().products.at(0).original_string << "'\n" << end();
+    if (current_instruction().products.empty()) break;
+    products.resize(2);
+    // pad the first product with sufficient zeros to match its type
+    products.at(0).resize(size_of(current_instruction().products.at(0)));
+    products.at(1).push_back(0);
+  }
+  break;
+}
+
+:(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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "rewind-ingredients", REWIND_INGREDIENTS);
+put(Recipe_ordinal, "rewind-inputs", REWIND_INGREDIENTS);
+:(before "End Primitive Recipe Checks")
+case REWIND_INGREDIENTS: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case REWIND_INGREDIENTS: {
+  current_call().next_ingredient_to_process = 0;
+  break;
+}
+
+//: another primitive: 'ingredient' for random access
+
+:(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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "ingredient", INGREDIENT);
+put(Recipe_ordinal, "input", INGREDIENT);
+:(before "End Primitive Recipe Checks")
+case INGREDIENT: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'ingredient' expects exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_literal(inst.ingredients.at(0)) && !is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'ingredient' expects a literal ingredient, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case INGREDIENT: {
+  if (static_cast<int>(ingredients.at(0).at(0)) < SIZE(current_call().ingredient_atoms)) {
+    current_call().next_ingredient_to_process = ingredients.at(0).at(0);
+    products.push_back(
+        current_call().ingredient_atoms.at(current_call().next_ingredient_to_process));
+    assert(SIZE(products) == 1);  products.resize(2);  // push a new vector
+    products.at(1).push_back(1);
+    ++current_call().next_ingredient_to_process;
+  }
+  else {
+    if (SIZE(current_instruction().products) > 1) {
+      products.resize(2);
+      products.at(0).push_back(0);  // todo: will fail noisily if we try to read a compound value
+      products.at(1).push_back(0);
+    }
+  }
+  break;
+}
+
+//: a particularly common array type is the text, or address:array:character
+:(code)
+bool is_mu_text(reagent/*copy*/ x) {
+  // End Preprocess is_mu_text(reagent x)
+  return x.type
+      && !x.type->atom
+      && x.type->left->atom
+      && x.type->left->value == Address_type_ordinal
+      && x.type->right
+      && !x.type->right->atom
+      && x.type->right->left->atom
+      && x.type->right->left->value == Array_type_ordinal
+      && x.type->right->right
+      && !x.type->right->right->atom
+      && x.type->right->right->left->value == Character_type_ordinal
+      && x.type->right->right->right == NULL;
+}
diff --git a/archive/2.vm/028call_return.cc b/archive/2.vm/028call_return.cc
new file mode 100644
index 00000000..056db7b9
--- /dev/null
+++ b/archive/2.vm/028call_return.cc
@@ -0,0 +1,197 @@
+//: Calls can also generate products, using 'reply' or 'return'.
+
+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"
+  );
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "return", RETURN);
+put(Recipe_ordinal, "reply", RETURN);  // synonym while teaching
+put(Recipe_ordinal, "output", RETURN);  // experiment
+:(before "End Primitive Recipe Checks")
+case RETURN: {
+  break;  // checks will be performed by a transform below
+}
+:(before "End Primitive Recipe Implementations")
+case RETURN: {
+  // Begin Return
+  trace(Callstack_depth+1, "trace") << current_instruction().name << ": decrementing callstack depth from " << Callstack_depth << end();
+  --Callstack_depth;
+  if (Callstack_depth < 0) {
+    Current_routine->calls.clear();
+    goto stop_running_current_routine;
+  }
+  Current_routine->calls.pop_front();
+  // just in case 'main' returns a value, drop it for now
+  if (Current_routine->calls.empty()) goto stop_running_current_routine;
+  for (int i = 0;  i < SIZE(ingredients);  ++i)
+    trace(Callstack_depth+1, "run") << "result " << i << " is " << to_string(ingredients.at(i)) << end();
+  // make return products available to caller
+  copy(ingredients.begin(), ingredients.end(), inserter(products, products.begin()));
+  // End Return
+  break;  // continue to process rest of *caller* instruction
+}
+
+//: Types in return instructions are checked ahead of time.
+
+:(before "End Checks")
+Transform.push_back(check_types_of_return_instructions);  // idempotent
+:(code)
+void check_types_of_return_instructions(const recipe_ordinal r) {
+  const recipe& caller = get(Recipe, r);
+  trace(9991, "transform") << "--- check types of return instructions in recipe " << caller.name << end();
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    const instruction& caller_instruction = caller.steps.at(i);
+    if (caller_instruction.is_label) continue;
+    if (caller_instruction.products.empty()) continue;
+    if (is_primitive(caller_instruction.operation)) continue;
+    const recipe& callee = get(Recipe, caller_instruction.operation);
+    for (int i = 0;  i < SIZE(callee.steps);  ++i) {
+      const instruction& return_inst = callee.steps.at(i);
+      if (return_inst.operation != RETURN) continue;
+      // check types with the caller
+      if (SIZE(caller_instruction.products) > SIZE(return_inst.ingredients)) {
+        raise << maybe(caller.name) << "too few values returned from " << callee.name << '\n' << end();
+        break;
+      }
+      for (int i = 0;  i < SIZE(caller_instruction.products);  ++i) {
+        reagent/*copy*/ lhs = return_inst.ingredients.at(i);
+        reagent/*copy*/ rhs = caller_instruction.products.at(i);
+        // End Check RETURN Copy(lhs, rhs)
+        if (!types_coercible(rhs, lhs)) {
+          raise << maybe(callee.name) << return_inst.name << " ingredient '" << lhs.original_string << "' can't be saved in '" << rhs.original_string << "'\n" << end();
+          raise << "  ['" << to_string(lhs.type) << "' vs '" << to_string(rhs.type) << "']\n" << end();
+          goto finish_return_check;
+        }
+      }
+      // check that any return ingredients with /same-as-ingredient connect up
+      // the corresponding ingredient and product in the caller.
+      for (int i = 0;  i < SIZE(caller_instruction.products);  ++i) {
+        if (has_property(return_inst.ingredients.at(i), "same-as-ingredient")) {
+          string_tree* tmp = property(return_inst.ingredients.at(i), "same-as-ingredient");
+          if (!tmp || !tmp->atom) {
+            raise << maybe(caller.name) << "'same-as-ingredient' metadata should take exactly one value in '" << to_original_string(return_inst) << "'\n" << end();
+            goto finish_return_check;
+          }
+          int ingredient_index = to_integer(tmp->value);
+          if (ingredient_index >= SIZE(caller_instruction.ingredients)) {
+            raise << maybe(caller.name) << "too few ingredients in '" << to_original_string(caller_instruction) << "'\n" << end();
+            goto finish_return_check;
+          }
+          if (!is_dummy(caller_instruction.products.at(i)) && !is_literal(caller_instruction.ingredients.at(ingredient_index)) && caller_instruction.products.at(i).name != caller_instruction.ingredients.at(ingredient_index).name) {
+            raise << maybe(caller.name) << "'" << to_original_string(caller_instruction) << "' should write to '" << caller_instruction.ingredients.at(ingredient_index).original_string << "' rather than '" << caller_instruction.products.at(i).original_string << "'\n" << end();
+          }
+        }
+      }
+      finish_return_check:;
+    }
+  }
+}
+
+bool is_primitive(recipe_ordinal r) {
+  return r < MAX_PRIMITIVE_RECIPES;
+}
+
+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
+//: the recipe's 'return' indicates that an ingredient is intended to be
+//: modified in place, and will help catch accidental misuse of such
+//: 'ingredient-products' (sometimes called in-out parameters in other
+//: languages).
+
+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"
+  );
+}
+
+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);
+}
+
+string to_string(const vector<double>& in) {
+  if (in.empty()) return "[]";
+  ostringstream out;
+  if (SIZE(in) == 1) {
+    out << no_scientific(in.at(0));
+    return out.str();
+  }
+  out << "[";
+  for (int i = 0;  i < SIZE(in);  ++i) {
+    if (i > 0) out << ", ";
+    out << no_scientific(in.at(i));
+  }
+  out << "]";
+  return out.str();
+}
diff --git a/archive/2.vm/029tools.cc b/archive/2.vm/029tools.cc
new file mode 100644
index 00000000..4cca09f6
--- /dev/null
+++ b/archive/2.vm/029tools.cc
@@ -0,0 +1,326 @@
+//: Allow Mu programs to log facts just like we've been doing in C++ so far.
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "trace", TRACE);
+:(before "End Primitive Recipe Checks")
+case TRACE: {
+  if (SIZE(inst.ingredients) < 3) {
+    raise << maybe(get(Recipe, r).name) << "'trace' takes three or more ingredients rather than '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'trace' should be a number (depth), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (!is_literal_text(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'trace' should be a literal string (label), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case TRACE: {
+  int depth = ingredients.at(0).at(0);
+  string label = current_instruction().ingredients.at(1).name;
+  ostringstream out;
+  for (int i = 2;  i < SIZE(current_instruction().ingredients);  ++i) {
+    if (i > 2) out << ' ';
+    out << inspect(current_instruction().ingredients.at(i), ingredients.at(i));
+  }
+  trace(depth, label) << out.str() << end();
+  break;
+}
+
+//: simpler limited version of 'trace'
+
+:(before "End Types")  //: include in all cleaved compilation units
+const int App_depth = 1;  // where all Mu code will trace to by default
+
+:(before "End Primitive Recipe Declarations")
+STASH,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "stash", STASH);
+:(before "End Primitive Recipe Checks")
+case STASH: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case STASH: {
+  ostringstream out;
+  for (int i = 0;  i < SIZE(current_instruction().ingredients);  ++i) {
+    if (i) out << ' ';
+    out << inspect(current_instruction().ingredients.at(i), ingredients.at(i));
+  }
+  trace(App_depth, "app") << out.str() << end();
+  break;
+}
+
+:(code)
+void test_stash_literal_string() {
+  run(
+      "def main [\n"
+      "  stash [foo]\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo\n"
+  );
+}
+
+void test_stash_literal_number() {
+  run(
+      "def main [\n"
+      "  stash [foo:], 4\n"
+      "]\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "app: foo: 4\n"
+  );
+}
+
+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) {
+  if (is_literal(r))
+    return r.name;
+  // End inspect Special-cases(r, data)
+  ostringstream out;
+  for (long long i = 0;  i < SIZE(data);  ++i) {
+    if (i) out << ' ';
+    out << no_scientific(data.at(i));
+  }
+  return out.str();
+}
+
+:(before "End Primitive Recipe Declarations")
+HIDE_ERRORS,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "hide-errors", HIDE_ERRORS);
+:(before "End Primitive Recipe Checks")
+case HIDE_ERRORS: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case HIDE_ERRORS: {
+  Hide_errors = true;
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+SHOW_ERRORS,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "show-errors", SHOW_ERRORS);
+:(before "End Primitive Recipe Checks")
+case SHOW_ERRORS: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SHOW_ERRORS: {
+  Hide_errors = false;
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+TRACE_UNTIL,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "trace-until", TRACE_UNTIL);
+:(before "End Primitive Recipe Checks")
+case TRACE_UNTIL: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case TRACE_UNTIL: {
+  if (Trace_stream) {
+    Trace_stream->collect_depth = ingredients.at(0).at(0);
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_DUMP_TRACE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$dump-trace", _DUMP_TRACE);
+:(before "End Primitive Recipe Checks")
+case _DUMP_TRACE: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _DUMP_TRACE: {
+  if (ingredients.empty()) {
+    DUMP("");
+  }
+  else {
+    DUMP(current_instruction().ingredients.at(0).name);
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_CLEAR_TRACE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$clear-trace", _CLEAR_TRACE);
+:(before "End Primitive Recipe Checks")
+case _CLEAR_TRACE: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _CLEAR_TRACE: {
+  if (Trace_stream) Trace_stream->past_lines.clear();
+  break;
+}
+
+//:: 'cheating' by using the host system
+
+:(before "End Primitive Recipe Declarations")
+_PRINT,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$print", _PRINT);
+:(before "End Primitive Recipe Checks")
+case _PRINT: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _PRINT: {
+  for (int i = 0;  i < SIZE(ingredients);  ++i) {
+    if (is_literal(current_instruction().ingredients.at(i))) {
+      trace(Callstack_depth+1, "run") << "$print: " << current_instruction().ingredients.at(i).name << end();
+      if (!has_property(current_instruction().ingredients.at(i), "newline")) {
+        cout << current_instruction().ingredients.at(i).name;
+      }
+      // hack: '$print 10' prints '10', but '$print 10/newline' prints '\n'
+      // End $print 10/newline Special-cases
+      else {
+        cout << '\n';
+      }
+    }
+    // End $print Special-cases
+    else {
+      for (int j = 0;  j < SIZE(ingredients.at(i));  ++j) {
+        trace(Callstack_depth+1, "run") << "$print: " << ingredients.at(i).at(j) << end();
+        if (j > 0) cout << " ";
+        cout << no_scientific(ingredients.at(i).at(j));
+      }
+    }
+  }
+  cout.flush();
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_EXIT,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$exit", _EXIT);
+:(before "End Primitive Recipe Checks")
+case _EXIT: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _EXIT: {
+  exit(0);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_SYSTEM,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$system", _SYSTEM);
+:(before "End Primitive Recipe Checks")
+case _SYSTEM: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$system' requires exactly one ingredient, but got '" << to_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_literal_text(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "ingredient to '$system' must be a literal text, but got '" << to_string(inst) << "'\n" << end();
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _SYSTEM: {
+  int status = system(current_instruction().ingredients.at(0).name.c_str());
+  products.resize(1);
+  products.at(0).push_back(status);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_DUMP_MEMORY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$dump-memory", _DUMP_MEMORY);
+:(before "End Primitive Recipe Checks")
+case _DUMP_MEMORY: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _DUMP_MEMORY: {
+  dump_memory();
+  break;
+}
+
+//: In times of real extremis we need to create a whole new modality for debug
+//: logs, independent of other changes to the screen or Trace_stream.
+
+:(before "End Globals")
+ofstream LOG;
+:(before "End One-time Setup")
+//? LOG.open("log");
+
+:(before "End Primitive Recipe Declarations")
+_LOG,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$log", _LOG);
+:(before "End Primitive Recipe Checks")
+case _LOG: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _LOG: {
+  ostringstream out;
+  for (int i = 0;  i < SIZE(current_instruction().ingredients);  ++i) {
+    out << inspect(current_instruction().ingredients.at(i), ingredients.at(i));
+  }
+  LOG << out.str() << '\n';
+  break;
+}
+
+//: set a variable from within Mu code
+//: useful for selectively tracing or printing after some point
+:(before "End Globals")
+bool Foo = false;
+:(before "End Primitive Recipe Declarations")
+_FOO,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$foo", _FOO);
+:(before "End Primitive Recipe Checks")
+case _FOO: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _FOO: {
+  Foo = true;
+  break;
+}
diff --git a/archive/2.vm/030container.cc b/archive/2.vm/030container.cc
new file mode 100644
index 00000000..e20af025
--- /dev/null
+++ b/archive/2.vm/030container.cc
@@ -0,0 +1,819 @@
+//: Containers contain a fixed number of elements of different types.
+
+:(before "End Mu Types Initialization")
+//: We'll use this container as a running example in scenarios below.
+type_ordinal point = put(Type_ordinal, "point", Next_type_ordinal++);
+get_or_insert(Type, point);  // initialize
+get(Type, point).kind = CONTAINER;
+get(Type, point).name = "point";
+get(Type, point).elements.push_back(reagent("x:number"));
+get(Type, point).elements.push_back(reagent("y:number"));
+
+//: Containers can be copied around with a single instruction just like
+//: numbers, no matter how large they are.
+
+//: Tests in this layer often explicitly set up memory before reading it as a
+//: container. Don't do this in general. I'm tagging such cases with /unsafe;
+//: they'll be exceptions to later checks.
+
+:(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
+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
+// its elements.
+type_ordinal point_number = put(Type_ordinal, "point-number", Next_type_ordinal++);
+get_or_insert(Type, point_number);  // initialize
+get(Type, point_number).kind = CONTAINER;
+get(Type, point_number).name = "point-number";
+get(Type, point_number).elements.push_back(reagent("xy:point"));
+get(Type, point_number).elements.push_back(reagent("z:number"));
+
+:(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
+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.
+
+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
+if (type->value == 0) return 1;
+if (!contains_key(Type, type->value)) {
+  raise << "no such type " << type->value << '\n' << end();
+  return 0;
+}
+type_info t = get(Type, type->value);
+if (t.kind == CONTAINER) {
+  // size of a container is the sum of the sizes of its elements
+  int result = 0;
+  for (int i = 0; i < SIZE(t.elements); ++i) {
+    // todo: strengthen assertion to disallow mutual type recursion
+    if (t.elements.at(i).type->value == type->value) {
+      raise << "container " << t.name << " can't include itself as a member\n" << end();
+      return 0;
+    }
+    result += size_of(element_type(type, i));
+  }
+  return result;
+}
+
+:(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.
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "get", GET);
+:(before "End Primitive Recipe Checks")
+case GET: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'get' expects exactly 2 ingredients in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ base = inst.ingredients.at(0);  // new copy for every invocation
+  // Update GET base in Check
+  if (!base.type) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'get' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  const type_tree* base_type = base.type;
+  // Update GET base_type in Check
+  if (!base_type->atom || base_type->value == 0 || !contains_key(Type, base_type->value) || get(Type, base_type->value).kind != CONTAINER) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'get' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  const reagent& offset = inst.ingredients.at(1);
+  if (!is_literal(offset) || !is_mu_scalar(offset)) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'get' should have type 'offset', but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  int offset_value = 0;
+  if (is_integer(offset.name)) {
+    offset_value = to_integer(offset.name);
+  }
+  // End update GET offset_value in Check
+  if (offset_value < 0 || offset_value >= SIZE(get(Type, base_type->value).elements)) {
+    raise << maybe(get(Recipe, r).name) << "invalid offset '" << offset_value << "' for '" << get(Type, base_type->value).name << "'\n" << end();
+    break;
+  }
+  if (inst.products.empty()) break;
+  reagent/*copy*/ product = inst.products.at(0);
+  // Update GET product in Check
+  //: use base.type rather than base_type because later layers will introduce compound types
+  const reagent/*copy*/ element = element_type(base.type, offset_value);
+  if (!types_coercible(product, element)) {
+    raise << maybe(get(Recipe, r).name) << "'get " << base.original_string << ", " << offset.original_string << "' should write to " << names_to_string_without_quotes(element.type) << " but '" << product.name << "' has type " << names_to_string_without_quotes(product.type) << '\n' << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case GET: {
+  reagent/*copy*/ base = current_instruction().ingredients.at(0);
+  // Update GET base in Run
+  int base_address = base.value;
+  if (base_address == 0) {
+    raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  const type_tree* base_type = base.type;
+  // Update GET base_type in Run
+  int offset = ingredients.at(1).at(0);
+  if (offset < 0 || offset >= SIZE(get(Type, base_type->value).elements)) break;  // copied from Check above
+  int src = base_address;
+  for (int i = 0; i < offset; ++i)
+    src += size_of(element_type(base.type, i));
+  trace(Callstack_depth+1, "run") << "address to copy is " << src << end();
+  //: use base.type rather than base_type because later layers will introduce compound types
+  reagent/*copy*/ element = element_type(base.type, offset);
+  element.set_value(src);
+  trace(Callstack_depth+1, "run") << "its type is " << names_to_string(element.type) << end();
+  // Read element
+  products.push_back(read_memory(element));
+  break;
+}
+
+:(code)
+const reagent element_type(const type_tree* type, int offset_value) {
+  assert(offset_value >= 0);
+  const type_tree* base_type = type;
+  // Update base_type in element_type
+  assert(contains_key(Type, base_type->value));
+  assert(!get(Type, base_type->value).name.empty());
+  const type_info& info = get(Type, base_type->value);
+  assert(info.kind == CONTAINER);
+  if (offset_value >= SIZE(info.elements)) return reagent();  // error handled elsewhere
+  reagent/*copy*/ element = info.elements.at(offset_value);
+  // End element_type Special-cases
+  return element;
+}
+
+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
+
+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'.
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "put", PUT);
+:(before "End Primitive Recipe Checks")
+case PUT: {
+  if (SIZE(inst.ingredients) != 3) {
+    raise << maybe(get(Recipe, r).name) << "'put' expects exactly 3 ingredients in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ base = inst.ingredients.at(0);
+  // Update PUT base in Check
+  if (!base.type) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'put' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  const type_tree* base_type = base.type;
+  // Update PUT base_type in Check
+  if (!base_type->atom || base_type->value == 0 || !contains_key(Type, base_type->value) || get(Type, base_type->value).kind != CONTAINER) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'put' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ offset = inst.ingredients.at(1);
+  // Update PUT offset in Check
+  if (!is_literal(offset) || !is_mu_scalar(offset)) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'put' should have type 'offset', but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  int offset_value = 0;
+  //: later layers will permit non-integer offsets
+  if (is_integer(offset.name)) {
+    offset_value = to_integer(offset.name);
+    if (offset_value < 0 || offset_value >= SIZE(get(Type, base_type->value).elements)) {
+      raise << maybe(get(Recipe, r).name) << "invalid offset '" << offset_value << "' for '" << get(Type, base_type->value).name << "'\n" << end();
+      break;
+    }
+  }
+  else {
+    offset_value = offset.value;
+  }
+  const reagent& value = inst.ingredients.at(2);
+  //: use base.type rather than base_type because later layers will introduce compound types
+  const reagent& element = element_type(base.type, offset_value);
+  if (!types_coercible(element, value)) {
+    raise << maybe(get(Recipe, r).name) << "'put " << base.original_string << ", " << offset.original_string << "' should write to " << names_to_string_without_quotes(element.type) << " but '" << value.name << "' has type " << names_to_string_without_quotes(value.type) << '\n' << end();
+    break;
+  }
+  if (inst.products.empty()) break;  // no more checks necessary
+  if (inst.products.at(0).name != inst.ingredients.at(0).name) {
+    raise << maybe(get(Recipe, r).name) << "product of 'put' must be first ingredient '" << inst.ingredients.at(0).original_string << "', but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  // End PUT Product Checks
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case PUT: {
+  reagent/*copy*/ base = current_instruction().ingredients.at(0);
+  // Update PUT base in Run
+  int base_address = base.value;
+  if (base_address == 0) {
+    raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  const type_tree* base_type = base.type;
+  // Update PUT base_type in Run
+  int offset = ingredients.at(1).at(0);
+  if (offset < 0 || offset >= SIZE(get(Type, base_type->value).elements)) break;  // copied from Check above
+  int address = base_address;
+  for (int i = 0; i < offset; ++i)
+    address += size_of(element_type(base.type, i));
+  trace(Callstack_depth+1, "run") << "address to copy to is " << address << end();
+  // optimization: directly write the element rather than updating 'product'
+  // and writing the entire container
+  // Write Memory in PUT in Run
+  write_products = false;
+  for (int i = 0;  i < SIZE(ingredients.at(2));  ++i) {
+    trace(Callstack_depth+1, "mem") << "storing " << no_scientific(ingredients.at(2).at(i)) << " in location " << address+i << end();
+    put(Memory, address+i, ingredients.at(2).at(i));
+  }
+  break;
+}
+
+:(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.
+
+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
+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") {
+  insert_container(command, CONTAINER, in);
+}
+
+//: Even though we allow containers to be extended, we don't allow this after
+//: a call to transform_all. But we do want to detect this situation and raise
+//: an error. This field will help us raise such errors.
+:(before "End type_info Fields")
+int Num_calls_to_transform_all_at_first_definition;
+:(before "End type_info Constructor")
+Num_calls_to_transform_all_at_first_definition = -1;
+
+:(code)
+void insert_container(const string& command, kind_of_type kind, istream& in) {
+  skip_whitespace_but_not_newline(in);
+  string name = next_word(in);
+  if (name.empty()) {
+    assert(!has_data(in));
+    raise << "incomplete container definition at end of file (0)\n" << end();
+    return;
+  }
+  // End container Name Refinements
+  trace(101, "parse") << "--- defining " << command << ' ' << name << end();
+  if (!contains_key(Type_ordinal, name)
+      || get(Type_ordinal, name) == 0) {
+    put(Type_ordinal, name, Next_type_ordinal++);
+  }
+  trace(102, "parse") << "type number: " << get(Type_ordinal, name) << end();
+  skip_bracket(in, "'"+command+"' must begin with '['");
+  type_info& info = get_or_insert(Type, get(Type_ordinal, name));
+  if (info.Num_calls_to_transform_all_at_first_definition == -1) {
+    // initial definition of this container
+    info.Num_calls_to_transform_all_at_first_definition = Num_calls_to_transform_all;
+  }
+  else if (info.Num_calls_to_transform_all_at_first_definition != Num_calls_to_transform_all) {
+    // extension after transform_all
+    raise << "there was a call to transform_all() between the definition of container '" << name << "' and a subsequent extension. This is not supported, since any recipes that used '" << name << "' values have already been transformed and \"frozen\".\n" << end();
+    return;
+  }
+  info.name = name;
+  info.kind = kind;
+  while (has_data(in)) {
+    skip_whitespace_and_comments(in);
+    string element = next_word(in);
+    if (element.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete container definition at end of file (1)\n" << end();
+      return;
+    }
+    if (element == "]") break;
+    if (in.peek() != '\n') {
+      raise << command << " '" << name << "' contains multiple elements on a single line. Containers and exclusive containers must only contain elements, one to a line, no code.\n" << end();
+      // skip rest of container declaration
+      while (has_data(in)) {
+        skip_whitespace_and_comments(in);
+        if (next_word(in) == "]") break;
+      }
+      break;
+    }
+    info.elements.push_back(reagent(element));
+    expand_type_abbreviations(info.elements.back().type);  // todo: use abbreviation before declaration
+    replace_unknown_types_with_unique_ordinals(info.elements.back().type, info);
+    trace(103, "parse") << "  element: " << to_string(info.elements.back()) << end();
+    // End Load Container Element Definition
+  }
+}
+
+void replace_unknown_types_with_unique_ordinals(type_tree* type, const type_info& info) {
+  if (!type) return;
+  if (!type->atom) {
+    replace_unknown_types_with_unique_ordinals(type->left, info);
+    replace_unknown_types_with_unique_ordinals(type->right, info);
+    return;
+  }
+  assert(!type->name.empty());
+  if (contains_key(Type_ordinal, type->name)) {
+    type->value = get(Type_ordinal, type->name);
+  }
+  // End insert_container Special-cases
+  else if (type->name != "->") {  // used in recipe types
+    put(Type_ordinal, type->name, Next_type_ordinal++);
+    type->value = get(Type_ordinal, type->name);
+  }
+}
+
+void skip_bracket(istream& in, string message) {
+  skip_whitespace_and_comments(in);
+  if (in.get() != '[')
+    raise << message << '\n' << end();
+}
+
+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
+
+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
+:(code)
+// extremely inefficient; we process all types over and over again, once for every single recipe
+// but it doesn't seem to cause any noticeable slowdown
+void expand_type_abbreviations_in_containers(const recipe_ordinal /*unused*/) {
+  for (map<type_ordinal, type_info>::iterator p = Type.begin();  p != Type.end();  ++p) {
+    for (int i = 0;  i < SIZE(p->second.elements);  ++i)
+      expand_type_abbreviations(p->second.elements.at(i).type);
+  }
+}
+
+//: ensure scenarios are consistent by always starting new container
+//: declarations at the same type number
+:(before "End Reset")  //: for tests
+Next_type_ordinal = 1000;
+:(before "End Test Run Initialization")
+assert(Next_type_ordinal < 1000);
+
+:(code)
+void test_error_on_transform_all_between_container_definition_and_extension() {
+  // define a container
+  run("container foo [\n"
+      "  a:num\n"
+      "]\n");
+  // try to extend the container after transform
+  transform_all();
+  CHECK_TRACE_DOESNT_CONTAIN_ERRORS();
+  Hide_errors = true;
+  run("container foo [\n"
+      "  b:num\n"
+      "]\n");
+  CHECK_TRACE_CONTAINS_ERRORS();
+}
+
+//:: Allow container definitions anywhere in the codebase, but complain if you
+//:: can't find a definition at the end.
+
+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
+
+:(code)
+void check_or_set_invalid_types(const recipe_ordinal r) {
+  recipe& caller = get(Recipe, r);
+  trace(101, "transform") << "--- check for invalid types in recipe " << caller.name << end();
+  for (int index = 0;  index < SIZE(caller.steps);  ++index) {
+    instruction& inst = caller.steps.at(index);
+    for (int i = 0;  i < SIZE(inst.ingredients);  ++i)
+      check_or_set_invalid_types(inst.ingredients.at(i), caller, inst);
+    for (int i = 0;  i < SIZE(inst.products);  ++i)
+      check_or_set_invalid_types(inst.products.at(i), caller, inst);
+  }
+  // End check_or_set_invalid_types
+}
+
+void check_or_set_invalid_types(reagent& r, const recipe& caller, const instruction& inst) {
+  // Begin check_or_set_invalid_types(r)
+  check_or_set_invalid_types(r.type, maybe(caller.name), "'"+to_original_string(inst)+"'");
+}
+
+void check_or_set_invalid_types(type_tree* type, const string& location_for_error_messages, const string& name_for_error_messages) {
+  if (!type) return;
+  // End Container Type Checks
+  if (!type->atom) {
+    check_or_set_invalid_types(type->left, location_for_error_messages, name_for_error_messages);
+    check_or_set_invalid_types(type->right, location_for_error_messages, name_for_error_messages);
+    return;
+  }
+  if (type->value == 0) return;
+  if (!contains_key(Type, type->value)) {
+    assert(!type->name.empty());
+    if (contains_key(Type_ordinal, type->name))
+      type->value = get(Type_ordinal, type->name);
+    else
+      raise << location_for_error_messages << "unknown type " << type->name << " in " << name_for_error_messages << '\n' << end();
+  }
+}
+
+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();
+
+:(code)
+void check_container_field_types() {
+  for (map<type_ordinal, type_info>::iterator p = Type.begin();  p != Type.end();  ++p) {
+    const type_info& info = p->second;
+    // Check Container Field Types(info)
+    for (int i = 0;  i < SIZE(info.elements);  ++i)
+      check_invalid_types(info.elements.at(i).type, maybe(info.name), info.elements.at(i).name);
+  }
+}
+
+void check_invalid_types(const type_tree* type, const string& location_for_error_messages, const string& name_for_error_messages) {
+  if (!type) return;  // will throw a more precise error elsewhere
+  if (!type->atom) {
+    check_invalid_types(type->left, location_for_error_messages, name_for_error_messages);
+    check_invalid_types(type->right, location_for_error_messages, name_for_error_messages);
+    return;
+  }
+  if (type->value != 0) {  // value 0 = compound types (layer parse_tree) or type ingredients (layer shape_shifting_container)
+    if (!contains_key(Type, type->value))
+      raise << location_for_error_messages << "unknown type in " << name_for_error_messages << '\n' << end();
+  }
+}
+
+string to_original_string(const type_ordinal t) {
+  ostringstream out;
+  if (!contains_key(Type, t)) return out.str();
+  const type_info& info = get(Type, t);
+  if (info.kind == PRIMITIVE) return out.str();
+  out << (info.kind == CONTAINER ? "container" : "exclusive-container") << " " << info.name << " [\n";
+  for (int i = 0;  i < SIZE(info.elements);  ++i) {
+    out << "  " << info.elements.at(i).original_string << "\n";
+  }
+  out << "]\n";
+  return out.str();
+}
diff --git a/archive/2.vm/031merge.cc b/archive/2.vm/031merge.cc
new file mode 100644
index 00000000..ace4387b
--- /dev/null
+++ b/archive/2.vm/031merge.cc
@@ -0,0 +1,270 @@
+//: Construct types out of their constituent fields.
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "merge", MERGE);
+:(before "End Primitive Recipe Checks")
+case MERGE: {
+  // type-checking in a separate transform below
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MERGE: {
+  products.resize(1);
+  for (int i = 0;  i < SIZE(ingredients);  ++i)
+    for (int j = 0;  j < SIZE(ingredients.at(i));  ++j)
+      products.at(0).push_back(ingredients.at(i).at(j));
+  break;
+}
+
+//: type-check 'merge' to avoid interpreting numbers as addresses
+
+:(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.
+
+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
+//: exclusive containers which contain just one of rather than all of their
+//: elements. That will also require backtracking capabilities. Here's the
+//: state we need to maintain for backtracking:
+
+:(before "End Types")
+struct merge_check_point {
+  reagent container;
+  int container_element_index;
+  merge_check_point(const reagent& c, int i) :container(c), container_element_index(i) {}
+};
+
+struct merge_check_state {
+  stack<merge_check_point> data;
+};
+
+:(before "End Checks")
+Transform.push_back(check_merge_calls);  // idempotent
+:(code)
+void check_merge_calls(const recipe_ordinal r) {
+  const recipe& caller = get(Recipe, r);
+  trace(101, "transform") << "--- type-check merge instructions in recipe " << caller.name << end();
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    const instruction& inst = caller.steps.at(i);
+    if (inst.name != "merge") continue;
+    if (SIZE(inst.products) != 1) {
+      raise << maybe(caller.name) << "'merge' should yield a single product in '" << to_original_string(inst) << "'\n" << end();
+      continue;
+    }
+    reagent/*copy*/ product = inst.products.at(0);
+    // Update product While Type-checking Merge
+    const type_tree* product_base_type = product.type->atom ? product.type : product.type->left;
+    assert(product_base_type->atom);
+    if (product_base_type->value == 0 || !contains_key(Type, product_base_type->value)) {
+      raise << maybe(caller.name) << "'merge' should yield a container in '" << to_original_string(inst) << "'\n" << end();
+      continue;
+    }
+    const type_info& info = get(Type, product_base_type->value);
+    if (info.kind != CONTAINER && info.kind != EXCLUSIVE_CONTAINER) {
+      raise << maybe(caller.name) << "'merge' should yield a container in '" << to_original_string(inst) << "'\n" << end();
+      continue;
+    }
+    check_merge_call(inst.ingredients, product, caller, inst);
+  }
+}
+
+void check_merge_call(const vector<reagent>& ingredients, const reagent& product, const recipe& caller, const instruction& inst) {
+  int ingredient_index = 0;
+  merge_check_state state;
+  state.data.push(merge_check_point(product, 0));
+  while (true) {
+    assert(!state.data.empty());
+    trace(102, "transform") << ingredient_index << " vs " << SIZE(ingredients) << end();
+    if (ingredient_index >= SIZE(ingredients)) {
+      raise << maybe(caller.name) << "too few ingredients in '" << to_original_string(inst) << "'\n" << end();
+      return;
+    }
+    reagent& container = state.data.top().container;
+    if (!container.type) return;  // error handled elsewhere
+    const type_tree* top_root_type = container.type->atom ? container.type : container.type->left;
+    assert(top_root_type->atom);
+    type_info& container_info = get(Type, top_root_type->value);
+    switch (container_info.kind) {
+      case CONTAINER: {
+        // degenerate case: merge with the same type always succeeds
+        if (state.data.top().container_element_index == 0 && types_coercible(container, inst.ingredients.at(ingredient_index)))
+          return;
+        const reagent& expected_ingredient = element_type(container.type, state.data.top().container_element_index);
+        trace(102, "transform") << "checking container " << to_string(container) << " || " << to_string(expected_ingredient) << " vs ingredient " << ingredient_index << end();
+        // if the current element is the ingredient we expect, move on to the next element/ingredient
+        if (types_coercible(expected_ingredient, ingredients.at(ingredient_index))) {
+          ++ingredient_index;
+          ++state.data.top().container_element_index;
+          while (state.data.top().container_element_index >= SIZE(get(Type, get_base_type(state.data.top().container.type)->value).elements)) {
+            state.data.pop();
+            if (state.data.empty()) {
+              if (ingredient_index < SIZE(ingredients))
+                raise << maybe(caller.name) << "too many ingredients in '" << to_original_string(inst) << "'\n" << end();
+              return;
+            }
+            ++state.data.top().container_element_index;
+          }
+        }
+        // if not, maybe it's a field of the current element
+        else {
+          // no change to ingredient_index
+          state.data.push(merge_check_point(expected_ingredient, 0));
+        }
+        break;
+      }
+      // End check_merge_call Special-cases
+      default: {
+        if (!types_coercible(container, ingredients.at(ingredient_index))) {
+          raise << maybe(caller.name) << "incorrect type of ingredient " << ingredient_index << " in '" << to_original_string(inst) << "'\n" << end();
+          raise << "  (expected '" << debug_string(container) << "')\n" << end();
+          raise << "  (got '" << debug_string(ingredients.at(ingredient_index)) << "')\n" << end();
+          return;
+        }
+        ++ingredient_index;
+        // ++state.data.top().container_element_index;  // unnecessary, but wouldn't do any harm
+        do {
+          state.data.pop();
+          if (state.data.empty()) {
+            if (ingredient_index < SIZE(ingredients))
+              raise << maybe(caller.name) << "too many ingredients in '" << to_original_string(inst) << "'\n" << end();
+            return;
+          }
+          ++state.data.top().container_element_index;
+        } while (state.data.top().container_element_index >= SIZE(get(Type, get_base_type(state.data.top().container.type)->value).elements));
+      }
+    }
+  }
+  // never gets here
+  assert(false);
+}
+
+//: replaced in a later layer
+//: todo: find some clean way to take this call completely out of this layer
+const type_tree* get_base_type(const type_tree* t) {
+  return t;
+}
+
+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>
+using std::stack;
diff --git a/archive/2.vm/032array.cc b/archive/2.vm/032array.cc
new file mode 100644
index 00000000..30adde20
--- /dev/null
+++ b/archive/2.vm/032array.cc
@@ -0,0 +1,635 @@
+//: Arrays contain a variable number of elements of the same type. Their value
+//: starts with the length of the array.
+//:
+//: You can create arrays of containers, but containers can only contain
+//: elements of a fixed size, so you can't create containers containing arrays.
+//: Create containers containing addresses to arrays instead.
+
+//: You can create arrays using 'create-array'.
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "create-array", CREATE_ARRAY);
+:(before "End Primitive Recipe Checks")
+case CREATE_ARRAY: {
+  if (inst.products.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'create-array' needs one product and no ingredients but got '" << to_original_string(inst) << '\n' << end();
+    break;
+  }
+  reagent/*copy*/ product = inst.products.at(0);
+  // Update CREATE_ARRAY product in Check
+  if (!is_mu_array(product)) {
+    raise << maybe(get(Recipe, r).name) << "'create-array' cannot create non-array '" << product.original_string << "'\n" << end();
+    break;
+  }
+  if (!product.type->right) {
+    raise << maybe(get(Recipe, r).name) << "create array of what? '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  // 'create-array' will need to check properties rather than types
+  type_tree* array_length_from_type = product.type->right->right;
+  if (!array_length_from_type) {
+    raise << maybe(get(Recipe, r).name) << "create array of what size? '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!product.type->right->right->atom)
+    array_length_from_type = array_length_from_type->left;
+  if (!is_integer(array_length_from_type->name)) {
+    raise << maybe(get(Recipe, r).name) << "'create-array' product should specify size of array after its element type, but got '" << product.type->right->right->name << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CREATE_ARRAY: {
+  reagent/*copy*/ product = current_instruction().products.at(0);
+  // Update CREATE_ARRAY product in Run
+  int base_address = product.value;
+  type_tree* array_length_from_type = product.type->right->right;
+  if (!product.type->right->right->atom)
+    array_length_from_type = array_length_from_type->left;
+  int array_length = to_integer(array_length_from_type->name);
+  // initialize array length, so that size_of will work
+  trace(Callstack_depth+1, "mem") << "storing " << array_length << " in location " << base_address << end();
+  put(Memory, base_address, array_length);  // in array elements
+  int size = size_of(product);  // in locations
+  trace(Callstack_depth+1, "run") << "creating array from " << size << " locations" << end();
+  // initialize array
+  for (int i = 1;  i <= size_of(product);  ++i)
+    put(Memory, base_address+i, 0);
+  // no need to update product
+  write_products = false;
+  break;
+}
+
+:(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))
+  return types_strictly_match(array_element(from.type), array_element(to.type));
+
+:(before "End size_of(reagent r) Special-cases")
+if (!r.type->atom && r.type->left->atom && r.type->left->value == Array_type_ordinal) {
+  if (!r.type->right) {
+    raise << maybe(current_recipe_name()) << "'" << r.original_string << "' is an array of what?\n" << end();
+    return 1;
+  }
+  return /*space for length*/1 + array_length(r)*size_of(array_element(r.type));
+}
+
+:(before "End size_of(type) Non-atom Special-cases")
+if (type->left->value == Array_type_ordinal) return static_array_length(type);
+:(code)
+int static_array_length(const type_tree* type) {
+  if (!type->atom && type->right && !type->right->atom && type->right->right && !type->right->right->atom && !type->right->right->right  // exactly 3 types
+      && type->right->right->left && type->right->right->left->atom && is_integer(type->right->right->left->name)) {  // third 'type' is a number
+    // get size from type
+    return to_integer(type->right->right->left->name);
+  }
+  cerr << to_string(type) << '\n';
+  assert(false);
+}
+
+//: disable the size mismatch check for arrays since the destination array
+//: need not be initialized
+:(before "End size_mismatch(x) Special-cases")
+if (x.type && !x.type->atom && x.type->left->value == Array_type_ordinal) return false;
+
+//:: arrays inside containers
+//: arrays are disallowed inside containers unless their length is fixed in
+//: advance
+
+:(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;
+}
+
+:(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")
+{
+  const type_tree* type = info.elements.back().type;
+  if (type && type->atom && type->name == "array") {
+    raise << "container '" << name << "' doesn't specify type of array elements for '" << info.elements.back().name << "'\n" << end();
+    continue;
+  }
+  if (type && !type->atom && type->left->atom && type->left->name == "array") {
+    if (!type->right) {
+      raise << "container '" << name << "' doesn't specify type of array elements for '" << info.elements.back().name << "'\n" << end();
+      continue;
+    }
+    if (!type->right->right || !is_integer(type->right->right->left->name)) {  // array has no length
+      raise << "container '" << name << "' cannot determine size of element '" << info.elements.back().name << "'\n" << end();
+      continue;
+    }
+  }
+}
+
+//: disable the size mismatch check for 'merge' instructions since containers
+//: can contain arrays, and since we already do plenty of checking for them
+:(before "End size_mismatch(x) Special-cases")
+if (current_call().running_step_index < SIZE(get(Recipe, current_call().running_recipe).steps)
+    && current_instruction().operation == MERGE) {
+  return false;
+}
+
+:(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'
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "index", INDEX);
+:(before "End Primitive Recipe Checks")
+case INDEX: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'index' expects exactly 2 ingredients in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ base = inst.ingredients.at(0);
+  // Update INDEX base in Check
+  if (!is_mu_array(base)) {
+    raise << maybe(get(Recipe, r).name) << "'index' on a non-array '" << base.original_string << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ index = inst.ingredients.at(1);
+  // Update INDEX index in Check
+  if (!is_mu_number(index)) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'index' should be a number, but got '" << index.original_string << "'\n" << end();
+    break;
+  }
+  if (inst.products.empty()) break;
+  reagent/*copy*/ product = inst.products.at(0);
+  // Update INDEX product in Check
+  reagent/*local*/ element(copy_array_element(base.type));
+  if (!types_coercible(product, element)) {
+    raise << maybe(get(Recipe, r).name) << "'index' on '" << base.original_string << "' can't be saved in '" << product.original_string << "'; type should be '" << names_to_string_without_quotes(element.type) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case INDEX: {
+  reagent/*copy*/ base = current_instruction().ingredients.at(0);
+  // Update INDEX base in Run
+  int base_address = base.value;
+  trace(Callstack_depth+1, "run") << "base address is " << base_address << end();
+  if (base_address == 0) {
+    raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ index = current_instruction().ingredients.at(1);
+  // Update INDEX index in Run
+  vector<double> index_val(read_memory(index));
+  if (index_val.at(0) < 0 || index_val.at(0) >= get_or_insert(Memory, base_address)) {
+    raise << maybe(current_recipe_name()) << "invalid index " << no_scientific(index_val.at(0)) << " in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  reagent/*local*/ element(copy_array_element(base.type));
+  element.set_value(base_address + /*skip length*/1 + index_val.at(0)*size_of(element.type));
+  trace(Callstack_depth+1, "run") << "address to copy is " << element.value << end();
+  trace(Callstack_depth+1, "run") << "its type is " << to_string(element.type) << end();
+  // Read element
+  products.push_back(read_memory(element));
+  break;
+}
+
+:(code)
+type_tree* copy_array_element(const type_tree* type) {
+  return new type_tree(*array_element(type));
+}
+
+type_tree* array_element(const type_tree* type) {
+  assert(type->right);
+  if (type->right->atom) {
+    return type->right;
+  }
+  else if (!type->right->right) {
+    return type->right->left;
+  }
+  // hack: support array:num:3 without requiring extra parens
+  else if (type->right->right->left && type->right->right->left->atom && is_integer(type->right->right->left->name)) {
+    assert(!type->right->right->right);
+    return type->right->left;
+  }
+  return type->right;
+}
+
+int array_length(const reagent& x) {
+  // x should already be canonized.
+  // hack: look for length in type
+  if (!x.type->atom && x.type->right && !x.type->right->atom && x.type->right->right && !x.type->right->right->atom && !x.type->right->right->right  // exactly 3 types
+      && x.type->right->right->left && x.type->right->right->left->atom && is_integer(x.type->right->right->left->name)) {  // third 'type' is a number
+    // get size from type
+    return to_integer(x.type->right->right->left->name);
+  }
+  // this should never happen at transform time
+  return get_or_insert(Memory, x.value);
+}
+
+:(before "End Unit Tests")
+void test_array_length_compound() {
+  put(Memory, 1, 3);
+  put(Memory, 2, 14);
+  put(Memory, 3, 15);
+  put(Memory, 4, 16);
+  reagent x("1:array:address:num");  // 3 types, but not a static array
+  populate_value(x);
+  CHECK_EQ(array_length(x), 3);
+}
+
+void test_array_length_static() {
+  reagent x("1:array:num:3");
+  CHECK_EQ(array_length(x), 3);
+}
+
+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
+
+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'.
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "put-index", PUT_INDEX);
+:(before "End Primitive Recipe Checks")
+case PUT_INDEX: {
+  if (SIZE(inst.ingredients) != 3) {
+    raise << maybe(get(Recipe, r).name) << "'put-index' expects exactly 3 ingredients in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ base = inst.ingredients.at(0);
+  // Update PUT_INDEX base in Check
+  if (!is_mu_array(base)) {
+    raise << maybe(get(Recipe, r).name) << "'put-index' on a non-array '" << base.original_string << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ index = inst.ingredients.at(1);
+  // Update PUT_INDEX index in Check
+  if (!is_mu_number(index)) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'put-index' should have type 'number', but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ value = inst.ingredients.at(2);
+  // Update PUT_INDEX value in Check
+  reagent/*local*/ element(copy_array_element(base.type));
+  if (!types_coercible(element, value)) {
+    raise << maybe(get(Recipe, r).name) << "'put-index " << base.original_string << ", " << inst.ingredients.at(1).original_string << "' should store " << names_to_string_without_quotes(element.type) << " but '" << value.name << "' has type " << names_to_string_without_quotes(value.type) << '\n' << end();
+    break;
+  }
+  if (inst.products.empty()) break;  // no more checks necessary
+  if (inst.products.at(0).name != inst.ingredients.at(0).name) {
+    raise << maybe(get(Recipe, r).name) << "product of 'put-index' must be first ingredient '" << inst.ingredients.at(0).original_string << "', but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  // End PUT_INDEX Product Checks
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case PUT_INDEX: {
+  reagent/*copy*/ base = current_instruction().ingredients.at(0);
+  // Update PUT_INDEX base in Run
+  int base_address = base.value;
+  if (base_address == 0) {
+    raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ index = current_instruction().ingredients.at(1);
+  // Update PUT_INDEX index in Run
+  vector<double> index_val(read_memory(index));
+  if (index_val.at(0) < 0 || index_val.at(0) >= get_or_insert(Memory, base_address)) {
+    raise << maybe(current_recipe_name()) << "invalid index " << no_scientific(index_val.at(0)) << " in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  int address = base_address + /*skip length*/1 + index_val.at(0)*size_of(array_element(base.type));
+  trace(Callstack_depth+1, "run") << "address to copy to is " << address << end();
+  // optimization: directly write the element rather than updating 'product'
+  // and writing the entire array
+  write_products = false;
+  vector<double> value = read_memory(current_instruction().ingredients.at(2));
+  // Write Memory in PUT_INDEX in Run
+  for (int i = 0;  i < SIZE(value);  ++i) {
+    trace(Callstack_depth+1, "mem") << "storing " << no_scientific(value.at(i)) << " in location " << address+i << end();
+    put(Memory, address+i, value.at(i));
+  }
+  break;
+}
+
+:(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
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "length", LENGTH);
+:(before "End Primitive Recipe Checks")
+case LENGTH: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'length' expects exactly 2 ingredients in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ array = inst.ingredients.at(0);
+  // Update LENGTH array in Check
+  if (!is_mu_array(array)) {
+    raise << "tried to calculate length of non-array '" << array.original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case LENGTH: {
+  reagent/*copy*/ array = current_instruction().ingredients.at(0);
+  // Update LENGTH array in Run
+  if (array.value == 0) {
+    raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  products.resize(1);
+  products.at(0).push_back(get_or_insert(Memory, array.value));
+  break;
+}
+
+//: optimization: none of the instructions in this layer use 'ingredients' so
+//: stop copying potentially huge arrays into it.
+:(before "End should_copy_ingredients Special-cases")
+recipe_ordinal r = current_instruction().operation;
+if (r == CREATE_ARRAY || r == INDEX || r == PUT_INDEX || r == LENGTH)
+  return false;
diff --git a/archive/2.vm/033exclusive_container.cc b/archive/2.vm/033exclusive_container.cc
new file mode 100644
index 00000000..fc944f8d
--- /dev/null
+++ b/archive/2.vm/033exclusive_container.cc
@@ -0,0 +1,554 @@
+//: Exclusive containers contain exactly one of a fixed number of 'variants'
+//: of different types.
+//:
+//: They also implicitly contain a tag describing precisely which variant is
+//: currently stored in them.
+
+:(before "End Mu Types Initialization")
+//: We'll use this container as a running example, with two number elements.
+{
+type_ordinal tmp = put(Type_ordinal, "number-or-point", Next_type_ordinal++);
+get_or_insert(Type, tmp);  // initialize
+get(Type, tmp).kind = EXCLUSIVE_CONTAINER;
+get(Type, tmp).name = "number-or-point";
+get(Type, tmp).elements.push_back(reagent("i:number"));
+get(Type, tmp).elements.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.
+
+:(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) {
+  // size of an exclusive container is the size of its largest variant
+  // (So like containers, it can't contain arrays.)
+  int result = 0;
+  for (int i = 0; i < SIZE(t.elements); ++i) {
+    reagent tmp;
+    tmp.type = new type_tree(*type);
+    int size = size_of(variant_type(tmp, i));
+    if (size > result) result = size;
+  }
+  // ...+1 for its tag.
+  return result+1;
+}
+
+//:: To access variants of an exclusive container, use 'maybe-convert'.
+//: It always returns an address (so that you can modify it) or null (to
+//: signal that the conversion failed (because the container contains a
+//: different variant).
+
+//: 'maybe-convert' requires a literal in ingredient 1. We'll use a synonym
+//: called 'variant'.
+:(before "End Mu Types Initialization")
+put(Type_ordinal, "variant", 0);
+
+:(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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "maybe-convert", MAYBE_CONVERT);
+:(before "End Primitive Recipe Checks")
+case MAYBE_CONVERT: {
+  const recipe& caller = get(Recipe, r);
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(caller.name) << "'maybe-convert' expects exactly 2 ingredients in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ base = inst.ingredients.at(0);
+  // Update MAYBE_CONVERT base in Check
+  if (!base.type) {
+    raise << maybe(caller.name) << "first ingredient of 'maybe-convert' should be an exclusive-container, but got '" << base.original_string << "'\n" << end();
+    break;
+  }
+  const type_tree* base_type = base.type;
+  // Update MAYBE_CONVERT base_type in Check
+  if (!base_type->atom || base_type->value == 0 || !contains_key(Type, base_type->value) || get(Type, base_type->value).kind != EXCLUSIVE_CONTAINER) {
+    raise << maybe(caller.name) << "first ingredient of 'maybe-convert' should be an exclusive-container, but got '" << base.original_string << "'\n" << end();
+    break;
+  }
+  if (!is_literal(inst.ingredients.at(1))) {
+    raise << maybe(caller.name) << "second ingredient of 'maybe-convert' should have type 'variant', but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  if (inst.products.empty()) break;
+  if (SIZE(inst.products) != 2) {
+    raise << maybe(caller.name) << "'maybe-convert' expects exactly 2 products in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ product = inst.products.at(0);
+  // Update MAYBE_CONVERT product in Check
+  reagent& offset = inst.ingredients.at(1);
+  populate_value(offset);
+  if (offset.value >= SIZE(get(Type, base_type->value).elements)) {
+    raise << maybe(caller.name) << "invalid tag " << offset.value << " in '" << to_original_string(inst) << '\n' << end();
+    break;
+  }
+  const reagent& variant = variant_type(base, offset.value);
+  if (!types_coercible(product, variant)) {
+    raise << maybe(caller.name) << "'maybe-convert " << base.original_string << ", " << inst.ingredients.at(1).original_string << "' should write to " << to_string(variant.type) << " but '" << product.name << "' has type " << to_string(product.type) << '\n' << end();
+    break;
+  }
+  reagent/*copy*/ status = inst.products.at(1);
+  // Update MAYBE_CONVERT status in Check
+  if (!is_mu_boolean(status)) {
+    raise << maybe(get(Recipe, r).name) << "second product yielded by 'maybe-convert' should be a boolean, but tried to write to '" << inst.products.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MAYBE_CONVERT: {
+  reagent/*copy*/ base = current_instruction().ingredients.at(0);
+  // Update MAYBE_CONVERT base in Run
+  int base_address = base.value;
+  if (base_address == 0) {
+    raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  int tag = current_instruction().ingredients.at(1).value;
+  reagent/*copy*/ product = current_instruction().products.at(0);
+  // Update MAYBE_CONVERT product in Run
+  reagent/*copy*/ status = current_instruction().products.at(1);
+  // Update MAYBE_CONVERT status in Run
+  // optimization: directly write results to only update first product when necessary
+  write_products = false;
+  if (tag == static_cast<int>(get_or_insert(Memory, base_address))) {
+    const reagent& variant = variant_type(base, tag);
+    trace(Callstack_depth+1, "mem") << "storing 1 in location " << status.value << end();
+    put(Memory, status.value, 1);
+    if (!is_dummy(product)) {
+      // Write Memory in Successful MAYBE_CONVERT in Run
+      for (int i = 0;  i < size_of(variant);  ++i) {
+        double val = get_or_insert(Memory, base_address+/*skip tag*/1+i);
+        trace(Callstack_depth+1, "mem") << "storing " << no_scientific(val) << " in location " << product.value+i << end();
+        put(Memory, product.value+i, val);
+      }
+    }
+  }
+  else {
+    trace(Callstack_depth+1, "mem") << "storing 0 in location " << status.value << end();
+    put(Memory, status.value, 0);
+  }
+  break;
+}
+
+:(code)
+const reagent variant_type(const reagent& base, int tag) {
+  return variant_type(base.type, tag);
+}
+
+const reagent variant_type(const type_tree* type, int tag) {
+  assert(tag >= 0);
+  const type_tree* root_type = type->atom ? type : type->left;
+  assert(contains_key(Type, root_type->value));
+  assert(!get(Type, root_type->value).name.empty());
+  const type_info& info = get(Type, root_type->value);
+  assert(info.kind == EXCLUSIVE_CONTAINER);
+  reagent/*copy*/ element = info.elements.at(tag);
+  // End variant_type Special-cases
+  return element;
+}
+
+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.
+
+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") {
+  insert_container(command, EXCLUSIVE_CONTAINER, in);
+}
+
+//: arrays are disallowed inside exclusive containers unless their length is
+//: fixed in advance
+
+:(code)
+void test_exclusive_container_contains_array() {
+  run(
+      "exclusive-container foo [\n"
+      "  x:@:num:3\n"
+      "]\n"
+  );
+  CHECK_TRACE_COUNT("error", 0);
+}
+
+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'.
+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
+
+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: {
+  assert(state.data.top().container_element_index == 0);
+  trace(102, "transform") << "checking exclusive container " << to_string(container) << " vs ingredient " << ingredient_index << end();
+  // easy case: exact match
+  if (types_strictly_match(container, inst.ingredients.at(ingredient_index)))
+    return;
+  if (!is_literal(ingredients.at(ingredient_index))) {
+    raise << maybe(caller.name) << "ingredient " << ingredient_index << " of 'merge' should be a literal, for the tag of exclusive-container '" << container_info.name << "' in '" << to_original_string(inst) << "'\n" << end();
+    return;
+  }
+  reagent/*copy*/ ingredient = ingredients.at(ingredient_index);  // unnecessary copy just to keep this function from modifying caller
+  populate_value(ingredient);
+  if (ingredient.value >= SIZE(container_info.elements)) {
+    raise << maybe(caller.name) << "invalid tag at " << ingredient_index << " for '" << container_info.name << "' in '" << to_original_string(inst) << "'\n" << end();
+    return;
+  }
+  const reagent& variant = variant_type(container, ingredient.value);
+  trace(102, "transform") << "tag: " << ingredient.value << end();
+  // replace union with its variant
+  state.data.pop();
+  state.data.push(merge_check_point(variant, 0));
+  ++ingredient_index;
+  break;
+}
+
+:(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.
+:(before "End size_mismatch(x) Special-cases")
+if (current_step_index() < SIZE(Current_routine->steps())
+    && current_instruction().operation == MERGE
+    && !current_instruction().products.empty()
+    && current_instruction().products.at(0).type) {
+  reagent/*copy*/ x = current_instruction().products.at(0);
+  // Update size_mismatch Check for MERGE(x)
+  const type_tree* root_type = x.type->atom ? x.type : x.type->left;
+  assert(root_type->atom);
+  if (get(Type, root_type->value).kind == EXCLUSIVE_CONTAINER)
+    return size_of(x) < SIZE(data);
+}
+
+:(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/archive/2.vm/034address.cc b/archive/2.vm/034address.cc
new file mode 100644
index 00000000..bafde7b4
--- /dev/null
+++ b/archive/2.vm/034address.cc
@@ -0,0 +1,514 @@
+//: Addresses help us spend less time copying data around.
+
+//: So far we've been operating on primitives like numbers and characters, and
+//: we've started combining these primitives together into larger logical
+//: units (containers or arrays) that may contain many different primitives at
+//: once. Containers and arrays can grow quite large in complex programs, and
+//: we'd like some way to efficiently share them between recipes without
+//: constantly having to make copies. Right now 'next-ingredient' and 'return'
+//: copy data across recipe boundaries. To avoid copying large quantities of
+//: data around, we'll use *addresses*. An address is a bookmark to some
+//: arbitrary quantity of data (the *payload*). It's a primitive, so it's as
+//: efficient to copy as a number. To read or modify the payload 'pointed to'
+//: by an address, we'll perform a *lookup*.
+//:
+//: The notion of 'lookup' isn't an instruction like 'add' or 'subtract'.
+//: Instead it's an operation that can be performed when reading any of the
+//: ingredients of an instruction, and when writing to any of the products. To
+//: write to the payload of an ingredient rather than its value, simply add
+//: the /lookup property to it. Modern computers provide efficient support for
+//: addresses and lookups, making this a realistic feature.
+//:
+//: To create addresses and allocate memory exclusively for their use, use
+//: 'new'. Memory is a finite resource so if the computer can't satisfy your
+//: request, 'new' may return a 0 (null) address.
+//:
+//: Computers these days have lots of memory so in practice we can often
+//: assume we'll never run out. If you start running out however, say in a
+//: long-running program, you'll need to switch mental gears and start
+//: husbanding our memory more carefully. The most important tool to avoid
+//: wasting memory is to 'abandon' an address when you don't need it anymore.
+//: That frees up the memory allocated to it to be reused in future calls to
+//: 'new'.
+
+//: Since memory can be reused multiple times, it can happen that you have a
+//: stale copy to an address that has since been abandoned and reused. Using
+//: the stale address is almost never safe, but it can be very hard to track
+//: down such copies because any errors caused by them may occur even millions
+//: of instructions after the copy or abandon instruction. To help track down
+//: such issues, Mu tracks an 'alloc id' for each allocation it makes. The
+//: first call to 'new' has an alloc id of 1, the second gets 2, and so on.
+//: The alloc id is never reused.
+:(before "End Globals")
+long long Next_alloc_id = 0;
+:(before "End Reset")
+Next_alloc_id = 0;
+
+//: The 'new' instruction records alloc ids both in the memory being allocated
+//: and *also* in the address. The 'abandon' instruction clears alloc ids in
+//: both places as well. Tracking alloc ids in this manner allows us to raise
+//: errors about stale addresses much earlier: 'lookup' operations always
+//: compare alloc ids between the address and its payload.
+
+//: todo: give 'new' a custodian ingredient. Following malloc/free is a temporary hack.
+
+:(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")
+put(Type_ordinal, "type", 0);
+:(code)
+bool is_mu_type_literal(const reagent& r) {
+  return is_literal(r) && r.type && r.type->name == "type";
+}
+
+:(before "End Primitive Recipe Declarations")
+NEW,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "new", NEW);
+:(before "End Primitive Recipe Checks")
+case NEW: {
+  const recipe& caller = get(Recipe, r);
+  if (inst.ingredients.empty() || SIZE(inst.ingredients) > 2) {
+    raise << maybe(caller.name) << "'new' requires one or two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  // End NEW Check Special-cases
+  const reagent& type = inst.ingredients.at(0);
+  if (!is_mu_type_literal(type)) {
+    raise << maybe(caller.name) << "first ingredient of 'new' should be a type, but got '" << type.original_string << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.ingredients) > 1 && !is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(caller.name) << "second ingredient of 'new' should be a number (array length), but got '" << type.original_string << "'\n" << end();
+    break;
+  }
+  if (inst.products.empty()) {
+    raise << maybe(caller.name) << "result of 'new' should never be ignored\n" << end();
+    break;
+  }
+  if (!product_of_new_is_valid(inst)) {
+    raise << maybe(caller.name) << "product of 'new' has incorrect type: '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  break;
+}
+
+:(code)
+bool product_of_new_is_valid(const instruction& inst) {
+  reagent/*copy*/ product = inst.products.at(0);
+  // Update NEW product in Check
+  if (!product.type || product.type->atom || product.type->left->value != Address_type_ordinal)
+    return false;
+  drop_from_type(product, "address");
+  if (SIZE(inst.ingredients) > 1) {
+    // array allocation
+    if (!product.type || product.type->atom || product.type->left->value != Array_type_ordinal)
+      return false;
+    drop_from_type(product, "array");
+  }
+  reagent/*local*/ expected_product(new_type_tree(inst.ingredients.at(0).name));
+  return types_strictly_match(product, expected_product);
+}
+
+void drop_from_type(reagent& r, string expected_type) {
+  assert(!r.type->atom);
+  if (r.type->left->name != expected_type) {
+    raise << "can't drop2 " << expected_type << " from '" << to_string(r) << "'\n" << end();
+    return;
+  }
+  // r.type = r.type->right
+  type_tree* tmp = r.type;
+  r.type = tmp->right;
+  tmp->right = NULL;
+  delete tmp;
+  // if (!r.type->right) r.type = r.type->left
+  assert(!r.type->atom);
+  if (r.type->right) return;
+  tmp = r.type;
+  r.type = tmp->left;
+  tmp->left = NULL;
+  delete tmp;
+}
+
+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
+//: allocate.
+
+//: Ensure that we never call 'allocate' directly, and that there's no 'new'
+//: instructions left after the transforms have run.
+:(before "End Primitive Recipe Checks")
+case ALLOCATE: {
+  raise << "never call 'allocate' directly'; always use 'new'\n" << end();
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case NEW: {
+  raise << "no implementation for 'new'; why wasn't it translated to 'allocate'? Please save a copy of your program and send it to Kartik.\n" << end();
+  break;
+}
+
+:(after "Transform.push_back(check_instruction)")  // check_instruction will guard against direct 'allocate' instructions below
+Transform.push_back(transform_new_to_allocate);  // idempotent
+
+:(code)
+void transform_new_to_allocate(const recipe_ordinal r) {
+  trace(101, "transform") << "--- convert 'new' to 'allocate' for recipe " << get(Recipe, r).name << end();
+  for (int i = 0;  i < SIZE(get(Recipe, r).steps);  ++i) {
+    instruction& inst = get(Recipe, r).steps.at(i);
+    // Convert 'new' To 'allocate'
+    if (inst.name == "new") {
+      if (inst.ingredients.empty()) return;  // error raised elsewhere
+      inst.operation = ALLOCATE;
+      type_tree* type = new_type_tree(inst.ingredients.at(0).name);
+      inst.ingredients.at(0).set_value(size_of(type));
+      trace(102, "new") << "size of '" << inst.ingredients.at(0).name << "' is " << inst.ingredients.at(0).value << end();
+      delete type;
+    }
+  }
+}
+
+//: implement 'allocate' based on size
+
+:(before "End Globals")
+extern const int Reserved_for_tests = 1000;
+int Memory_allocated_until = Reserved_for_tests;
+int Initial_memory_per_routine = 100000;
+:(before "End Reset")
+Memory_allocated_until = Reserved_for_tests;
+Initial_memory_per_routine = 100000;
+:(before "End routine Fields")
+int alloc, alloc_max;
+:(before "End routine Constructor")
+alloc = Memory_allocated_until;
+Memory_allocated_until += Initial_memory_per_routine;
+alloc_max = Memory_allocated_until;
+trace(Callstack_depth+1, "new") << "routine allocated memory from " << alloc << " to " << alloc_max << end();
+
+:(before "End Primitive Recipe Declarations")
+ALLOCATE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "allocate", ALLOCATE);
+:(before "End Primitive Recipe Implementations")
+case ALLOCATE: {
+  // compute the space we need
+  int size = ingredients.at(0).at(0);
+  int alloc_id = Next_alloc_id;
+  Next_alloc_id++;
+  if (SIZE(ingredients) > 1) {
+    // array allocation
+    trace(Callstack_depth+1, "mem") << "array length is " << ingredients.at(1).at(0) << end();
+    size = /*space for length*/1 + size*ingredients.at(1).at(0);
+  }
+  int result = allocate(size);
+  // initialize alloc-id in payload
+  trace(Callstack_depth+1, "mem") << "storing alloc-id " << alloc_id << " in location " << result << end();
+  put(Memory, result, alloc_id);
+  if (SIZE(current_instruction().ingredients) > 1) {
+    // initialize array length
+    trace(Callstack_depth+1, "mem") << "storing array length " << ingredients.at(1).at(0) << " in location " << result+/*skip alloc id*/1 << end();
+    put(Memory, result+/*skip alloc id*/1, ingredients.at(1).at(0));
+  }
+  products.resize(1);
+  products.at(0).push_back(alloc_id);
+  products.at(0).push_back(result);
+  break;
+}
+:(code)
+int allocate(int size) {
+  // include space for alloc id
+  ++size;
+  trace(Callstack_depth+1, "mem") << "allocating size " << size << end();
+//?   Total_alloc += size;
+//?   ++Num_alloc;
+  // Allocate Special-cases
+  // compute the region of memory to return
+  // really crappy at the moment
+  ensure_space(size);
+  const int result = Current_routine->alloc;
+  trace(Callstack_depth+1, "mem") << "new alloc: " << result << end();
+  // initialize allocated space
+  for (int address = result;  address < result+size;  ++address) {
+    trace(Callstack_depth+1, "mem") << "storing 0 in location " << address << end();
+    put(Memory, address, 0);
+  }
+  Current_routine->alloc += size;
+  // no support yet for reclaiming memory between routines
+  assert(Current_routine->alloc <= Current_routine->alloc_max);
+  return result;
+}
+
+//: statistics for debugging
+//? :(before "End Globals")
+//? int Total_alloc = 0;
+//? int Num_alloc = 0;
+//? int Total_free = 0;
+//? int Num_free = 0;
+//? :(before "End Reset")
+//? if (!Memory.empty()) {
+//?   cerr << Total_alloc << "/" << Num_alloc
+//?        << " vs " << Total_free << "/" << Num_free << '\n';
+//?   cerr << SIZE(Memory) << '\n';
+//? }
+//? Total_alloc = Num_alloc = Total_free = Num_free = 0;
+
+:(code)
+void ensure_space(int size) {
+  if (size > Initial_memory_per_routine) {
+    cerr << "can't allocate " << size << " locations, that's too much compared to " << Initial_memory_per_routine << ".\n";
+    exit(1);
+  }
+  if (Current_routine->alloc + size > Current_routine->alloc_max) {
+    // waste the remaining space and create a new chunk
+    Current_routine->alloc = Memory_allocated_until;
+    Memory_allocated_until += Initial_memory_per_routine;
+    Current_routine->alloc_max = Memory_allocated_until;
+    trace(Callstack_depth+1, "new") << "routine allocated memory from " << Current_routine->alloc << " to " << Current_routine->alloc_max << end();
+  }
+}
+
+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.
+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
+
+:(before "End Primitive Recipe Declarations")
+DEADDRESS,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "deaddress", DEADDRESS);
+:(before "End Primitive Recipe Checks")
+case DEADDRESS: {
+  // primary goal of these checks is to forbid address arithmetic
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_address(inst.ingredients.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'deaddress' requires address ingredients, but got '" << inst.ingredients.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  if (SIZE(inst.products) > SIZE(inst.ingredients)) {
+    raise << maybe(get(Recipe, r).name) << "too many products in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.products);  ++i) {
+    if (!is_real_mu_number(inst.products.at(i))) {
+      raise << maybe(get(Recipe, r).name) << "'deaddress' requires number products, but got '" << inst.products.at(i).original_string << "'\n" << end();
+      goto finish_checking_instruction;
+    }
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case DEADDRESS: {
+  products.resize(SIZE(ingredients));
+  for (int i = 0;  i < SIZE(ingredients);  ++i) {
+    products.at(i).push_back(ingredients.at(i).at(/*skip alloc id*/1));
+  }
+  break;
+}
diff --git a/archive/2.vm/035lookup.cc b/archive/2.vm/035lookup.cc
new file mode 100644
index 00000000..4229651a
--- /dev/null
+++ b/archive/2.vm/035lookup.cc
@@ -0,0 +1,664 @@
+//: Go from an address to the payload it points at using /lookup.
+//:
+//: The tests in this layer use unsafe operations so as to stay decoupled from
+//: 'new'.
+
+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
+:(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
+:(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
+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"
+  );
+}
+
+void canonize(reagent& x) {
+  if (is_literal(x)) return;
+  // Begin canonize(x) Lookups
+  while (has_property(x, "lookup"))
+    lookup_memory(x);
+}
+
+void lookup_memory(reagent& x) {
+  if (!x.type || x.type->atom || x.type->left->value != Address_type_ordinal) {
+    raise << maybe(current_recipe_name()) << "tried to lookup '" << x.original_string << "' but it isn't an address\n" << end();
+    dump_callstack();
+    return;
+  }
+  // compute value
+  if (x.value == 0) {
+    raise << maybe(current_recipe_name()) << "tried to lookup 0\n" << end();
+    dump_callstack();
+    return;
+  }
+  lookup_memory_core(x, /*check_for_null*/true);
+}
+
+void lookup_memory_core(reagent& x, bool check_for_null) {
+  double address = x.value + /*skip alloc id in address*/1;
+  double new_value = get_or_insert(Memory, address);
+  trace(Callstack_depth+1, "mem") << "location " << address << " contains " << no_scientific(new_value) << end();
+  // check for null
+  if (check_for_null && new_value == 0) {
+    if (Current_routine) {
+      raise << maybe(current_recipe_name()) << "tried to lookup 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+      dump_callstack();
+    }
+    else {
+      raise << "tried to lookup 0\n" << end();
+    }
+  }
+  // validate alloc-id
+  double alloc_id_in_address = get_or_insert(Memory, x.value);
+  double alloc_id_in_payload = get_or_insert(Memory, new_value);
+//?   cerr << x.value << ": " << alloc_id_in_address << " vs " << new_value << ": " << alloc_id_in_payload << '\n';
+  if (alloc_id_in_address != alloc_id_in_payload) {
+      raise << maybe(current_recipe_name()) << "address is already abandoned in '" << to_original_string(current_instruction()) << "'\n" << end();
+      dump_callstack();
+  }
+  // all well; complete the lookup
+  x.set_value(new_value+/*skip alloc id in payload*/1);
+  drop_from_type(x, "address");
+  drop_one_lookup(x);
+}
+
+:(after "Begin types_coercible(reagent to, reagent from)")
+if (!canonize_type(to)) return false;
+if (!canonize_type(from)) return false;
+:(after "Begin types_match(reagent to, reagent from)")
+if (!canonize_type(to)) return false;
+if (!canonize_type(from)) return false;
+:(after "Begin types_strictly_match(reagent to, reagent from)")
+if (!canonize_type(to)) return false;
+if (!canonize_type(from)) return false;
+
+:(before "End Preprocess is_mu_array(reagent r)")
+if (!canonize_type(r)) return false;
+
+:(before "End Preprocess is_mu_address(reagent r)")
+if (!canonize_type(r)) return false;
+
+:(before "End Preprocess is_mu_number(reagent r)")
+if (!canonize_type(r)) return false;
+:(before "End Preprocess is_mu_boolean(reagent r)")
+if (!canonize_type(r)) return false;
+:(before "End Preprocess is_mu_character(reagent r)")
+if (!canonize_type(r)) return false;
+
+:(after "Update product While Type-checking Merge")
+if (!canonize_type(product)) continue;
+
+:(before "End Compute Call Ingredient")
+canonize_type(ingredient);
+:(before "End Preprocess NEXT_INGREDIENT product")
+canonize_type(product);
+:(before "End Check RETURN Copy(lhs, rhs)
+canonize_type(lhs);
+canonize_type(rhs);
+
+:(code)
+bool canonize_type(reagent& r) {
+  while (has_property(r, "lookup")) {
+    if (!r.type || r.type->atom || !r.type->left || !r.type->left->atom || r.type->left->value != Address_type_ordinal) {
+      raise << "cannot perform lookup on '" << r.name << "' because it has non-address type " << to_string(r.type) << '\n' << end();
+      return false;
+    }
+    drop_from_type(r, "address");
+    drop_one_lookup(r);
+  }
+  return true;
+}
+
+void drop_one_lookup(reagent& r) {
+  for (vector<pair<string, string_tree*> >::iterator p = r.properties.begin();  p != r.properties.end();  ++p) {
+    if (p->first == "lookup") {
+      r.properties.erase(p);
+      return;
+    }
+  }
+  assert(false);
+}
+
+//: Tedious fixup to support addresses in container/array instructions of previous layers.
+//: Most instructions don't require fixup if they use the 'ingredients' and
+//: 'products' variables in run_current_routine().
+
+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;
+:(after "Update GET product in Check")
+if (!canonize_type(product)) break;
+:(after "Update GET base in Run")
+canonize(base);
+
+:(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;
+:(after "Update PUT offset in Check")
+if (!canonize_type(offset)) break;
+:(after "Update PUT base in Run")
+canonize(base);
+
+:(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);
+if (!canonize_type(p)) break;  // error raised elsewhere
+reagent/*copy*/ i = inst.ingredients.at(0);
+if (!canonize_type(i)) break;  // error raised elsewhere
+if (!types_strictly_match(p, i)) {
+  raise << maybe(get(Recipe, r).name) << "product of 'put' must be first ingredient '" << inst.ingredients.at(0).original_string << "', but got '" << inst.products.at(0).original_string << "'\n" << end();
+  break;
+}
+
+:(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);
+
+:(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);
+
+:(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;
+:(before "Update INDEX index in Check")
+if (!canonize_type(index)) break;
+:(before "Update INDEX product in Check")
+if (!canonize_type(product)) break;
+
+:(before "Update INDEX base in Run")
+canonize(base);
+:(before "Update INDEX index in Run")
+canonize(index);
+
+:(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);
+if (!canonize_type(p)) break;  // error raised elsewhere
+reagent/*copy*/ i = inst.ingredients.at(0);
+if (!canonize_type(i)) break;  // error raised elsewhere
+if (!types_strictly_match(p, i)) {
+  raise << maybe(get(Recipe, r).name) << "product of 'put-index' must be first ingredient '" << inst.ingredients.at(0).original_string << "', but got '" << inst.products.at(0).original_string << "'\n" << end();
+  break;
+}
+
+:(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;
+:(before "Update PUT_INDEX index in Check")
+if (!canonize_type(index)) break;
+:(before "Update PUT_INDEX value in Check")
+if (!canonize_type(value)) break;
+
+:(before "Update PUT_INDEX base in Run")
+canonize(base);
+:(before "Update PUT_INDEX index in Run")
+canonize(index);
+
+:(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);
+
+:(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;
+:(before "Update MAYBE_CONVERT product in Check")
+if (!canonize_type(product)) break;
+:(before "Update MAYBE_CONVERT status in Check")
+if (!canonize_type(status)) break;
+
+:(before "Update MAYBE_CONVERT base in Run")
+canonize(base);
+:(before "Update MAYBE_CONVERT product in Run")
+canonize(product);
+:(before "Update MAYBE_CONVERT status in Run")
+canonize(status);
+
+:(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 '*'
+
+:(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")
+{
+  while (starts_with(name, "*")) {
+    name.erase(0, 1);
+    properties.push_back(pair<string, string_tree*>("lookup", NULL));
+  }
+  if (name.empty())
+    raise << "illegal name '" << original_string << "'\n" << end();
+}
+
+//:: helpers for debugging
+
+:(before "End Primitive Recipe Declarations")
+_DUMP,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$dump", _DUMP);
+:(before "End Primitive Recipe Implementations")
+case _DUMP: {
+  reagent/*copy*/ after_canonize = current_instruction().ingredients.at(0);
+  canonize(after_canonize);
+  cerr << maybe(current_recipe_name()) << current_instruction().ingredients.at(0).name << ' ' << no_scientific(current_instruction().ingredients.at(0).value) << " => " << no_scientific(after_canonize.value) << " => " << no_scientific(get_or_insert(Memory, after_canonize.value)) << '\n';
+  break;
+}
+
+//: grab an address, and then dump its value at intervals
+//: useful for tracking down memory corruption (writing to an out-of-bounds address)
+:(before "End Globals")
+int Bar = -1;
+:(before "End Primitive Recipe Declarations")
+_BAR,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$bar", _BAR);
+:(before "End Primitive Recipe Implementations")
+case _BAR: {
+  if (current_instruction().ingredients.empty()) {
+    if (Bar != -1) cerr << Bar << ": " << no_scientific(get_or_insert(Memory, Bar)) << '\n';
+    else cerr << '\n';
+  }
+  else {
+    reagent/*copy*/ tmp = current_instruction().ingredients.at(0);
+    canonize(tmp);
+    Bar = tmp.value;
+  }
+  break;
+}
diff --git a/archive/2.vm/036abandon.cc b/archive/2.vm/036abandon.cc
new file mode 100644
index 00000000..a1f66523
--- /dev/null
+++ b/archive/2.vm/036abandon.cc
@@ -0,0 +1,153 @@
+//: Reclaiming memory when it's no longer used.
+
+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.
+
+//: Before, suppose variable V contains address A which points to payload P:
+//:   location V contains an alloc-id N
+//:   location V+1 contains A
+//:   location A contains alloc-id N
+//:   location A+1 onwards contains P
+//: Additionally, suppose the head of the free list is initially F.
+//: After abandoning:
+//:   location V contains invalid alloc-id -1
+//:   location V+1 contains 0
+//:   location A contains invalid alloc-id N
+//:   location A+1 contains the previous head of free-list F
+
+:(before "End routine Fields")
+map<int, int> free_list;
+
+:(before "End Primitive Recipe Declarations")
+ABANDON,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "abandon", ABANDON);
+:(before "End Primitive Recipe Checks")
+case ABANDON: {
+  if (!inst.products.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'abandon' shouldn't write to any products in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_mu_address(inst.ingredients.at(i)))
+      raise << maybe(get(Recipe, r).name) << "ingredients of 'abandon' should be addresses, but ingredient " << i << " is '" << to_string(inst.ingredients.at(i)) << '\n' << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case ABANDON: {
+  for (int i = 0;  i < SIZE(current_instruction().ingredients);  ++i) {
+    reagent/*copy*/ ingredient = current_instruction().ingredients.at(i);
+    canonize(ingredient);
+    abandon(get_or_insert(Memory, ingredient.value+/*skip alloc id*/1), payload_size(ingredient));
+//?     cerr << "clear after abandon: " << ingredient.value << '\n';
+    put(Memory, /*alloc id*/ingredient.value, /*invalid*/-1);
+    put(Memory, /*address*/ingredient.value+1, 0);
+  }
+  break;
+}
+
+:(code)
+void abandon(int address, int payload_size) {
+  put(Memory, address, /*invalid alloc-id*/-1);
+//?   cerr << "abandon: " << address << '\n';
+  // clear rest of payload
+  for (int curr = address+1;  curr < address+payload_size;  ++curr)
+    put(Memory, curr, 0);
+  // append existing free list to address
+  trace(Callstack_depth+1, "abandon") << "saving " << address << " in free-list of size " << payload_size << end();
+  put(Memory, address+/*skip invalid alloc-id*/1, get_or_insert(Current_routine->free_list, payload_size));
+  put(Current_routine->free_list, payload_size, address);
+}
+
+int payload_size(reagent/*copy*/ x) {
+  x.properties.push_back(pair<string, string_tree*>("lookup", NULL));
+  lookup_memory_core(x, /*check_for_null*/false);
+  return size_of(x)+/*alloc id*/1;
+}
+
+:(after "Allocate Special-cases")
+if (get_or_insert(Current_routine->free_list, size)) {
+  trace(Callstack_depth+1, "abandon") << "picking up space from free-list of size " << size << end();
+  int result = get_or_insert(Current_routine->free_list, size);
+  trace(Callstack_depth+1, "mem") << "new alloc from free list: " << result << end();
+  put(Current_routine->free_list, size, get_or_insert(Memory, result+/*skip alloc id*/1));
+  // clear 'deleted' tag
+  put(Memory, result, 0);
+  // clear next pointer
+  put(Memory, result+/*skip alloc id*/1, 0);
+  for (int curr = result;  curr < result+size;  ++curr) {
+    if (get_or_insert(Memory, curr) != 0) {
+      raise << maybe(current_recipe_name()) << "memory in free list was not zeroed out: " << curr << '/' << result << "; somebody wrote to us after free!!!\n" << end();
+      break;  // always fatal
+    }
+  }
+  return result;
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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/archive/2.vm/038new_text.cc b/archive/2.vm/038new_text.cc
new file mode 100644
index 00000000..5317d31d
--- /dev/null
+++ b/archive/2.vm/038new_text.cc
@@ -0,0 +1,288 @@
+//: Extend 'new' to handle a unicode string literal argument or 'text'.
+
+//: A Mu text is an address to an array of characters.
+:(before "End Mu Types Initialization")
+put(Type_abbreviations, "text", new_type_tree("&:@:character"));
+
+:(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;
+:(before "Convert 'new' To 'allocate'")
+if (inst.name == "new" && !inst.ingredients.empty() && is_literal_text(inst.ingredients.at(0))) continue;
+:(after "case NEW" following "Primitive Recipe Implementations")
+  if (is_literal_text(current_instruction().ingredients.at(0))) {
+    products.resize(1);
+    products.at(0).push_back(/*alloc id*/0);
+    products.at(0).push_back(new_mu_text(current_instruction().ingredients.at(0).name));
+    trace(Callstack_depth+1, "mem") << "new string alloc: " << products.at(0).at(0) << end();
+    break;
+  }
+
+:(code)
+int new_mu_text(const string& contents) {
+  // allocate an array just large enough for it
+  int string_length = unicode_length(contents);
+//?   Total_alloc += string_length+1;
+//?   ++Num_alloc;
+  int result = allocate(/*array length*/1 + string_length);
+  int curr_address = result;
+  ++curr_address;  // skip alloc id
+  trace(Callstack_depth+1, "mem") << "storing string length " << string_length << " in location " << curr_address << end();
+  put(Memory, curr_address, string_length);
+  ++curr_address;  // skip length
+  int curr = 0;
+  const char* raw_contents = contents.c_str();
+  for (int i = 0;  i < string_length;  ++i) {
+    uint32_t curr_character;
+    assert(curr < SIZE(contents));
+    tb_utf8_char_to_unicode(&curr_character, &raw_contents[curr]);
+    trace(Callstack_depth+1, "mem") << "storing string character " << curr_character << " in location " << curr_address << end();
+    put(Memory, curr_address, curr_character);
+    curr += tb_utf8_char_length(raw_contents[curr]);
+    ++curr_address;
+  }
+  // Mu strings are not null-terminated in memory.
+  return result;
+}
+
+//: a new kind of typo
+
+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
+
+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)) {
+  return read_mu_text(data.at(/*skip alloc id*/1));
+}
+
+:(before "End $print Special-cases")
+else if (is_mu_text(current_instruction().ingredients.at(i))) {
+  cout << read_mu_text(ingredients.at(i).at(/*skip alloc id*/1));
+}
+
+:(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"
+  );
+}
+
+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"
+  );
+}
+
+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
+:(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)
+int unicode_length(const string& s) {
+  const char* in = s.c_str();
+  int result = 0;
+  int curr = 0;
+  while (curr < SIZE(s)) {  // carefully bounds-check on the string
+    // before accessing its raw pointer
+    ++result;
+    curr += tb_utf8_char_length(in[curr]);
+  }
+  return result;
+}
+
+string read_mu_text(int address) {
+  if (address == 0) return "";
+  int length = get_or_insert(Memory, address+/*alloc id*/1);
+  if (length == 0) return "";
+  return read_mu_characters(address+/*alloc id*/1+/*length*/1, length);
+}
+
+string read_mu_characters(int start, int length) {
+  ostringstream tmp;
+  for (int curr = start;  curr < start+length;  ++curr)
+    tmp << to_unicode(static_cast<uint32_t>(get_or_insert(Memory, curr)));
+  return tmp.str();
+}
+
+//:: some miscellaneous helpers now that we have text
+
+//: assert: perform sanity checks at runtime
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "assert", ASSERT);
+:(before "End Primitive Recipe Checks")
+case ASSERT: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'assert' takes exactly two ingredients rather than '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_address(inst.ingredients.at(0)) && !is_mu_scalar(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'assert' requires a scalar or address for its first ingredient, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (!is_literal_text(inst.ingredients.at(1)) && !is_mu_text(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "'assert' requires a text as its second ingredient, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case ASSERT: {
+  if (!scalar_ingredient(ingredients, 0)) {
+    if (is_literal_text(current_instruction().ingredients.at(1)))
+      raise << current_instruction().ingredients.at(1).name << '\n' << end();
+    else
+      raise << read_mu_text(ingredients.at(1).at(/*skip alloc id*/1)) << '\n' << end();
+    if (!Hide_errors) exit(1);
+  }
+  break;
+}
+
+//: 'cheating' by using the host system
+
+:(before "End Primitive Recipe Declarations")
+_READ,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$read", _READ);
+:(before "End Primitive Recipe Checks")
+case _READ: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _READ: {
+  skip_whitespace(cin);
+  string result;
+  if (has_data(cin))
+    cin >> result;
+  products.resize(1);
+  products.at(0).push_back(new_mu_text(result));
+  break;
+}
+
+:(code)
+void skip_whitespace(istream& in) {
+  while (true) {
+    if (!has_data(in)) break;
+    if (isspace(in.peek())) in.get();
+    else break;
+  }
+}
diff --git a/archive/2.vm/040brace.cc b/archive/2.vm/040brace.cc
new file mode 100644
index 00000000..89ff7b01
--- /dev/null
+++ b/archive/2.vm/040brace.cc
@@ -0,0 +1,566 @@
+//: Structured programming
+//:
+//: Our jump recipes are quite inconvenient to use, so Mu provides a
+//: lightweight tool called 'transform_braces' to work in a slightly more
+//: convenient format with nested braces:
+//:
+//:   {
+//:     some instructions
+//:     {
+//:       more instructions
+//:     }
+//:   }
+//:
+//: Braces are just labels, they require no special parsing. The pseudo
+//: instructions 'loop' and 'break' jump to just after the enclosing '{' and
+//: '}' respectively.
+//:
+//: Conditional and unconditional 'loop' and 'break' should give us 80% of the
+//: benefits of the control-flow primitives we're used to in other languages,
+//: like 'if', 'while', 'for', etc.
+
+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
+
+:(code)
+void transform_braces(const recipe_ordinal r) {
+  const bool OPEN = false, CLOSE = true;
+  // use signed integer for step index because we'll be doing arithmetic on it
+  list<pair<bool/*OPEN/CLOSE*/, /*step*/int> > braces;
+  trace(101, "transform") << "--- transform braces for recipe " << get(Recipe, r).name << end();
+  for (int index = 0;  index < SIZE(get(Recipe, r).steps);  ++index) {
+    const instruction& inst = get(Recipe, r).steps.at(index);
+    if (inst.label == "{") {
+      trace(103, "transform") << maybe(get(Recipe, r).name) << "push (open, " << index << ")" << end();
+      braces.push_back(pair<bool,int>(OPEN, index));
+    }
+    if (inst.label == "}") {
+      trace(103, "transform") << "push (close, " << index << ")" << end();
+      braces.push_back(pair<bool,int>(CLOSE, index));
+    }
+  }
+  stack</*step*/int> open_braces;
+  for (int index = 0;  index < SIZE(get(Recipe, r).steps);  ++index) {
+    instruction& inst = get(Recipe, r).steps.at(index);
+    if (inst.label == "{") {
+      open_braces.push(index);
+      continue;
+    }
+    if (inst.label == "}") {
+      if (open_braces.empty()) {
+        raise << maybe(get(Recipe, r).name) << "unbalanced '}'\n" << end();
+        return;
+      }
+      open_braces.pop();
+      continue;
+    }
+    if (inst.is_label) continue;
+    if (inst.name != "loop"
+         && inst.name != "loop-if"
+         && inst.name != "loop-unless"
+         && inst.name != "break"
+         && inst.name != "break-if"
+         && inst.name != "break-unless") {
+      trace(102, "transform") << inst.name << " ..." << end();
+      continue;
+    }
+    // check for errors
+    if (inst.name.find("-if") != string::npos || inst.name.find("-unless") != string::npos) {
+      if (inst.ingredients.empty()) {
+        raise << maybe(get(Recipe, r).name) << "'" << inst.name << "' expects 1 or 2 ingredients, but got none\n" << end();
+        continue;
+      }
+    }
+    // update instruction operation
+    string old_name = inst.name;  // save a copy
+    if (inst.name.find("-if") != string::npos) {
+      inst.name = "jump-if";
+      inst.operation = JUMP_IF;
+    }
+    else if (inst.name.find("-unless") != string::npos) {
+      inst.name = "jump-unless";
+      inst.operation = JUMP_UNLESS;
+    }
+    else {
+      inst.name = "jump";
+      inst.operation = JUMP;
+    }
+    // check for explicitly provided targets
+    if (inst.name.find("-if") != string::npos || inst.name.find("-unless") != string::npos) {
+      // conditional branches check arg 1
+      if (SIZE(inst.ingredients) > 1 && is_literal(inst.ingredients.at(1))) {
+        trace(102, "transform") << inst.name << ' ' << inst.ingredients.at(1).name << ":offset" << end();
+        continue;
+      }
+    }
+    else {
+      // unconditional branches check arg 0
+      if (!inst.ingredients.empty() && is_literal(inst.ingredients.at(0))) {
+        trace(102, "transform") << "jump " << inst.ingredients.at(0).name << ":offset" << end();
+        continue;
+      }
+    }
+    // if implicit, compute target
+    reagent target(new type_tree("offset"));
+    target.set_value(0);
+    if (open_braces.empty())
+      raise << maybe(get(Recipe, r).name) << "'" << old_name << "' needs a '{' before\n" << end();
+    else if (old_name.find("loop") != string::npos)
+      target.set_value(open_braces.top()-index);
+    else  // break instruction
+      target.set_value(matching_brace(open_braces.top(), braces, r) - index - 1);
+    inst.ingredients.push_back(target);
+    // log computed target
+    if (inst.name == "jump")
+      trace(102, "transform") << "jump " << no_scientific(target.value) << ":offset" << end();
+    else
+      trace(102, "transform") << inst.name << ' ' << inst.ingredients.at(0).name << ", " << no_scientific(target.value) << ":offset" << end();
+  }
+}
+
+// returns a signed integer not just so that we can return -1 but also to
+// enable future signed arithmetic
+int matching_brace(int index, const list<pair<bool, int> >& braces, recipe_ordinal r) {
+  int stacksize = 0;
+  for (list<pair<bool, int> >::const_iterator p = braces.begin();  p != braces.end();  ++p) {
+    if (p->second < index) continue;
+    stacksize += (p->first ? 1 : -1);
+    if (stacksize == 0) return p->second;
+  }
+  raise << maybe(get(Recipe, r).name) << "unbalanced '{'\n" << end();
+  return SIZE(get(Recipe, r).steps);  // exit current routine
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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
+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"
+  );
+}
+
+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"
+  );
+}
+
+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.
+
+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"
+  );
+}
+
+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
+//   ```
+//   {
+//     break-unless a
+//     return b, c, ...
+//   }
+//   ```
+if (curr.name == "return-if" || curr.name == "reply-if") {
+  if (curr.products.empty()) {
+    emit_return_block(result, "break-unless", curr);
+    curr.clear();
+  }
+  else {
+    raise << "'" << curr.name << "' never yields any products\n" << end();
+  }
+}
+// rewrite 'return-unless a, b, c, ...' to
+//   ```
+//   {
+//     break-if a
+//     return b, c, ...
+//   }
+//   ```
+if (curr.name == "return-unless" || curr.name == "reply-unless") {
+  if (curr.products.empty()) {
+    emit_return_block(result, "break-if", curr);
+    curr.clear();
+  }
+  else {
+    raise << "'" << curr.name << "' never yields any products\n" << end();
+  }
+}
+
+:(code)
+void emit_return_block(recipe& out, const string& break_command, const instruction& inst) {
+  const vector<reagent>& ingredients = inst.ingredients;
+  reagent/*copy*/ condition = ingredients.at(0);
+  vector<reagent> return_ingredients;
+  copy(++ingredients.begin(), ingredients.end(), inserter(return_ingredients, return_ingredients.end()));
+
+  // {
+  instruction open_label;  open_label.is_label=true;  open_label.label = "{";
+  out.steps.push_back(open_label);
+
+  // <break command> <condition>
+  instruction break_inst;
+  break_inst.operation = get(Recipe_ordinal, break_command);
+  break_inst.name = break_command;
+  break_inst.ingredients.push_back(condition);
+  out.steps.push_back(break_inst);
+
+  // return <return ingredients>
+  instruction return_inst;
+  return_inst.operation = get(Recipe_ordinal, "return");
+  return_inst.name = "return";
+  return_inst.ingredients.swap(return_ingredients);
+  return_inst.original_string = inst.original_string;
+  out.steps.push_back(return_inst);
+
+  // }
+  instruction close_label;  close_label.is_label=true;  close_label.label = "}";
+  out.steps.push_back(close_label);
+}
+
+//: Make sure these pseudo recipes get consistent numbers in all tests, even
+//: though they aren't implemented. Allows greater flexibility in ordering
+//: transforms.
+
+:(before "End Primitive Recipe Declarations")
+BREAK,
+BREAK_IF,
+BREAK_UNLESS,
+LOOP,
+LOOP_IF,
+LOOP_UNLESS,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "break", BREAK);
+put(Recipe_ordinal, "break-if", BREAK_IF);
+put(Recipe_ordinal, "break-unless", BREAK_UNLESS);
+put(Recipe_ordinal, "loop", LOOP);
+put(Recipe_ordinal, "loop-if", LOOP_IF);
+put(Recipe_ordinal, "loop-unless", LOOP_UNLESS);
+:(before "End Primitive Recipe Checks")
+case BREAK: break;
+case BREAK_IF: break;
+case BREAK_UNLESS: break;
+case LOOP: break;
+case LOOP_IF: break;
+case LOOP_UNLESS: break;
diff --git a/archive/2.vm/041jump_target.cc b/archive/2.vm/041jump_target.cc
new file mode 100644
index 00000000..ec7adede
--- /dev/null
+++ b/archive/2.vm/041jump_target.cc
@@ -0,0 +1,220 @@
+//: Support jumps to a specific label (the 'jump target') in the same recipe.
+//: Jump targets must be unique and unambiguous within any recipe.
+//:
+//: The 'break' and 'loop' pseudo instructions can also take jump targets.
+//: Which instruction you use is just documentation about intent; use 'break'
+//: to indicate you're exiting one or more loop nests, and 'loop' to indicate
+//: you're skipping to the next iteration of some containing loop nest.
+
+//: Since they have to be unique in a recipe, not all labels can be jump
+//: targets.
+bool is_jump_target(const string& label) {
+  if (label == "{" || label == "}") return false;
+  // End is_jump_target Special-cases
+  return is_label_word(label);
+}
+
+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);
+
+:(before "End Instruction Modifying Transforms")
+Transform.push_back(transform_labels);  // idempotent
+
+:(code)
+void transform_labels(const recipe_ordinal r) {
+  map<string, int> offset;
+  for (int i = 0;  i < SIZE(get(Recipe, r).steps);  ++i) {
+    const instruction& inst = get(Recipe, r).steps.at(i);
+    if (!inst.is_label) continue;
+    if (is_jump_target(inst.label)) {
+      if (!contains_key(offset, inst.label)) {
+        put(offset, inst.label, i);
+      }
+      else {
+        raise << maybe(get(Recipe, r).name) << "duplicate label '" << inst.label << "'" << end();
+        // have all jumps skip some random but noticeable and deterministic amount of code
+        put(offset, inst.label, 9999);
+      }
+    }
+  }
+  for (int i = 0;  i < SIZE(get(Recipe, r).steps);  ++i) {
+    instruction& inst = get(Recipe, r).steps.at(i);
+    if (inst.name == "jump") {
+      if (inst.ingredients.empty()) {
+        raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' expects an ingredient but got 0\n" << end();
+        return;
+      }
+      replace_offset(inst.ingredients.at(0), offset, i, r);
+    }
+    if (inst.name == "jump-if" || inst.name == "jump-unless") {
+      if (SIZE(inst.ingredients) < 2) {
+        raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' expects 2 ingredients but got " << SIZE(inst.ingredients) << '\n' << end();
+        return;
+      }
+      replace_offset(inst.ingredients.at(1), offset, i, r);
+    }
+    if ((inst.name == "loop" || inst.name == "break")
+        && SIZE(inst.ingredients) >= 1) {
+      replace_offset(inst.ingredients.at(0), offset, i, r);
+    }
+    if ((inst.name == "loop-if" || inst.name == "loop-unless"
+            || inst.name == "break-if" || inst.name == "break-unless")
+        && SIZE(inst.ingredients) >= 2) {
+      replace_offset(inst.ingredients.at(1), offset, i, r);
+    }
+  }
+}
+
+void replace_offset(reagent& x, /*const*/ map<string, int>& offset, const int current_offset, const recipe_ordinal r) {
+  if (!is_literal(x)) {
+    raise << maybe(get(Recipe, r).name) << "jump target must be offset or label but is '" << x.original_string << "'\n" << end();
+    x.set_value(0);  // no jump by default
+    return;
+  }
+  if (x.initialized) return;
+  if (is_integer(x.name)) return;  // non-labels will be handled like other number operands
+  if (!is_jump_target(x.name)) {
+    raise << maybe(get(Recipe, r).name) << "can't jump to label '" << x.name << "'\n" << end();
+    x.set_value(0);  // no jump by default
+    return;
+  }
+  if (!contains_key(offset, x.name)) {
+    raise << maybe(get(Recipe, r).name) << "can't find label '" << x.name << "'\n" << end();
+    x.set_value(0);  // no jump by default
+    return;
+  }
+  x.set_value(get(offset, x.name) - current_offset);
+}
+
+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");
+}
+
+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");
+}
+
+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");
+}
+
+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");
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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/archive/2.vm/042name.cc b/archive/2.vm/042name.cc
new file mode 100644
index 00000000..557469d0
--- /dev/null
+++ b/archive/2.vm/042name.cc
@@ -0,0 +1,414 @@
+//: A big convenience high-level languages provide is the ability to name memory
+//: locations. In Mu, a transform called 'transform_names' provides this
+//: convenience.
+
+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
+
+:(before "End Globals")
+map<recipe_ordinal, map<string, int> > Name;
+
+//: the Name map is a global, so save it before tests and reset it for every
+//: test, just to be safe.
+:(before "End Globals")
+map<recipe_ordinal, map<string, int> > Name_snapshot;
+:(before "End save_snapshots")
+Name_snapshot = Name;
+:(before "End restore_snapshots")
+Name = Name_snapshot;
+
+:(code)
+void transform_names(const recipe_ordinal r) {
+  recipe& caller = get(Recipe, r);
+  trace(101, "transform") << "--- transform names for recipe " << caller.name << end();
+  bool names_used = false;
+  bool numeric_locations_used = false;
+  map<string, int>& names = Name[r];
+  // record the indices 'used' so far in the map
+  int& curr_idx = names[""];
+  // reserve indices 0 and 1 for the chaining slot in a later layer.
+  // transform_names may get called multiple times in later layers, so
+  // curr_idx may already be set.
+  if (curr_idx < 2) curr_idx = 2;
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    instruction& inst = caller.steps.at(i);
+    // End transform_names(inst) Special-cases
+    // map names to addresses
+    for (int in = 0;  in < SIZE(inst.ingredients);  ++in) {
+      reagent& ingredient = inst.ingredients.at(in);
+      if (is_disqualified(ingredient, inst, caller.name)) continue;
+      if (is_numeric_location(ingredient)) numeric_locations_used = true;
+      if (is_named_location(ingredient)) names_used = true;
+      if (is_integer(ingredient.name)) continue;
+      if (!already_transformed(ingredient, names)) {
+        raise << maybe(caller.name) << "tried to read ingredient '" << ingredient.name << "' in '" << to_original_string(inst) << "' but it hasn't been written to yet\n" << end();
+        // use-before-set Error
+        return;
+      }
+      int v = lookup_name(ingredient, r);
+      if (v >= 0) {
+        ingredient.set_value(v);
+        // Done Placing Ingredient(ingredient, inst, caller)
+      }
+      else {
+        raise << maybe(caller.name) << "can't find a place to store '" << ingredient.name << "'\n" << end();
+        return;
+      }
+    }
+    for (int out = 0;  out < SIZE(inst.products);  ++out) {
+      reagent& product = inst.products.at(out);
+      if (is_disqualified(product, inst, caller.name)) continue;
+      if (is_numeric_location(product)) numeric_locations_used = true;
+      if (is_named_location(product)) names_used = true;
+      if (is_integer(product.name)) continue;
+      if (names.find(product.name) == names.end()) {
+        trace(103, "name") << "assign " << product.name << " " << curr_idx << end();
+        names[product.name] = curr_idx;
+        curr_idx += size_of(product);
+      }
+      int v = lookup_name(product, r);
+      if (v >= 0) {
+        product.set_value(v);
+        // Done Placing Product(product, inst, caller)
+      }
+      else {
+        raise << maybe(caller.name) << "can't find a place to store '" << product.name << "'\n" << end();
+        return;
+      }
+    }
+  }
+  if (names_used && numeric_locations_used)
+    raise << maybe(caller.name) << "mixing variable names and numeric addresses\n" << end();
+}
+
+bool is_disqualified(/*mutable*/ reagent& x, const instruction& inst, const string& recipe_name) {
+  if (!x.type) {
+    raise << maybe(recipe_name) << "missing type for '" << x.original_string << "' in '" << to_original_string(inst) << "'\n" << end();
+    // missing-type Error 1
+    return true;
+  }
+  if (is_raw(x)) return true;
+  if (is_literal(x)) return true;
+  // End is_disqualified Special-cases
+  if (x.initialized) return true;
+  return false;
+}
+
+bool already_transformed(const reagent& r, const map<string, int>& names) {
+  return contains_key(names, r.name);
+}
+
+int lookup_name(const reagent& r, const recipe_ordinal default_recipe) {
+  return Name[default_recipe][r.name];
+}
+
+type_ordinal skip_addresses(type_tree* type) {
+  while (type && is_compound_type_starting_with(type, "address"))
+    type = type->right;
+  if (!type) return -1;  // error handled elsewhere
+  if (type->atom) return type->value;
+  const type_tree* base_type = type;
+  // Update base_type in skip_addresses
+  if (base_type->atom)
+    return base_type->value;
+  assert(base_type->left->atom);
+  return base_type->left->value;
+}
+
+bool is_compound_type_starting_with(const type_tree* type, const string& expected_name) {
+  if (!type) return false;
+  if (type->atom) return false;
+  if (!type->left->atom) return false;
+  return type->left->value == get(Type_ordinal, expected_name);
+}
+
+int find_element_offset(const type_ordinal t, const string& name, const string& recipe_name) {
+  const type_info& container = get(Type, t);
+  for (int i = 0;  i < SIZE(container.elements);  ++i)
+    if (container.elements.at(i).name == name) return i;
+  raise << maybe(recipe_name) << "unknown element '" << name << "' in container '" << get(Type, t).name << "'\n" << end();
+  return -1;
+}
+int find_element_location(int base_address, const string& name, const type_tree* type, const string& recipe_name) {
+  int offset = find_element_offset(get_base_type(type)->value, name, recipe_name);
+  if (offset == -1) return offset;
+  int result = base_address;
+  for (int i = 0; i < offset; ++i)
+    result += size_of(element_type(type, i));
+  return result;
+}
+
+bool is_numeric_location(const reagent& x) {
+  if (is_literal(x)) return false;
+  if (is_raw(x)) return false;
+  if (x.name == "0") return false;  // used for chaining lexical scopes
+  return is_integer(x.name);
+}
+
+bool is_named_location(const reagent& x) {
+  if (is_literal(x)) return false;
+  if (is_raw(x)) return false;
+  if (is_special_name(x.name)) return false;
+  return !is_integer(x.name);
+}
+
+// all names here should either be disqualified or also in bind_special_scenario_names
+bool is_special_name(const string& s) {
+  if (s == "_") return true;
+  if (s == "0") return true;
+  // End is_special_name Special-cases
+  return false;
+}
+
+bool is_raw(const reagent& r) {
+  return has_property(r, "raw");
+}
+
+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
+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)
+
+:(before "End update GET offset_value in Check")
+else {
+  if (!offset.initialized) {
+    raise << maybe(get(Recipe, r).name) << "uninitialized offset '" << offset.name << "' in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  offset_value = offset.value;
+}
+
+:(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
+if (inst.name == "get" || inst.name == "get-location" || inst.name == "put") {
+  //: avoid raising any errors here; later layers will support overloading new
+  //: instructions with the same names (static dispatch), which could lead to
+  //: spurious errors
+  if (SIZE(inst.ingredients) < 2)
+    break;  // error raised elsewhere
+  if (!is_literal(inst.ingredients.at(1)))
+    break;  // error raised elsewhere
+  if (inst.ingredients.at(1).name.find_first_not_of("0123456789") != string::npos) {
+    // since first non-address in base type must be a container, we don't have to canonize
+    type_ordinal base_type = skip_addresses(inst.ingredients.at(0).type);
+    if (contains_key(Type, base_type)) {  // otherwise we'll raise an error elsewhere
+      inst.ingredients.at(1).set_value(find_element_offset(base_type, inst.ingredients.at(1).name, get(Recipe, r).name));
+      trace(103, "name") << "element " << inst.ingredients.at(1).name << " of type " << get(Type, base_type).name << " is at offset " << no_scientific(inst.ingredients.at(1).value) << end();
+    }
+  }
+}
+
+:(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'.
+
+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
+if (inst.name == "maybe-convert") {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "exactly 2 ingredients expected in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  assert(is_literal(inst.ingredients.at(1)));
+  if (inst.ingredients.at(1).name.find_first_not_of("0123456789") != string::npos) {
+    // since first non-address in base type must be an exclusive container, we don't have to canonize
+    type_ordinal base_type = skip_addresses(inst.ingredients.at(0).type);
+    if (contains_key(Type, base_type)) {  // otherwise we'll raise an error elsewhere
+      inst.ingredients.at(1).set_value(find_element_offset(base_type, inst.ingredients.at(1).name, get(Recipe, r).name));
+      trace(103, "name") << "variant " << inst.ingredients.at(1).name << " of type " << get(Type, base_type).name << " has tag " << no_scientific(inst.ingredients.at(1).value) << end();
+    }
+  }
+}
+
+:(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/archive/2.vm/043space.cc b/archive/2.vm/043space.cc
new file mode 100644
index 00000000..bd931701
--- /dev/null
+++ b/archive/2.vm/043space.cc
@@ -0,0 +1,331 @@
+//: Spaces help isolate recipes from each other. You can create them at will,
+//: and all addresses in arguments are implicitly based on the 'default-space'
+//: (unless they have the /raw property)
+//:
+//: Spaces are often called 'scopes' in other languages. Stack frames are a
+//: limited form of space that can't outlive callers.
+//:
+//: Warning: messing with 'default-space' can corrupt memory. Don't share
+//: default-space between recipes. Later we'll see how to chain spaces safely.
+//:
+//: Tests in this layer can write to a location as part of one type, and read
+//: it as part of another. This is unsafe and insecure, and we'll stop doing
+//: this once we switch to variable names.
+
+//: Under the hood, a space is an array of locations in memory.
+:(before "End Mu Types Initialization")
+put(Type_abbreviations, "space", new_type_tree("address:array:location"));
+
+:(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"
+  );
+}
+
+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'
+
+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")
+  x.initialized = true;
+:(before "End is_special_name Special-cases")
+if (s == "default-space") return true;
+
+//: core implementation
+
+:(before "End call Fields")
+int default_space;
+:(before "End call Constructor")
+default_space = 0;
+
+:(before "Begin canonize(x) Lookups")
+absolutize(x);
+:(code)
+void absolutize(reagent& x) {
+  if (is_raw(x) || is_dummy(x)) return;
+  if (x.name == "default-space") return;
+  if (!x.initialized)
+    raise << to_original_string(current_instruction()) << ": reagent not initialized: '" << x.original_string << "'\n" << end();
+  x.set_value(address(x.value, space_base(x)));
+  x.properties.push_back(pair<string, string_tree*>("raw", NULL));
+  assert(is_raw(x));
+}
+
+//: hook replaced in a later layer
+int space_base(const reagent& x) {
+  return current_call().default_space ? (current_call().default_space + /*skip alloc id*/1) : 0;
+}
+
+int address(int offset, int base) {
+  assert(offset >= 0);
+  if (base == 0) return offset;  // raw
+  int size = get_or_insert(Memory, base);
+  if (offset >= size) {
+    // todo: test
+    raise << current_recipe_name() << ": location " << offset << " is out of bounds " << size << " at " << base << '\n' << end();
+    DUMP("");
+    exit(1);
+    return 0;
+  }
+  return base + /*skip length*/1 + offset;
+}
+
+//: reads and writes to the 'default-space' variable have special behavior
+
+:(after "Begin Preprocess write_memory(x, data)")
+if (x.name == "default-space") {
+  if (!is_mu_space(x))
+    raise << maybe(current_recipe_name()) << "'default-space' should be of type address:array:location, but is " << to_string(x.type) << '\n' << end();
+  if (SIZE(data) != 2)
+    raise << maybe(current_recipe_name()) << "'default-space' getting data from non-address\n" << end();
+  current_call().default_space = data.at(/*skip alloc id*/1);
+  return;
+}
+:(code)
+bool is_mu_space(reagent/*copy*/ x) {
+  canonize_type(x);
+  if (!is_compound_type_starting_with(x.type, "address")) return false;
+  drop_from_type(x, "address");
+  if (!is_compound_type_starting_with(x.type, "array")) return false;
+  drop_from_type(x, "array");
+  return x.type && x.type->atom && x.type->name == "location";
+}
+
+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") {
+  vector<double> result;
+  result.push_back(/*alloc id*/0);
+  result.push_back(current_call().default_space);
+  return result;
+}
+
+//:: fix 'get'
+
+:(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'
+
+:(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));
+
+//:: 'local-scope' is a convenience operation to automatically deduce
+//:: the amount of space to allocate in a default space with names
+
+:(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")
+  x.initialized = true;
+:(before "End is_special_name Special-cases")
+if (s == "number-of-locals") return true;
+
+:(before "End Rewrite Instruction(curr, recipe result)")
+// rewrite 'local-scope' to
+//   ```
+//   default-space:space <- new location:type, number-of-locals:literal
+//   ```
+// where number-of-locals is Name[recipe][""]
+if (curr.name == "local-scope") {
+  rewrite_default_space_instruction(curr);
+}
+:(code)
+void rewrite_default_space_instruction(instruction& curr) {
+  if (!curr.ingredients.empty())
+    raise << "'" << to_original_string(curr) << "' can't take any ingredients\n" << end();
+  curr.name = "new";
+  curr.ingredients.push_back(reagent("location:type"));
+  curr.ingredients.push_back(reagent("number-of-locals:literal"));
+  if (!curr.products.empty())
+    raise << "local-scope can't take any results\n" << end();
+  curr.products.push_back(reagent("default-space:space"));
+}
+:(after "Begin Preprocess read_memory(x)")
+if (x.name == "number-of-locals") {
+  vector<double> result;
+  result.push_back(Name[get(Recipe_ordinal, current_recipe_name())][""]);
+  if (result.back() == 0)
+    raise << "no space allocated for default-space in recipe " << current_recipe_name() << "; are you using names?\n" << end();
+  return result;
+}
+:(after "Begin Preprocess write_memory(x, data)")
+if (x.name == "number-of-locals") {
+  raise << maybe(current_recipe_name()) << "can't write to special name 'number-of-locals'\n" << end();
+  return;
+}
+
+//:: all recipes must set default-space one way or another
+
+:(before "End Globals")
+bool Hide_missing_default_space_errors = true;
+:(before "End Checks")
+Transform.push_back(check_default_space);  // idempotent
+:(code)
+void check_default_space(const recipe_ordinal r) {
+  if (Hide_missing_default_space_errors) return;  // skip previous core tests; this is only for Mu code
+  const recipe& caller = get(Recipe, r);
+  // End check_default_space Special-cases
+  // assume recipes with only numeric addresses know what they're doing (usually tests)
+  if (!contains_non_special_name(r)) return;
+  trace(101, "transform") << "--- check that recipe " << caller.name << " sets default-space" << end();
+  if (caller.steps.empty()) return;
+  if (!starts_by_setting_default_space(caller))
+    raise << caller.name << " does not seem to start with 'local-scope' or 'default-space'\n" << end();
+}
+bool starts_by_setting_default_space(const recipe& r) {
+  return !r.steps.empty()
+      && !r.steps.at(0).products.empty()
+      && r.steps.at(0).products.at(0).name == "default-space";
+}
+
+:(after "Load Mu Prelude")
+Hide_missing_default_space_errors = false;
+:(after "Test Runs")
+Hide_missing_default_space_errors = true;
+:(after "Running Main")
+Hide_missing_default_space_errors = false;
+
+:(code)
+bool contains_non_special_name(const recipe_ordinal r) {
+  for (map<string, int>::iterator p = Name[r].begin();  p != Name[r].end();  ++p) {
+    if (p->first.empty()) continue;
+    if (p->first.find("stash_") == 0) continue;  // generated by rewrite_stashes_to_text (cross-layer)
+    if (!is_special_name(p->first))
+      return true;
+  }
+  return false;
+}
diff --git a/archive/2.vm/044space_surround.cc b/archive/2.vm/044space_surround.cc
new file mode 100644
index 00000000..5a4afb5e
--- /dev/null
+++ b/archive/2.vm/044space_surround.cc
@@ -0,0 +1,79 @@
+//: So far you can have global variables by not setting default-space, and
+//: local variables by setting default-space. You can isolate variables
+//: between those extremes by creating 'surrounding' spaces.
+//:
+//: (Surrounding spaces are like lexical scopes in other languages.)
+
+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
+//: one.
+
+:(replace{} "int space_base(const reagent& x)")
+int space_base(const reagent& x) {
+  int base = current_call().default_space ? (current_call().default_space+/*skip alloc id*/1) : 0;
+  return space_base(x, space_index(x), base);
+}
+
+int space_base(const reagent& x, int space_index, int base) {
+  if (space_index == 0)
+    return base;
+  double chained_space_address = base+/*skip length*/1+/*skip alloc id of chaining slot*/1;
+  double chained_space_base = get_or_insert(Memory, chained_space_address) + /*skip alloc id of chained space*/1;
+  return space_base(x, space_index-1, chained_space_base);
+}
+
+int space_index(const reagent& x) {
+  for (int i = 0;  i < SIZE(x.properties);  ++i) {
+    if (x.properties.at(i).first == "space") {
+      if (!x.properties.at(i).second || x.properties.at(i).second->right)
+        raise << maybe(current_recipe_name()) << "/space metadata should take exactly one value in '" << x.original_string << "'\n" << end();
+      return to_integer(x.properties.at(i).second->value);
+    }
+  }
+  return 0;
+}
+
+:(code)
+void test_permit_space_as_variable_name() {
+  run(
+      "def main [\n"
+      "  space:num <- copy 0\n"
+      "]\n"
+  );
+}
diff --git a/archive/2.vm/045closure_name.cc b/archive/2.vm/045closure_name.cc
new file mode 100644
index 00000000..d5f26f81
--- /dev/null
+++ b/archive/2.vm/045closure_name.cc
@@ -0,0 +1,204 @@
+//: Writing to a literal (not computed) address of 0 in a recipe chains two
+//: spaces together. When a variable has a property of /space:1, it looks up
+//: the variable in the chained/surrounding space. /space:2 looks up the
+//: surrounding space of the surrounding space, etc.
+//:
+//: todo: warn on default-space abuse. default-space for one recipe should
+//: never come from another, otherwise memory will be corrupted.
+
+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.
+
+:(before "End Globals")
+map<recipe_ordinal, recipe_ordinal> Surrounding_space;  // internal to transform; no need to snapshot
+:(before "End Reset")
+Surrounding_space.clear();
+
+:(before "Begin Type Modifying Transforms")
+Transform.push_back(collect_surrounding_spaces);  // idempotent
+
+:(code)
+void collect_surrounding_spaces(const recipe_ordinal r) {
+  trace(101, "transform") << "--- collect surrounding spaces for recipe " << get(Recipe, r).name << end();
+  for (int i = 0;  i < SIZE(get(Recipe, r).steps);  ++i) {
+    const instruction& inst = get(Recipe, r).steps.at(i);
+    if (inst.is_label) continue;
+    for (int j = 0;  j < SIZE(inst.products);  ++j) {
+      if (is_literal(inst.products.at(j))) continue;
+      if (inst.products.at(j).name != "0") continue;
+      if (!is_mu_space(inst.products.at(j))) {
+        raise << "slot 0 should always have type address:array:location, but is '" << to_string(inst.products.at(j)) << "'\n" << end();
+        continue;
+      }
+      string_tree* s = property(inst.products.at(j), "names");
+      if (!s) {
+        raise << "slot 0 requires a /names property in recipe '" << get(Recipe, r).name << "'\n" << end();
+        continue;
+      }
+      if (!s->atom) raise << "slot 0 should have a single value in /names, but got '" << to_string(inst.products.at(j)) << "'\n" << end();
+      const string& surrounding_recipe_name = s->value;
+      if (surrounding_recipe_name.empty()) {
+        raise << "slot 0 doesn't initialize its /names property in recipe '" << get(Recipe, r).name << "'\n" << end();
+        continue;
+      }
+      if (contains_key(Surrounding_space, r)
+          && get(Surrounding_space, r) != get(Recipe_ordinal, surrounding_recipe_name)) {
+        raise << "recipe '" << get(Recipe, r).name << "' can have only one 'surrounding' recipe but has '" << get(Recipe, get(Surrounding_space, r)).name << "' and '" << surrounding_recipe_name << "'\n" << end();
+        continue;
+      }
+      trace(103, "name") << "lexically surrounding space for recipe " << get(Recipe, r).name << " comes from " << surrounding_recipe_name << end();
+      if (!contains_key(Recipe_ordinal, surrounding_recipe_name)) {
+        raise << "can't find recipe providing surrounding space for '" << get(Recipe, r).name << "'; looking for '" << surrounding_recipe_name << "'\n" << end();
+        continue;
+      }
+      put(Surrounding_space, r, get(Recipe_ordinal, surrounding_recipe_name));
+    }
+  }
+}
+
+//: Once surrounding spaces are available, transform_names uses them to handle
+//: /space properties.
+
+:(replace{} "int lookup_name(const reagent& r, const recipe_ordinal default_recipe)")
+int lookup_name(const reagent& x, const recipe_ordinal default_recipe) {
+  if (!has_property(x, "space")) {
+    if (Name[default_recipe].empty()) raise << "name not found: " << x.name << '\n' << end();
+    return Name[default_recipe][x.name];
+  }
+  string_tree* p = property(x, "space");
+  if (!p || !p->atom) raise << "/space property should have exactly one (non-negative integer) value\n" << end();
+  int n = to_integer(p->value);
+  assert(n >= 0);
+  recipe_ordinal surrounding_recipe = lookup_surrounding_recipe(default_recipe, n);
+  if (surrounding_recipe == -1) return -1;
+  set<recipe_ordinal> done;
+  vector<recipe_ordinal> path;
+  return lookup_name(x, surrounding_recipe, done, path);
+}
+
+// If the recipe we need to lookup this name in doesn't have names done yet,
+// recursively call transform_names on it.
+int lookup_name(const reagent& x, const recipe_ordinal r, set<recipe_ordinal>& done, vector<recipe_ordinal>& path) {
+  if (!Name[r].empty()) return Name[r][x.name];
+  if (contains_key(done, r)) {
+    raise << "can't compute address of '" << to_string(x) << "' because\n" << end();
+    for (int i = 1;  i < SIZE(path);  ++i) {
+      raise << path.at(i-1) << " requires computing names of " << path.at(i) << '\n' << end();
+    }
+    raise << path.at(SIZE(path)-1) << " requires computing names of " << r << "..ad infinitum\n" << end();
+    return -1;
+  }
+  done.insert(r);
+  path.push_back(r);
+  transform_names(r);  // Not passing 'done' through. Might this somehow cause an infinite loop?
+  assert(!Name[r].empty());
+  return Name[r][x.name];
+}
+
+recipe_ordinal lookup_surrounding_recipe(const recipe_ordinal r, int n) {
+  if (n == 0) return r;
+  if (!contains_key(Surrounding_space, r)) {
+    raise << "don't know surrounding recipe of '" << get(Recipe, r).name << "'\n" << end();
+    return -1;
+  }
+  assert(contains_key(Surrounding_space, r));
+  return lookup_surrounding_recipe(get(Surrounding_space, r), n-1);
+}
+
+//: weaken use-before-set detection just a tad
+:(replace{} "bool already_transformed(const reagent& r, const map<string, int>& names)")
+bool already_transformed(const reagent& r, const map<string, int>& names) {
+  if (has_property(r, "space")) {
+    string_tree* p = property(r, "space");
+    if (!p || !p->atom) {
+      raise << "/space property should have exactly one (non-negative integer) value in '" << r.original_string << "'\n" << end();
+      return false;
+    }
+    if (p->value != "0") return true;
+  }
+  return contains_key(names, r.name);
+}
+
+:(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
+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"
+  );
+}
+
+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/archive/2.vm/046check_type_by_name.cc b/archive/2.vm/046check_type_by_name.cc
new file mode 100644
index 00000000..9b123d37
--- /dev/null
+++ b/archive/2.vm/046check_type_by_name.cc
@@ -0,0 +1,265 @@
+//: Some simple sanity checks for types, and also attempts to guess them where
+//: they aren't provided.
+//:
+//: You still have to provide the full type the first time you mention a
+//: variable in a recipe. You have to explicitly name :offset and :variant
+//: every single time. You can't use the same name with multiple types in a
+//: single recipe.
+
+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)")
+Transform.push_back(check_or_set_types_by_name);  // idempotent
+
+// Keep the name->type mapping for all recipes around for the entire
+// transformation phase.
+:(before "End Globals")
+map<recipe_ordinal, set<reagent, name_lt> > Types_by_space;  // internal to transform; no need to snapshot
+:(before "End Reset")
+Types_by_space.clear();
+:(before "End transform_all")
+Types_by_space.clear();
+:(before "End Types")
+struct name_lt {
+  bool operator()(const reagent& a, const reagent& b) const { return a.name < b.name; }
+};
+
+:(code)
+void check_or_set_types_by_name(const recipe_ordinal r) {
+  recipe& caller = get(Recipe, r);
+  trace(101, "transform") << "--- deduce types for recipe " << caller.name << end();
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    instruction& inst = caller.steps.at(i);
+    for (int in = 0;  in < SIZE(inst.ingredients);  ++in)
+      check_or_set_type(inst.ingredients.at(in), caller);
+    for (int out = 0;  out < SIZE(inst.products);  ++out)
+      check_or_set_type(inst.products.at(out), caller);
+  }
+}
+
+void check_or_set_type(reagent& curr, const recipe& caller) {
+  if (is_literal(curr)) return;
+  if (is_integer(curr.name)) return;  // no type-checking for raw locations
+  set<reagent, name_lt>& known_types = Types_by_space[owning_recipe(curr, caller.ordinal)];
+  deduce_missing_type(known_types, curr, caller);
+  check_type(known_types, curr, caller);
+}
+
+void deduce_missing_type(set<reagent, name_lt>& known_types, reagent& x, const recipe& caller) {
+  // Deduce Missing Type(x, caller)
+  if (x.type) return;
+  if (is_jump_target(x.name)) {
+    x.type = new type_tree("label");
+    return;
+  }
+  if (known_types.find(x) == known_types.end()) return;
+  const reagent& exemplar = *known_types.find(x);
+  x.type = new type_tree(*exemplar.type);
+  trace(102, "transform") << x.name << " <= " << names_to_string(x.type) << end();
+  // spaces are special; their type includes their /names property
+  if (is_mu_space(x) && !has_property(x, "names")) {
+    if (!has_property(exemplar, "names")) {
+      raise << maybe(caller.name) << "missing /names property for space variable '" << exemplar.name << "'\n" << end();
+      return;
+    }
+    x.properties.push_back(pair<string, string_tree*>("names", new string_tree(*property(exemplar, "names"))));
+  }
+}
+
+void check_type(set<reagent, name_lt>& known_types, const reagent& x, const recipe& caller) {
+  if (is_literal(x)) return;
+  if (!x.type) return;  // might get filled in by other logic later
+  if (is_jump_target(x.name)) {
+    if (!x.type->atom || x.type->name != "label")
+      raise << maybe(caller.name) << "non-label '" << x.name << "' must begin with a letter\n" << end();
+    return;
+  }
+  if (known_types.find(x) == known_types.end()) {
+    trace(102, "transform") << x.name << " => " << names_to_string(x.type) << end();
+    known_types.insert(x);
+  }
+  if (!types_strictly_match(known_types.find(x)->type, x.type)) {
+    raise << maybe(caller.name) << "'" << x.name << "' used with multiple types\n" << end();
+    raise << "  " << to_string(known_types.find(x)->type) << " vs " << to_string(x.type) << '\n' << end();
+    return;
+  }
+  if (is_mu_array(x)) {
+    if (!x.type->right) {
+      raise << maybe(caller.name) << "'" << x.name << ": can't be just an array. What is it an array of?\n" << end();
+      return;
+    }
+    if (!x.type->right->right) {
+      raise << caller.name << " can't determine the size of array variable '" << x.name << "'. Either allocate it separately and make the type of '" << x.name << "' an address, or specify the length of the array in the type of '" << x.name << "'.\n" << end();
+      return;
+    }
+  }
+}
+
+recipe_ordinal owning_recipe(const reagent& x, recipe_ordinal r) {
+  for (int s = space_index(x); s > 0; --s) {
+    if (!contains_key(Surrounding_space, r)) break;  // error raised elsewhere
+    r = Surrounding_space[r];
+  }
+  return r;
+}
+
+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/archive/2.vm/050scenario.cc b/archive/2.vm/050scenario.cc
new file mode 100644
index 00000000..5b9dcc14
--- /dev/null
+++ b/archive/2.vm/050scenario.cc
@@ -0,0 +1,1039 @@
+//: Mu scenarios. This will get long, but these are the tests we want to
+//: support in this layer.
+
+//: We avoid raw numeric locations in Mu -- except in scenarios, where they're
+//: handy to check the values of specific variables
+
+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
+
+:(before "End Types")
+struct scenario {
+  string name;
+  string to_run;
+  void clear() {
+    name.clear();
+    to_run.clear();
+  }
+};
+
+:(before "End Globals")
+vector<scenario> Scenarios, Scenarios_snapshot;
+set<string> Scenario_names, Scenario_names_snapshot;
+:(before "End save_snapshots")
+Scenarios_snapshot = Scenarios;
+Scenario_names_snapshot = Scenario_names;
+:(before "End restore_snapshots")
+Scenarios = Scenarios_snapshot;
+Scenario_names = Scenario_names_snapshot;
+
+//:: Parse the 'scenario' form.
+//: Simply store the text of the scenario.
+
+:(before "End Command Handlers")
+else if (command == "scenario") {
+  scenario result = parse_scenario(in);
+  if (!result.name.empty())
+    Scenarios.push_back(result);
+}
+else if (command == "pending-scenario") {
+  // for temporary use only
+  parse_scenario(in);  // discard
+}
+
+:(code)
+scenario parse_scenario(istream& in) {
+  scenario result;
+  result.name = next_word(in);
+  if (contains_key(Scenario_names, result.name))
+    raise << "duplicate scenario name: '" << result.name << "'\n" << end();
+  Scenario_names.insert(result.name);
+  if (result.name.empty()) {
+    assert(!has_data(in));
+    raise << "incomplete scenario at end of file\n" << end();
+    return result;
+  }
+  skip_whitespace_and_comments(in);
+  if (in.peek() != '[') {
+    raise << "Expected '[' after scenario '" << result.name << "'\n" << end();
+    exit(0);
+  }
+  // scenarios are take special 'code' strings so we need to ignore brackets
+  // inside comments
+  result.to_run = slurp_quoted(in);
+  // delete [] delimiters
+  if (!starts_with(result.to_run, "[")) {
+    raise << "scenario " << result.name << " should start with '['\n" << end();
+    result.clear();
+    return result;
+  }
+  result.to_run.erase(0, 1);
+  if (result.to_run.at(SIZE(result.to_run)-1) != ']') {
+    raise << "scenario " << result.name << " has an unbalanced '['\n" << end();
+    result.clear();
+    return result;
+  }
+  result.to_run.erase(SIZE(result.to_run)-1);
+  return result;
+}
+
+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.
+
+:(before "End Globals")
+int Num_core_mu_scenarios = 0;
+:(after "Check For .mu Files")
+Num_core_mu_scenarios = SIZE(Scenarios);
+:(before "End Tests")
+Hide_missing_default_space_errors = false;
+if (Num_core_mu_scenarios > 0) {
+  time(&t);
+  cerr << "Mu tests: " << ctime(&t);
+  for (int i = 0;  i < Num_core_mu_scenarios;  ++i) {
+//?     cerr << '\n' << i << ": " << Scenarios.at(i).name;
+    run_mu_scenario(Scenarios.at(i));
+    if (Passed) cerr << ".";
+    else ++num_failures;
+  }
+  cerr << "\n";
+}
+run_app_scenarios:
+if (Num_core_mu_scenarios != SIZE(Scenarios)) {
+  time(&t);
+  cerr << "App tests: " << ctime(&t);
+  for (int i = Num_core_mu_scenarios;  i < SIZE(Scenarios);  ++i) {
+//?     cerr << '\n' << i << ": " << Scenarios.at(i).name;
+    run_mu_scenario(Scenarios.at(i));
+    if (Passed) cerr << ".";
+    else ++num_failures;
+  }
+  cerr << "\n";
+}
+
+//: For faster debugging, support running tests for just the Mu app(s) we are
+//: loading.
+:(before "End Globals")
+bool Test_only_app = false;
+:(before "End Commandline Options(*arg)")
+else if (is_equal(*arg, "--test-only-app")) {
+  Test_only_app = true;
+}
+:(after "End Test Run Initialization")
+if (Test_only_app && Num_core_mu_scenarios < SIZE(Scenarios)) {
+  goto run_app_scenarios;
+}
+
+//: Convenience: run a single named scenario.
+:(after "Test Runs")
+for (int i = 0;  i < SIZE(Scenarios);  ++i) {
+  if (Scenarios.at(i).name == argv[argc-1]) {
+    run_mu_scenario(Scenarios.at(i));
+    if (Passed) cerr << ".\n";
+    return 0;
+  }
+}
+
+:(before "End Globals")
+// this isn't a constant, just a global of type const*
+const scenario* Current_scenario = NULL;
+:(code)
+void run_mu_scenario(const scenario& s) {
+  Current_scenario = &s;
+  bool not_already_inside_test = !Trace_stream;
+//?   cerr << s.name << '\n';
+  if (not_already_inside_test) {
+    Trace_stream = new trace_stream;
+    reset();
+  }
+  vector<recipe_ordinal> tmp = load("recipe scenario_"+s.name+" [ "+s.to_run+" ]");
+  mark_autogenerated(tmp.at(0));
+  bind_special_scenario_names(tmp.at(0));
+  transform_all();
+  if (!trace_contains_errors())
+    run(tmp.front());
+  // End Mu Test Teardown
+  if (!Hide_errors && trace_contains_errors() && !Scenario_testing_scenario)
+    Passed = false;
+  if (not_already_inside_test && Trace_stream) {
+    delete Trace_stream;
+    Trace_stream = NULL;
+  }
+  Current_scenario = NULL;
+}
+
+//: Permit numeric locations to be accessed in scenarios.
+:(before "End check_default_space Special-cases")
+// user code should never create recipes with underscores in their names
+if (starts_with(caller.name, "scenario_")) return;  // skip Mu scenarios which will use raw memory locations
+if (starts_with(caller.name, "run_")) return;  // skip calls to 'run', which should be in scenarios and will also use raw memory locations
+
+:(before "End maybe(recipe_name) Special-cases")
+if (starts_with(recipe_name, "scenario_"))
+  return recipe_name.substr(strlen("scenario_")) + ": ";
+
+//: Some variables for fake resources always get special /raw addresses in scenarios.
+
+:(code)
+// Should contain everything passed by is_special_name but failed by is_disqualified.
+void bind_special_scenario_names(const recipe_ordinal r) {
+  // Special Scenario Variable Names(r)
+  // End Special Scenario Variable Names(r)
+}
+:(before "Done Placing Ingredient(ingredient, inst, caller)")
+maybe_make_raw(ingredient, caller);
+:(before "Done Placing Product(product, inst, caller)")
+maybe_make_raw(product, caller);
+:(code)
+void maybe_make_raw(reagent& r, const recipe& caller) {
+  if (!is_special_name(r.name)) return;
+  if (starts_with(caller.name, "scenario_"))
+    r.properties.push_back(pair<string, string_tree*>("raw", NULL));
+  // End maybe_make_raw
+}
+
+//: 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)")
+//: ugly: we only need this for this one test, but need to define it for all time
+Name[r]["__maybe_make_raw_test__"] = Reserved_for_tests-1;
+:(code)
+void test_maybe_make_raw() {
+  // check that scenarios can use local-scope and special variables together
+  vector<recipe_ordinal> tmp = load(
+      "def scenario_foo [\n"
+      "  local-scope\n"
+      "  __maybe_make_raw_test__:num <- copy 34\n"
+      "]\n");
+  mark_autogenerated(tmp.at(0));
+  bind_special_scenario_names(tmp.at(0));
+  transform_all();
+  run(tmp.at(0));
+  CHECK_TRACE_DOESNT_CONTAIN_ERRORS();
+}
+
+//: Watch out for redefinitions of scenario routines. We should never ever be
+//: doing that, regardless of anything else.
+
+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;
+
+//:: The special instructions we want to support inside scenarios.
+//: These are easy to support in an interpreter, but will require more work
+//: when we eventually build a compiler.
+
+//: 'run' is a purely lexical convenience to separate the code actually being
+//: tested from any setup
+
+:(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") {
+  // Just inline all instructions inside the run block in the containing
+  // recipe. 'run' is basically a comment; pretend it doesn't exist.
+  istringstream in2("[\n"+curr.ingredients.at(0).name+"\n]\n");
+  slurp_body(in2, result);
+  curr.clear();
+}
+
+:(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.
+
+:(before "End Globals")
+bool Scenario_testing_scenario = false;
+:(before "End Reset")
+Scenario_testing_scenario = false;
+
+:(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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "memory-should-contain", MEMORY_SHOULD_CONTAIN);
+:(before "End Primitive Recipe Checks")
+case MEMORY_SHOULD_CONTAIN: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MEMORY_SHOULD_CONTAIN: {
+  if (!Passed) break;
+  check_memory(current_instruction().ingredients.at(0).name);
+  break;
+}
+
+:(code)
+void check_memory(const string& s) {
+  istringstream in(s);
+  in >> std::noskipws;
+  set<int> locations_checked;
+  while (true) {
+    skip_whitespace_and_comments(in);
+    if (!has_data(in)) break;
+    string lhs = next_word(in);
+    if (lhs.empty()) {
+      assert(!has_data(in));
+      raise << maybe(current_recipe_name()) << "incomplete 'memory-should-contain' block at end of file (0)\n" << end();
+      return;
+    }
+    if (!is_integer(lhs)) {
+      check_type(lhs, in);
+      continue;
+    }
+    int address = to_integer(lhs);
+    skip_whitespace_and_comments(in);
+    string _assign;  in >> _assign;  assert(_assign == "<-");
+    skip_whitespace_and_comments(in);
+    string rhs = next_word(in);
+    if (rhs.empty()) {
+      assert(!has_data(in));
+      raise << maybe(current_recipe_name()) << "incomplete 'memory-should-contain' block at end of file (1)\n" << end();
+      return;
+    }
+    if (!is_integer(rhs) && !is_noninteger(rhs)) {
+      if (!Hide_errors) cerr << '\n';
+      raise << "F - " << maybe(current_recipe_name()) << "location '" << address << "' can't contain non-number " << rhs << '\n' << end();
+      if (!Scenario_testing_scenario) Passed = false;
+      return;
+    }
+    double value = to_double(rhs);
+    if (contains_key(locations_checked, address))
+      raise << maybe(current_recipe_name()) << "duplicate expectation for location '" << address << "'\n" << end();
+    trace(Callstack_depth+1, "run") << "checking location " << address << end();
+    if (get_or_insert(Memory, address) != value) {
+      if (!Hide_errors) cerr << '\n';
+      raise << "F - " << maybe(current_recipe_name()) << "expected location '" << address << "' to contain " << no_scientific(value) << " but saw " << no_scientific(get_or_insert(Memory, address)) << '\n' << end();
+      if (!Scenario_testing_scenario) Passed = false;
+      return;
+    }
+    locations_checked.insert(address);
+  }
+}
+
+void check_type(const string& lhs, istream& in) {
+  reagent x(lhs);
+  if (is_mu_array(x.type) && is_mu_character(array_element(x.type))) {
+    x.set_value(to_integer(x.name));
+    skip_whitespace_and_comments(in);
+    string _assign = next_word(in);
+    if (_assign.empty()) {
+      assert(!has_data(in));
+      raise << maybe(current_recipe_name()) << "incomplete 'memory-should-contain' block at end of file (2)\n" << end();
+      return;
+    }
+    assert(_assign == "<-");
+    skip_whitespace_and_comments(in);
+    string literal = next_word(in);
+    if (literal.empty()) {
+      assert(!has_data(in));
+      raise << maybe(current_recipe_name()) << "incomplete 'memory-should-contain' block at end of file (3)\n" << end();
+      return;
+    }
+    int address = x.value;
+    // exclude quoting brackets
+    if (*literal.begin() != '[') {
+      raise << maybe(current_recipe_name()) << "array:character types inside 'memory-should-contain' can only be compared with text literals surrounded by [], not '" << literal << "'\n" << end();
+      return;
+    }
+    literal.erase(literal.begin());
+    assert(*--literal.end() == ']');  literal.erase(--literal.end());
+    check_mu_text(address, literal);
+    return;
+  }
+  // End Scenario Type Special-cases
+  raise << "don't know how to check memory for '" << lhs << "'\n" << end();
+}
+
+void check_mu_text(int start, const string& literal) {
+  trace(Callstack_depth+1, "run") << "checking text length at " << start << end();
+  int array_length = static_cast<int>(get_or_insert(Memory, start));
+  if (array_length != SIZE(literal)) {
+    if (!Hide_errors) cerr << '\n';
+    raise << "F - " << maybe(current_recipe_name()) << "expected location '" << start << "' to contain length " << SIZE(literal) << " of text [" << literal << "] but saw " << array_length << " (for text [" << read_mu_characters(start+/*skip length*/1, array_length) << "])\n" << end();
+    if (!Scenario_testing_scenario) Passed = false;
+    return;
+  }
+  int curr = start+1;  // now skip length
+  for (int i = 0;  i < SIZE(literal);  ++i) {
+    trace(Callstack_depth+1, "run") << "checking location " << curr+i << end();
+    if (get_or_insert(Memory, curr+i) != literal.at(i)) {
+      if (!Hide_errors) cerr << '\n';
+      raise << "F - " << maybe(current_recipe_name()) << "expected location " << (curr+i) << " to contain " << literal.at(i) << " but saw " << no_scientific(get_or_insert(Memory, curr+i)) << '\n' << end();
+      if (!Scenario_testing_scenario) Passed = false;
+      return;
+    }
+  }
+}
+
+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.)
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "trace-should-contain", TRACE_SHOULD_CONTAIN);
+:(before "End Primitive Recipe Checks")
+case TRACE_SHOULD_CONTAIN: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case TRACE_SHOULD_CONTAIN: {
+  if (!Passed) break;
+  check_trace(current_instruction().ingredients.at(0).name);
+  break;
+}
+
+:(code)
+// simplified version of check_trace_contents() that emits errors rather
+// than just printing to stderr
+void check_trace(const string& expected) {
+  Trace_stream->newline();
+  vector<trace_line> expected_lines = parse_trace(expected);
+  if (expected_lines.empty()) return;
+  int curr_expected_line = 0;
+  for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin();  p != Trace_stream->past_lines.end();  ++p) {
+    if (expected_lines.at(curr_expected_line).label != p->label) continue;
+    if (expected_lines.at(curr_expected_line).contents != trim(p->contents)) continue;
+    // match
+    ++curr_expected_line;
+    if (curr_expected_line == SIZE(expected_lines)) return;
+  }
+  if (!Hide_errors) cerr << '\n';
+  raise << "F - " << maybe(current_recipe_name()) << "missing [" << expected_lines.at(curr_expected_line).contents << "] "
+        << "in trace with label '" << expected_lines.at(curr_expected_line).label << "'\n" << end();
+  if (!Hide_errors)
+    DUMP(expected_lines.at(curr_expected_line).label);
+  if (!Scenario_testing_scenario) Passed = false;
+}
+
+vector<trace_line> parse_trace(const string& expected) {
+  vector<string> buf = split(expected, "\n");
+  vector<trace_line> result;
+  const string separator = ": ";
+  for (int i = 0;  i < SIZE(buf);  ++i) {
+    buf.at(i) = trim(buf.at(i));
+    if (buf.at(i).empty()) continue;
+    int delim = buf.at(i).find(separator);
+    if (delim == -1) {
+      raise << maybe(current_recipe_name()) << "lines in 'trace-should-contain' should be of the form <label>: <contents>. Both parts are required.\n" << end();
+      result.clear();
+      return result;
+    }
+    result.push_back(trace_line(/*contents*/  trim(buf.at(i).substr(delim+SIZE(separator)        )),
+                                /*label*/     trim(buf.at(i).substr(0,                      delim))));
+  }
+  return result;
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "trace-should-not-contain", TRACE_SHOULD_NOT_CONTAIN);
+:(before "End Primitive Recipe Checks")
+case TRACE_SHOULD_NOT_CONTAIN: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case TRACE_SHOULD_NOT_CONTAIN: {
+  if (!Passed) break;
+  check_trace_missing(current_instruction().ingredients.at(0).name);
+  break;
+}
+
+:(code)
+// simplified version of check_trace_contents() that emits errors rather
+// than just printing to stderr
+bool check_trace_missing(const string& in) {
+  Trace_stream->newline();
+  vector<trace_line> lines = parse_trace(in);
+  for (int i = 0;  i < SIZE(lines);  ++i) {
+    if (trace_count(lines.at(i).label, lines.at(i).contents) != 0) {
+      raise << "F - " << maybe(current_recipe_name()) << "unexpected [" << lines.at(i).contents << "] in trace with label '" << lines.at(i).label << "'\n" << end();
+      if (!Scenario_testing_scenario) Passed = false;
+      return false;
+    }
+  }
+  return true;
+}
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "check-trace-count-for-label", CHECK_TRACE_COUNT_FOR_LABEL);
+:(before "End Primitive Recipe Checks")
+case CHECK_TRACE_COUNT_FOR_LABEL: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'check-trace-count-for-label' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'check-trace-count-for-label' should be a number (count), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (!is_literal_text(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'check-trace-count-for-label' should be a literal string (label), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CHECK_TRACE_COUNT_FOR_LABEL: {
+  if (!Passed) break;
+  int expected_count = ingredients.at(0).at(0);
+  string label = current_instruction().ingredients.at(1).name;
+  int count = trace_count(label);
+  if (count != expected_count) {
+    if (!Hide_errors) cerr << '\n';
+    raise << "F - " << maybe(current_recipe_name()) << "expected " << expected_count << " lines in trace with label '" << label << "' in trace\n" << end();
+    if (!Hide_errors) DUMP(label);
+    if (!Scenario_testing_scenario) Passed = false;
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CHECK_TRACE_COUNT_FOR_LABEL_GREATER_THAN,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "check-trace-count-for-label-greater-than", CHECK_TRACE_COUNT_FOR_LABEL_GREATER_THAN);
+:(before "End Primitive Recipe Checks")
+case CHECK_TRACE_COUNT_FOR_LABEL_GREATER_THAN: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'check-trace-count-for-label' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'check-trace-count-for-label' should be a number (count), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (!is_literal_text(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'check-trace-count-for-label' should be a literal string (label), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CHECK_TRACE_COUNT_FOR_LABEL_GREATER_THAN: {
+  if (!Passed) break;
+  int expected_count = ingredients.at(0).at(0);
+  string label = current_instruction().ingredients.at(1).name;
+  int count = trace_count(label);
+  if (count <= expected_count) {
+    if (!Hide_errors) cerr << '\n';
+    raise << maybe(current_recipe_name()) << "expected more than " << expected_count << " lines in trace with label '" << label << "' in trace\n" << end();
+    if (!Hide_errors) {
+      cerr << "trace contents:\n";
+      DUMP(label);
+    }
+    if (!Scenario_testing_scenario) Passed = false;
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "check-trace-count-for-label-lesser-than", CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN);
+:(before "End Primitive Recipe Checks")
+case CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'check-trace-count-for-label' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'check-trace-count-for-label' should be a number (count), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (!is_literal_text(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'check-trace-count-for-label' should be a literal string (label), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN: {
+  if (!Passed) break;
+  int expected_count = ingredients.at(0).at(0);
+  string label = current_instruction().ingredients.at(1).name;
+  int count = trace_count(label);
+  if (count >= expected_count) {
+    if (!Hide_errors) cerr << '\n';
+    raise << "F - " << maybe(current_recipe_name()) << "expected less than " << expected_count << " lines in trace with label '" << label << "' in trace\n" << end();
+    if (!Hide_errors) {
+      cerr << "trace contents:\n";
+      DUMP(label);
+    }
+    if (!Scenario_testing_scenario) Passed = false;
+  }
+  break;
+}
+
+:(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.
+:(after "case _SYSTEM:")
+  if (Current_scenario) break;
+
+//:: Warn if people use '_' manually in recipe names. They're reserved for internal use.
+
+:(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;
+:(before "End recipe Constructor")
+is_autogenerated = false;
+:(code)
+void mark_autogenerated(recipe_ordinal r) {
+  get(Recipe, r).is_autogenerated = true;
+}
+
+:(after "void transform_all()")
+  for (map<recipe_ordinal, recipe>::iterator p = Recipe.begin();  p != Recipe.end();  ++p) {
+    const recipe& r = p->second;
+    if (r.name.find('_') == string::npos) continue;
+    if (r.is_autogenerated) continue;  // created by previous call to transform_all()
+    raise << r.name << ": don't create recipes with '_' in the name\n" << end();
+  }
+
+//:: Helpers
+
+:(code)
+// just for the scenarios running scenarios in C++ layers
+void run_mu_scenario(const string& form) {
+  Scenario_names.clear();
+  istringstream in(form);
+  in >> std::noskipws;
+  skip_whitespace_and_comments(in);
+  string _scenario = next_word(in);
+  if (_scenario.empty()) {
+    assert(!has_data(in));
+    raise << "no scenario in string passed into run_mu_scenario()\n" << end();
+    return;
+  }
+  assert(_scenario == "scenario");
+  scenario s = parse_scenario(in);
+  run_mu_scenario(s);
+}
diff --git a/archive/2.vm/051scenario_test.mu b/archive/2.vm/051scenario_test.mu
new file mode 100644
index 00000000..0d6f04a6
--- /dev/null
+++ b/archive/2.vm/051scenario_test.mu
@@ -0,0 +1,70 @@
+# tests for 'scenario' in previous layer
+
+scenario first_scenario_in_mu [
+  run [
+    10:num <- add 2, 2
+  ]
+  memory-should-contain [
+    10 <- 4
+  ]
+]
+
+scenario scenario_with_comment_in_mu [
+  run [
+    # comment
+    10:num <- add 2, 2
+  ]
+  memory-should-contain [
+    10 <- 4
+  ]
+]
+
+scenario scenario_with_multiple_comments_in_mu [
+  run [
+    # comment1
+    # comment2
+    10:num <- add 2, 2
+  ]
+  memory-should-contain [
+    10 <- 4
+  ]
+]
+
+scenario check_text_in_memory [
+  run [
+    10:num <- copy 3
+    11:char <- copy 97  # 'a'
+    12:char <- copy 98  # 'b'
+    13:char <- copy 99  # 'c'
+  ]
+  memory-should-contain [
+    10:array:character <- [abc]
+  ]
+]
+
+scenario check_trace [
+  run [
+    10:num <- add 2, 2
+  ]
+  trace-should-contain [
+    mem: storing 4 in location 10
+  ]
+]
+
+scenario check_trace_negative [
+  run [
+    10:num <- add 2, 2
+  ]
+  trace-should-not-contain [
+    mem: storing 3 in location 10
+  ]
+]
+
+scenario check_trace_instruction [
+  run [
+    trace 1, [foo], [aaa]
+  ]
+  trace-should-contain [
+    foo: aaa
+  ]
+]
diff --git a/archive/2.vm/052tangle.cc b/archive/2.vm/052tangle.cc
new file mode 100644
index 00000000..a5332e1b
--- /dev/null
+++ b/archive/2.vm/052tangle.cc
@@ -0,0 +1,529 @@
+//: Allow code for recipes to be pulled in from multiple places and inserted
+//: at special labels called 'waypoints' using two new top-level commands:
+//:   before
+//:   after
+
+//: Most labels are local: they must be unique to a recipe, and are invisible
+//: outside the recipe. However, waypoints are global: a recipe can have
+//: multiple of them, you can't use them as jump targets.
+:(before "End is_jump_target Special-cases")
+if (is_waypoint(label)) return false;
+//: Waypoints are always surrounded by '<>', e.g. <handle-request>.
+:(code)
+bool is_waypoint(string label) {
+  return *label.begin() == '<' && *label.rbegin() == '>';
+}
+
+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
+
+:(before "End Globals")
+map<string /*label*/, recipe> Before_fragments, After_fragments;
+set<string /*label*/> Fragments_used;
+:(before "End Reset")
+Before_fragments.clear();
+After_fragments.clear();
+Fragments_used.clear();
+
+:(before "End Command Handlers")
+else if (command == "before") {
+  string label = next_word(in);
+  if (label.empty()) {
+    assert(!has_data(in));
+    raise << "incomplete 'before' block at end of file\n" << end();
+    return result;
+  }
+  recipe tmp;
+  slurp_body(in, tmp);
+  if (is_waypoint(label))
+    Before_fragments[label].steps.insert(Before_fragments[label].steps.end(), tmp.steps.begin(), tmp.steps.end());
+  else
+    raise << "can't tangle before non-waypoint " << label << '\n' << end();
+  // End before Command Handler
+}
+else if (command == "after") {
+  string label = next_word(in);
+  if (label.empty()) {
+    assert(!has_data(in));
+    raise << "incomplete 'after' block at end of file\n" << end();
+    return result;
+  }
+  recipe tmp;
+  slurp_body(in, tmp);
+  if (is_waypoint(label))
+    After_fragments[label].steps.insert(After_fragments[label].steps.begin(), tmp.steps.begin(), tmp.steps.end());
+  else
+    raise << "can't tangle after non-waypoint " << label << '\n' << end();
+  // End after Command Handler
+}
+
+//: after all recipes are loaded, insert fragments at appropriate labels.
+
+:(after "Begin Instruction Inserting/Deleting Transforms")
+Transform.push_back(insert_fragments);  // NOT idempotent
+
+//: We might need to perform multiple passes, in case inserted fragments
+//: include more labels that need further insertions. Track which labels we've
+//: already processed using an extra field.
+:(before "End instruction Fields")
+mutable bool tangle_done;
+:(before "End instruction Constructor")
+tangle_done = false;
+
+:(code)
+void insert_fragments(const recipe_ordinal r) {
+  insert_fragments(get(Recipe, r));
+}
+
+void insert_fragments(recipe& r) {
+  trace(101, "transform") << "--- insert fragments into recipe " << r.name << end();
+  bool made_progress = true;
+  int pass = 0;
+  while (made_progress) {
+    made_progress = false;
+    // create a new vector because insertions invalidate iterators
+    vector<instruction> result;
+    for (int i = 0;  i < SIZE(r.steps);  ++i) {
+      const instruction& inst = r.steps.at(i);
+      if (!inst.is_label || !is_waypoint(inst.label) || inst.tangle_done) {
+        result.push_back(inst);
+        continue;
+      }
+      inst.tangle_done = true;
+      made_progress = true;
+      Fragments_used.insert(inst.label);
+      ostringstream prefix;
+      prefix << '+' << r.name << '_' << pass << '_' << i;
+      // ok to use contains_key even though Before_fragments uses [],
+      // because appending an empty recipe is a noop
+      if (contains_key(Before_fragments, inst.label)) {
+        trace(102, "transform") << "insert fragments before label " << inst.label << end();
+        append_fragment(result, Before_fragments[inst.label].steps, prefix.str());
+      }
+      result.push_back(inst);
+      if (contains_key(After_fragments, inst.label)) {
+        trace(102, "transform") << "insert fragments after label " << inst.label << end();
+        append_fragment(result, After_fragments[inst.label].steps, prefix.str());
+      }
+    }
+    r.steps.swap(result);
+    ++pass;
+  }
+}
+
+void append_fragment(vector<instruction>& base, const vector<instruction>& patch, const string prefix) {
+  // append 'patch' to 'base' while keeping 'base' oblivious to any new jump
+  // targets in 'patch' oblivious to 'base' by prepending 'prefix' to them.
+  // we might tangle the same fragment at multiple points in a single recipe,
+  // and we need to avoid duplicate jump targets.
+  // so we'll keep jump targets local to the specific before/after fragment
+  // that introduces them.
+  set<string> jump_targets;
+  for (int i = 0;  i < SIZE(patch);  ++i) {
+    const instruction& inst = patch.at(i);
+    if (inst.is_label && is_jump_target(inst.label))
+      jump_targets.insert(inst.label);
+  }
+  for (int i = 0;  i < SIZE(patch);  ++i) {
+    instruction inst = patch.at(i);
+    if (inst.is_label) {
+      if (contains_key(jump_targets, inst.label))
+        inst.label = prefix+inst.label;
+      base.push_back(inst);
+      continue;
+    }
+    for (int j = 0;  j < SIZE(inst.ingredients);  ++j) {
+      reagent& x = inst.ingredients.at(j);
+      if (is_jump_target(x.name) && contains_key(jump_targets, x.name))
+        x.name = prefix+x.name;
+    }
+    base.push_back(inst);
+  }
+}
+
+//: complain about unapplied fragments
+//: This can't run during transform because later (shape-shifting recipes)
+//: we'll encounter situations where fragments might get used long after
+//: they're loaded, and we might run transform_all in between. To avoid
+//: spurious errors, run this check right at the end, after all code is
+//: loaded, right before we run main.
+:(before "End Commandline Parsing")
+check_insert_fragments();
+:(code)
+void check_insert_fragments() {
+  for (map<string, recipe>::iterator p = Before_fragments.begin();  p != Before_fragments.end();  ++p) {
+    if (!contains_key(Fragments_used, p->first))
+      raise << "could not locate insert before label " << p->first << '\n' << end();
+  }
+  for (map<string, recipe>::iterator p = After_fragments.begin();  p != After_fragments.end();  ++p) {
+    if (!contains_key(Fragments_used, p->first))
+      raise << "could not locate insert after label " << p->first << '\n' << end();
+  }
+}
+
+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);
+}
+
+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"
+  );
+}
+
+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);
+}
+
+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);
+}
+
+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);
+}
+
+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);
+}
+
+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);
+}
+
+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);
+}
+
+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);
+}
+
+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);
+}
+
+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
+
+void test_new_fragment_after_tangle() {
+  // define a recipe
+  load("def foo [\n"
+       "  local-scope\n"
+       "  <label>\n"
+       "]\n"
+       "after <label> [\n"
+       "  1:num/raw <- copy 34\n"
+       "]\n");
+  transform_all();
+  CHECK_TRACE_DOESNT_CONTAIN_ERRORS();
+  Hide_errors = true;
+  // try to tangle into recipe foo after transform
+  load("before <label> [\n"
+       "  2:num/raw <- copy 35\n"
+       "]\n");
+  CHECK_TRACE_CONTAINS_ERRORS();
+}
+
+:(before "End before Command Handler")
+if (contains_key(Fragments_used, label))
+  raise << "we've already tangled some code at label " << label << " in a previous call to transform_all(). Those locations won't be updated.\n" << end();
+:(before "End after Command Handler")
+if (contains_key(Fragments_used, label))
+  raise << "we've already tangled some code at label " << label << " in a previous call to transform_all(). Those locations won't be updated.\n" << end();
diff --git a/archive/2.vm/053recipe_header.cc b/archive/2.vm/053recipe_header.cc
new file mode 100644
index 00000000..73fde510
--- /dev/null
+++ b/archive/2.vm/053recipe_header.cc
@@ -0,0 +1,793 @@
+//: Advanced notation for the common/easy case where a recipe takes some fixed
+//: number of ingredients and yields some fixed number of products.
+
+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.
+
+:(before "End recipe Fields")
+bool has_header;
+vector<reagent> ingredients;
+vector<reagent> products;
+:(before "End recipe Constructor")
+has_header = false;
+
+:(before "End Recipe Refinements")
+if (in.peek() != '[') {
+  trace(101, "parse") << "recipe has a header; parsing" << end();
+  load_recipe_header(in, result);
+}
+
+:(code)
+void load_recipe_header(istream& in, recipe& result) {
+  result.has_header = true;
+  while (has_data(in) && in.peek() != '[' && in.peek() != '\n') {
+    string s = next_word(in);
+    if (s.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete recipe header at end of file (0)\n" << end();
+      return;
+    }
+    if (s == "<-")
+      raise << "recipe " << result.name << " should say '->' and not '<-'\n" << end();
+    if (s == "->") break;
+    result.ingredients.push_back(reagent(s));
+    trace(101, "parse") << "header ingredient: " << result.ingredients.back().original_string << end();
+    skip_whitespace_but_not_newline(in);
+  }
+  while (has_data(in) && in.peek() != '[' && in.peek() != '\n') {
+    string s = next_word(in);
+    if (s.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete recipe header at end of file (1)\n" << end();
+      return;
+    }
+    result.products.push_back(reagent(s));
+    trace(101, "parse") << "header product: " << result.products.back().original_string << end();
+    skip_whitespace_but_not_newline(in);
+  }
+  // End Load Recipe Header(result)
+}
+
+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";
+for (int i = 0;  i < SIZE(x.ingredients);  ++i)
+  out << "  " << debug_string(x.ingredients.at(i)) << '\n';
+out << "products:\n";
+for (int i = 0;  i < SIZE(x.products);  ++i)
+  out << "  " << debug_string(x.products.at(i)) << '\n';
+
+//: If a recipe never mentions any ingredients or products, assume it 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) {
+  result.has_header = true;
+  for (int i = 0;  i < SIZE(result.steps);  ++i) {
+    const instruction& inst = result.steps.at(i);
+    if ((inst.name == "reply" && !inst.ingredients.empty())
+        || (inst.name == "return" && !inst.ingredients.empty())
+        || inst.name == "next-ingredient"
+        || inst.name == "ingredient"
+        || inst.name == "rewind-ingredients") {
+      result.has_header = false;
+      break;
+    }
+  }
+}
+if (result.has_header) {
+  trace(101, "parse") << "recipe " << result.name << " has a header" << end();
+}
+
+//: Support type abbreviations in headers.
+
+:(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)
+  expand_type_abbreviations(caller.ingredients.at(i).type);
+for (long int i = 0;  i < SIZE(caller.products);  ++i)
+  expand_type_abbreviations(caller.products.at(i).type);
+
+//: Rewrite 'load-ingredients' to instructions to create all reagents in the header.
+
+:(before "End Rewrite Instruction(curr, recipe result)")
+if (curr.name == "load-ingredients" || curr.name == "load-inputs") {
+  curr.clear();
+  recipe_ordinal op = get(Recipe_ordinal, "next-ingredient-without-typechecking");
+  for (int i = 0;  i < SIZE(result.ingredients);  ++i) {
+    curr.operation = op;
+    curr.name = "next-ingredient-without-typechecking";
+    curr.products.push_back(result.ingredients.at(i));
+    result.steps.push_back(curr);
+    curr.clear();
+  }
+}
+if (curr.name == "next-ingredient-without-typechecking") {
+  raise << maybe(result.name) << "never call 'next-ingredient-without-typechecking' directly\n" << end();
+  curr.clear();
+}
+
+//: internal version of next-ingredient; don't call this directly
+:(before "End Primitive Recipe Declarations")
+NEXT_INGREDIENT_WITHOUT_TYPECHECKING,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "next-ingredient-without-typechecking", NEXT_INGREDIENT_WITHOUT_TYPECHECKING);
+:(before "End Primitive Recipe Checks")
+case NEXT_INGREDIENT_WITHOUT_TYPECHECKING: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case NEXT_INGREDIENT_WITHOUT_TYPECHECKING: {
+  assert(!Current_routine->calls.empty());
+  if (current_call().next_ingredient_to_process < SIZE(current_call().ingredient_atoms)) {
+    products.push_back(
+        current_call().ingredient_atoms.at(current_call().next_ingredient_to_process));
+    assert(SIZE(products) == 1);  products.resize(2);  // push a new vector
+    products.at(1).push_back(1);
+    ++current_call().next_ingredient_to_process;
+  }
+  else {
+    products.resize(2);
+    // pad the first product with sufficient zeros to match its type
+    products.at(0).resize(size_of(current_instruction().products.at(0)));
+    products.at(1).push_back(0);
+  }
+  break;
+}
+
+//: more useful error messages if someone forgets '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();
+
+:(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))
+  raise << "  did you forget 'load-ingredients'?\n" << end();
+
+:(code)
+bool is_present_in_ingredients(const recipe& callee, const string& ingredient_name) {
+  for (int i = 0;  i < SIZE(callee.ingredients);  ++i) {
+    if (callee.ingredients.at(i).name == ingredient_name)
+      return true;
+  }
+  return false;
+}
+
+//:: Check all calls against headers.
+
+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
+:(code)
+void check_calls_against_header(const recipe_ordinal r) {
+  const recipe& caller = get(Recipe, r);
+  trace(101, "transform") << "--- type-check calls inside recipe " << caller.name << end();
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    const instruction& inst = caller.steps.at(i);
+    if (is_primitive(inst.operation)) continue;
+    const recipe& callee = get(Recipe, inst.operation);
+    if (!callee.has_header) continue;
+    for (long int i = 0;  i < min(SIZE(inst.ingredients), SIZE(callee.ingredients));  ++i) {
+      // ingredients coerced from call to callee
+      if (!types_coercible(callee.ingredients.at(i), inst.ingredients.at(i))) {
+        raise << maybe(caller.name) << "ingredient " << i << " has the wrong type at '" << to_original_string(inst) << "'\n" << end();
+        raise << "  ['" << to_string(callee.ingredients.at(i).type) << "' vs '" << to_string(inst.ingredients.at(i).type) << "']\n" << end();
+      }
+    }
+    for (long int i = 0;  i < min(SIZE(inst.products), SIZE(callee.products));  ++i) {
+      if (is_dummy(inst.products.at(i))) continue;
+      // products coerced from callee to call
+      if (!types_coercible(inst.products.at(i), callee.products.at(i))) {
+        raise << maybe(caller.name) << "product " << i << " has the wrong type at '" << to_original_string(inst) << "'\n" << end();
+        raise << "  ['" << to_string(inst.products.at(i).type) << "' vs '" << to_string(callee.products.at(i).type) << "']\n" << end();
+      }
+    }
+  }
+}
+
+//:: Check types going in and out of all recipes with headers.
+
+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
+
+:(code)
+void check_return_instructions_against_header(const recipe_ordinal r) {
+  const recipe& caller_recipe = get(Recipe, r);
+  if (!caller_recipe.has_header) return;
+  trace(101, "transform") << "--- checking return instructions against header for " << caller_recipe.name << end();
+  for (int i = 0;  i < SIZE(caller_recipe.steps);  ++i) {
+    const instruction& inst = caller_recipe.steps.at(i);
+    if (inst.name != "reply" && inst.name != "return") continue;
+    if (SIZE(caller_recipe.products) != SIZE(inst.ingredients)) {
+      raise << maybe(caller_recipe.name) << "replied with the wrong number of products at '" << to_original_string(inst) << "'\n" << end();
+      continue;
+    }
+    for (int i = 0;  i < SIZE(caller_recipe.products);  ++i) {
+      if (!types_match(caller_recipe.products.at(i), inst.ingredients.at(i)))
+        raise << maybe(caller_recipe.name) << "replied with the wrong type at '" << to_original_string(inst) << "'\n" << end();
+    }
+  }
+}
+
+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;
+
+:(after "Begin Instruction Modifying Transforms")
+Transform.push_back(check_header_ingredients);  // idempotent
+
+:(code)
+void check_header_ingredients(const recipe_ordinal r) {
+  recipe& caller_recipe = get(Recipe, r);
+  caller_recipe.ingredient_index.clear();
+  trace(101, "transform") << "--- checking return instructions against header for " << caller_recipe.name << end();
+  for (int i = 0;  i < SIZE(caller_recipe.ingredients);  ++i) {
+    if (caller_recipe.ingredients.at(i).type == NULL)
+      raise << maybe(caller_recipe.name) << "ingredient '" << caller_recipe.ingredients.at(i).name << "' has no type\n" << end();
+    if (contains_key(caller_recipe.ingredient_index, caller_recipe.ingredients.at(i).name))
+      raise << maybe(caller_recipe.name) << "'" << caller_recipe.ingredients.at(i).name << "' can't repeat in the ingredients\n" << end();
+    put(caller_recipe.ingredient_index, caller_recipe.ingredients.at(i).name, i);
+  }
+}
+
+//: Deduce types from the header if possible.
+
+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
+
+:(code)
+void deduce_types_from_header(const recipe_ordinal r) {
+  recipe& caller_recipe = get(Recipe, r);
+  if (caller_recipe.products.empty()) return;
+  trace(101, "transform") << "--- deduce types from header for " << caller_recipe.name << end();
+  map<string, const type_tree*> header_type;
+  for (int i = 0;  i < SIZE(caller_recipe.ingredients);  ++i) {
+    if (!caller_recipe.ingredients.at(i).type) continue;  // error handled elsewhere
+    put(header_type, caller_recipe.ingredients.at(i).name, caller_recipe.ingredients.at(i).type);
+    trace(103, "transform") << "type of " << caller_recipe.ingredients.at(i).name << " is " << names_to_string(caller_recipe.ingredients.at(i).type) << end();
+  }
+  for (int i = 0;  i < SIZE(caller_recipe.products);  ++i) {
+    if (!caller_recipe.products.at(i).type) continue;  // error handled elsewhere
+    put(header_type, caller_recipe.products.at(i).name, caller_recipe.products.at(i).type);
+    trace(103, "transform") << "type of " << caller_recipe.products.at(i).name << " is " << names_to_string(caller_recipe.products.at(i).type) << end();
+  }
+  for (int i = 0;  i < SIZE(caller_recipe.steps);  ++i) {
+    instruction& inst = caller_recipe.steps.at(i);
+    trace(102, "transform") << "instruction: " << to_string(inst) << end();
+    for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+      if (inst.ingredients.at(i).type) continue;
+      if (header_type.find(inst.ingredients.at(i).name) == header_type.end())
+        continue;
+      if (!contains_key(header_type, inst.ingredients.at(i).name)) continue;  // error handled elsewhere
+      inst.ingredients.at(i).type = new type_tree(*get(header_type, inst.ingredients.at(i).name));
+      trace(103, "transform") << "type of " << inst.ingredients.at(i).name << " is " << names_to_string(inst.ingredients.at(i).type) << end();
+    }
+    for (int i = 0;  i < SIZE(inst.products);  ++i) {
+      trace(103, "transform") << "  product: " << to_string(inst.products.at(i)) << end();
+      if (inst.products.at(i).type) continue;
+      if (header_type.find(inst.products.at(i).name) == header_type.end())
+        continue;
+      if (!contains_key(header_type, inst.products.at(i).name)) continue;  // error handled elsewhere
+      inst.products.at(i).type = new type_tree(*get(header_type, inst.products.at(i).name));
+      trace(103, "transform") << "type of " << inst.products.at(i).name << " is " << names_to_string(inst.products.at(i).type) << end();
+    }
+  }
+}
+
+//: One final convenience: no need to say what to return if the information is
+//: in the header.
+
+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
+
+:(code)
+void fill_in_return_ingredients(const recipe_ordinal r) {
+  recipe& caller_recipe = get(Recipe, r);
+  trace(101, "transform") << "--- fill in return ingredients from header for recipe " << caller_recipe.name << end();
+  if (!caller_recipe.has_header) return;
+  for (int i = 0;  i < SIZE(caller_recipe.steps);  ++i) {
+    instruction& inst = caller_recipe.steps.at(i);
+    if (inst.name == "reply" || inst.name == "return")
+      add_header_products(inst, caller_recipe);
+  }
+  // fall through return
+  if (!caller_recipe.steps.empty()) {
+    const instruction& final_instruction = caller_recipe.steps.at(SIZE(caller_recipe.steps)-1);
+    if (final_instruction.name == "reply" || final_instruction.name == "return")
+      return;
+  }
+  instruction inst;
+  inst.name = "return";
+  add_header_products(inst, caller_recipe);
+  caller_recipe.steps.push_back(inst);
+}
+
+void add_header_products(instruction& inst, const recipe& caller_recipe) {
+  assert(inst.name == "reply" || inst.name == "return");
+  // collect any products with the same names as ingredients
+  for (int i = 0;  i < SIZE(caller_recipe.products);  ++i) {
+    // if the ingredient is missing, add it from the header
+    if (SIZE(inst.ingredients) == i)
+      inst.ingredients.push_back(caller_recipe.products.at(i));
+    // if it's missing /same_as_ingredient, try to fill it in
+    if (contains_key(caller_recipe.ingredient_index, caller_recipe.products.at(i).name) && !has_property(inst.ingredients.at(i), "same_as_ingredient")) {
+      ostringstream same_as_ingredient;
+      same_as_ingredient << get(caller_recipe.ingredient_index, caller_recipe.products.at(i).name);
+      inst.ingredients.at(i).properties.push_back(pair<string, string_tree*>("same-as-ingredient", new string_tree(same_as_ingredient.str())));
+    }
+  }
+}
+
+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.
+
+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);
+:(code)
+void check_recipe_header_constraints(const recipe_ordinal r) {
+  const recipe& caller = get(Recipe, r);
+  if (caller.name != "main") return;
+  trace(102, "transform") << "check recipe header constraints for recipe " << caller.name << end();
+  if (!caller.has_header) return;
+  reagent/*local*/ expected_ingredient("x:address:array:character");
+  for (int i = 0; i < SIZE(caller.ingredients); ++i) {
+    if (!types_strictly_match(expected_ingredient, caller.ingredients.at(i))) {
+      raise << "ingredients of recipe 'main' must all be text (address:array:character)\n" << end();
+      break;
+    }
+  }
+  int nprod = SIZE(caller.products);
+  reagent/*local*/ expected_product("x:number");
+  if (nprod > 1
+      || (nprod == 1 && !types_strictly_match(expected_product, caller.products.at(0)))) {
+    raise << "recipe 'main' must return at most a single product, a number\n" << end();
+  }
+}
diff --git a/archive/2.vm/054static_dispatch.cc b/archive/2.vm/054static_dispatch.cc
new file mode 100644
index 00000000..289dce87
--- /dev/null
+++ b/archive/2.vm/054static_dispatch.cc
@@ -0,0 +1,683 @@
+//: Transform to maintain multiple variants of a recipe depending on the
+//: number and types of the ingredients and products. Allows us to use nice
+//: names like 'print' or 'length' in many mutually extensible ways.
+
+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.
+
+:(before "End Globals")
+map<string, vector<recipe_ordinal> > Recipe_variants;
+:(before "End One-time Setup")
+put(Recipe_variants, "main", vector<recipe_ordinal>());  // since we manually added main to Recipe_ordinal
+
+:(before "End Globals")
+map<string, vector<recipe_ordinal> > Recipe_variants_snapshot;
+:(before "End save_snapshots")
+Recipe_variants_snapshot = Recipe_variants;
+:(before "End restore_snapshots")
+Recipe_variants = Recipe_variants_snapshot;
+
+:(before "End Load Recipe Header(result)")
+// there can only ever be one variant for main
+if (result.name != "main" && contains_key(Recipe_ordinal, result.name)) {
+  const recipe_ordinal r = get(Recipe_ordinal, result.name);
+  if (!contains_key(Recipe, r) || get(Recipe, r).has_header) {
+    string new_name = matching_variant_name(result);
+    if (new_name.empty()) {
+      // variant doesn't already exist
+      new_name = next_unused_recipe_name(result.name);
+      put(Recipe_ordinal, new_name, Next_recipe_ordinal++);
+      get_or_insert(Recipe_variants, result.name).push_back(get(Recipe_ordinal, new_name));
+    }
+    trace(101, "load") << "switching " << result.name << " to " << new_name << end();
+    result.name = new_name;
+    result.is_autogenerated = true;
+  }
+}
+else {
+  // save first variant
+  put(Recipe_ordinal, result.name, Next_recipe_ordinal++);
+  get_or_insert(Recipe_variants, result.name).push_back(get(Recipe_ordinal, result.name));
+}
+
+:(code)
+string matching_variant_name(const recipe& rr) {
+  const vector<recipe_ordinal>& variants = get_or_insert(Recipe_variants, rr.name);
+  for (int i = 0;  i < SIZE(variants);  ++i) {
+    if (!contains_key(Recipe, variants.at(i))) continue;
+    const recipe& candidate = get(Recipe, variants.at(i));
+    if (!all_reagents_match(rr, candidate)) continue;
+    return candidate.name;
+  }
+  return "";
+}
+
+bool all_reagents_match(const recipe& r1, const recipe& r2) {
+  if (SIZE(r1.ingredients) != SIZE(r2.ingredients)) return false;
+  if (SIZE(r1.products) != SIZE(r2.products)) return false;
+  for (int i = 0;  i < SIZE(r1.ingredients);  ++i) {
+    expand_type_abbreviations(r1.ingredients.at(i).type);
+    expand_type_abbreviations(r2.ingredients.at(i).type);
+    if (!deeply_equal_type_names(r1.ingredients.at(i), r2.ingredients.at(i)))
+      return false;
+  }
+  for (int i = 0;  i < SIZE(r1.products);  ++i) {
+    expand_type_abbreviations(r1.products.at(i).type);
+    expand_type_abbreviations(r2.products.at(i).type);
+    if (!deeply_equal_type_names(r1.products.at(i), r2.products.at(i)))
+      return false;
+  }
+  return true;
+}
+
+:(before "End Globals")
+set<string> Literal_type_names;
+:(before "End One-time Setup")
+Literal_type_names.insert("number");
+Literal_type_names.insert("character");
+:(code)
+bool deeply_equal_type_names(const reagent& a, const reagent& b) {
+  return deeply_equal_type_names(a.type, b.type);
+}
+bool deeply_equal_type_names(const type_tree* a, const type_tree* b) {
+  if (!a) return !b;
+  if (!b) return !a;
+  if (a->atom != b->atom) return false;
+  if (a->atom) {
+    if (a->name == "literal" && b->name == "literal")
+      return true;
+    if (a->name == "literal")
+      return Literal_type_names.find(b->name) != Literal_type_names.end();
+    if (b->name == "literal")
+      return Literal_type_names.find(a->name) != Literal_type_names.end();
+    return a->name == b->name;
+  }
+  return deeply_equal_type_names(a->left, b->left)
+      && deeply_equal_type_names(a->right, b->right);
+}
+
+string next_unused_recipe_name(const string& recipe_name) {
+  for (int i = 2;  /*forever*/;  ++i) {
+    ostringstream out;
+    out << recipe_name << '_' << i;
+    if (!contains_key(Recipe_ordinal, out.str()))
+      return out.str();
+  }
+}
+
+//: Once all the recipes are loaded, transform their bodies to replace each
+//: call with the most suitable variant.
+
+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")
+for (int i = 0;  i < SIZE(caller.ingredients);  ++i)
+  check_or_set_invalid_types(caller.ingredients.at(i).type, maybe(caller.name), "recipe header ingredient");
+for (int i = 0;  i < SIZE(caller.products);  ++i)
+  check_or_set_invalid_types(caller.products.at(i).type, maybe(caller.name), "recipe header product");
+
+//: save original name of recipes before renaming them
+:(before "End recipe Fields")
+string original_name;
+//: original name is only set during load
+:(before "End Load Recipe Name")
+result.original_name = result.name;
+
+//: after filling in all missing types (because we'll be introducing 'blank' types in this transform in a later layer, for shape-shifting recipes)
+:(after "Transform.push_back(transform_names)")
+Transform.push_back(resolve_ambiguous_calls);  // idempotent
+
+//: In a later layer we'll introduce recursion in resolve_ambiguous_calls, by
+//: having it generate code for shape-shifting recipes and then transform such
+//: code. This data structure will help error messages be more useful.
+//:
+//: We're punning the 'call' data structure just because it has slots for
+//: calling recipe and calling instruction.
+:(before "End Globals")
+list<call> Resolve_stack;
+
+:(code)
+void resolve_ambiguous_calls(const recipe_ordinal r) {
+  recipe& caller_recipe = get(Recipe, r);
+  trace(101, "transform") << "--- resolve ambiguous calls for recipe " << caller_recipe.name << end();
+  for (int index = 0;  index < SIZE(caller_recipe.steps);  ++index) {
+    instruction& inst = caller_recipe.steps.at(index);
+    if (inst.is_label) continue;
+    resolve_ambiguous_call(r, index, inst, caller_recipe);
+  }
+}
+
+void resolve_ambiguous_call(const recipe_ordinal r, int index, instruction& inst, const recipe& caller_recipe) {
+  // End resolve_ambiguous_call(r, index, inst, caller_recipe) Special-cases
+  if (non_ghost_size(get_or_insert(Recipe_variants, inst.name)) == 0) return;
+  trace(102, "transform") << "instruction " << to_original_string(inst) << end();
+  Resolve_stack.push_front(call(r, index));
+  string new_name = best_variant(inst, caller_recipe);
+  if (!new_name.empty())
+    inst.name = new_name;
+  assert(Resolve_stack.front().running_recipe == r);
+  assert(Resolve_stack.front().running_step_index == index);
+  Resolve_stack.pop_front();
+}
+
+string best_variant(const instruction& inst, const recipe& caller_recipe) {
+  const vector<recipe_ordinal>& variants = get(Recipe_variants, inst.name);
+  vector<recipe_ordinal> candidates;
+
+  // Static Dispatch Phase 1
+//?   cerr << inst.name << " phase 1\n";
+  candidates = strictly_matching_variants(inst, variants);
+  if (!candidates.empty()) return best_variant(inst, candidates).name;
+
+//?   cerr << inst.name << " phase 3\n";
+  // Static Dispatch Phase 2
+  //: (shape-shifting recipes in a later layer)
+  // End Static Dispatch Phase 2
+
+  // Static Dispatch Phase 3
+//?   cerr << inst.name << " phase 4\n";
+  candidates = matching_variants(inst, variants);
+  if (!candidates.empty()) return best_variant(inst, candidates).name;
+
+  // error messages
+  if (!is_primitive(get(Recipe_ordinal, inst.name))) {  // we currently don't check types for primitive variants
+    if (SIZE(variants) == 1) {
+      raise << maybe(caller_recipe.name) << "types don't match in call for '" << to_original_string(inst) << "'\n" << end();
+      raise << "  which tries to call '" << original_header_label(get(Recipe, variants.at(0))) << "'\n" << end();
+    }
+    else {
+      raise << maybe(caller_recipe.name) << "failed to find a matching call for '" << to_original_string(inst) << "'\n" << end();
+      raise << "  available variants are:\n" << end();
+      for (int i = 0;  i < SIZE(variants);  ++i)
+        raise << "    " << original_header_label(get(Recipe, variants.at(i))) << '\n' << end();
+    }
+    for (list<call>::iterator p = /*skip*/++Resolve_stack.begin();  p != Resolve_stack.end();  ++p) {
+      const recipe& specializer_recipe = get(Recipe, p->running_recipe);
+      const instruction& specializer_inst = specializer_recipe.steps.at(p->running_step_index);
+      if (specializer_recipe.name != "interactive")
+        raise << "  (from '" << to_original_string(specializer_inst) << "' in " << specializer_recipe.name << ")\n" << end();
+      else
+        raise << "  (from '" << to_original_string(specializer_inst) << "')\n" << end();
+      // One special-case to help with the rewrite_stash transform. (cross-layer)
+      if (specializer_inst.products.at(0).name.find("stash_") == 0) {
+        instruction stash_inst;
+        if (next_stash(*p, &stash_inst)) {
+          if (specializer_recipe.name != "interactive")
+            raise << "  (part of '" << to_original_string(stash_inst) << "' in " << specializer_recipe.name << ")\n" << end();
+          else
+            raise << "  (part of '" << to_original_string(stash_inst) << "')\n" << end();
+        }
+      }
+    }
+  }
+  return "";
+}
+
+// phase 1
+vector<recipe_ordinal> strictly_matching_variants(const instruction& inst, const vector<recipe_ordinal>& variants) {
+  vector<recipe_ordinal> result;
+  for (int i = 0;  i < SIZE(variants);  ++i) {
+    if (variants.at(i) == -1) continue;
+    trace(102, "transform") << "checking variant (strict) " << i << ": " << header_label(variants.at(i)) << end();
+    if (all_header_reagents_strictly_match(inst, get(Recipe, variants.at(i))))
+      result.push_back(variants.at(i));
+  }
+  return result;
+}
+
+bool all_header_reagents_strictly_match(const instruction& inst, const recipe& variant) {
+  for (int i = 0;  i < min(SIZE(inst.ingredients), SIZE(variant.ingredients));  ++i) {
+    if (!types_strictly_match(variant.ingredients.at(i), inst.ingredients.at(i))) {
+      trace(103, "transform") << "strict match failed: ingredient " << i << end();
+      return false;
+    }
+  }
+  for (int i = 0;  i < min(SIZE(inst.products), SIZE(variant.products));  ++i) {
+    if (is_dummy(inst.products.at(i))) continue;
+    if (!types_strictly_match(variant.products.at(i), inst.products.at(i))) {
+      trace(103, "transform") << "strict match failed: product " << i << end();
+      return false;
+    }
+  }
+  return true;
+}
+
+// phase 3
+vector<recipe_ordinal> matching_variants(const instruction& inst, const vector<recipe_ordinal>& variants) {
+  vector<recipe_ordinal> result;
+  for (int i = 0;  i < SIZE(variants);  ++i) {
+    if (variants.at(i) == -1) continue;
+    trace(102, "transform") << "checking variant " << i << ": " << header_label(variants.at(i)) << end();
+    if (all_header_reagents_match(inst, get(Recipe, variants.at(i))))
+      result.push_back(variants.at(i));
+  }
+  return result;
+}
+
+bool all_header_reagents_match(const instruction& inst, const recipe& variant) {
+  for (int i = 0;  i < min(SIZE(inst.ingredients), SIZE(variant.ingredients));  ++i) {
+    if (!types_match(variant.ingredients.at(i), inst.ingredients.at(i))) {
+      trace(103, "transform") << "match failed: ingredient " << i << end();
+      return false;
+    }
+  }
+  for (int i = 0;  i < min(SIZE(variant.products), SIZE(inst.products));  ++i) {
+    if (is_dummy(inst.products.at(i))) continue;
+    if (!types_match(variant.products.at(i), inst.products.at(i))) {
+      trace(103, "transform") << "match failed: product " << i << end();
+      return false;
+    }
+  }
+  return true;
+}
+
+// tie-breaker for each phase
+const recipe& best_variant(const instruction& inst, vector<recipe_ordinal>& candidates) {
+  assert(!candidates.empty());
+  if (SIZE(candidates) == 1) return get(Recipe, candidates.at(0));
+  int min_score = 999;
+  int min_index = 0;
+  for (int i = 0;  i < SIZE(candidates);  ++i) {
+    const recipe& candidate = get(Recipe, candidates.at(i));
+    // prefer variants without extra or missing ingredients or products
+    int score = abs(SIZE(candidate.products)-SIZE(inst.products))
+                          + abs(SIZE(candidate.ingredients)-SIZE(inst.ingredients));
+    // prefer variants with non-address ingredients or products
+    for (int j = 0;  j < SIZE(candidate.ingredients);  ++j) {
+      if (is_mu_address(candidate.ingredients.at(j)))
+        ++score;
+    }
+    for (int j = 0;  j < SIZE(candidate.products);  ++j) {
+      if (is_mu_address(candidate.products.at(j)))
+        ++score;
+    }
+    assert(score < 999);
+    if (score < min_score) {
+      min_score = score;
+      min_index = i;
+    }
+  }
+  return get(Recipe, candidates.at(min_index));
+}
+
+int non_ghost_size(vector<recipe_ordinal>& variants) {
+  int result = 0;
+  for (int i = 0;  i < SIZE(variants);  ++i)
+    if (variants.at(i) != -1) ++result;
+  return result;
+}
+
+bool next_stash(const call& c, instruction* stash_inst) {
+  const recipe& specializer_recipe = get(Recipe, c.running_recipe);
+  int index = c.running_step_index;
+  for (++index;  index < SIZE(specializer_recipe.steps);  ++index) {
+    const instruction& inst = specializer_recipe.steps.at(index);
+    if (inst.name == "stash") {
+      *stash_inst = inst;
+      return true;
+    }
+  }
+  return false;
+}
+
+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) {
+  return header_label(get(Recipe, r));
+}
+:(code)
+string header_label(const recipe& caller) {
+  ostringstream out;
+  out << "recipe " << caller.name;
+  for (int i = 0;  i < SIZE(caller.ingredients);  ++i)
+    out << ' ' << to_string(caller.ingredients.at(i));
+  if (!caller.products.empty()) out << " ->";
+  for (int i = 0;  i < SIZE(caller.products);  ++i)
+    out << ' ' << to_string(caller.products.at(i));
+  return out.str();
+}
+
+string original_header_label(const recipe& caller) {
+  ostringstream out;
+  out << "recipe " << caller.original_name;
+  for (int i = 0;  i < SIZE(caller.ingredients);  ++i)
+    out << ' ' << caller.ingredients.at(i).original_string;
+  if (!caller.products.empty()) out << " ->";
+  for (int i = 0;  i < SIZE(caller.products);  ++i)
+    out << ' ' << caller.products.at(i).original_string;
+  return out.str();
+}
+
+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/archive/2.vm/055shape_shifting_container.cc b/archive/2.vm/055shape_shifting_container.cc
new file mode 100644
index 00000000..909e6fc5
--- /dev/null
+++ b/archive/2.vm/055shape_shifting_container.cc
@@ -0,0 +1,773 @@
+//:: Container definitions can contain 'type ingredients'
+
+//: pre-requisite: extend our notion of containers to not necessarily be
+//: atomic types
+:(after "Update GET base_type in Check")
+base_type = get_base_type(base_type);
+:(after "Update GET base_type in Run")
+base_type = get_base_type(base_type);
+:(after "Update PUT base_type in Check")
+base_type = get_base_type(base_type);
+:(after "Update PUT base_type in Run")
+base_type = get_base_type(base_type);
+:(after "Update MAYBE_CONVERT base_type in Check")
+base_type = get_base_type(base_type);
+:(after "Update base_type in element_type")
+base_type = get_base_type(base_type);
+:(after "Update base_type in skip_addresses")
+base_type = get_base_type(base_type);
+:(replace{} "const type_tree* get_base_type(const type_tree* t)")
+const type_tree* get_base_type(const type_tree* t) {
+  const type_tree* result = t->atom ? t : t->left;
+  if (!result->atom)
+    raise << "invalid type " << to_string(t) << '\n' << end();
+  return result;
+}
+
+:(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
+
+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".
+// For example, if we have a generic type called foo:_elem, the type
+// ingredient _elem in foo's type_info will have value START_TYPE_INGREDIENTS,
+// and we'll handle it by looking in the current reagent for the next type
+// that appears after foo.
+extern const int START_TYPE_INGREDIENTS = 2000;
+:(before "End Commandline Parsing")  // after loading .mu files
+assert(Next_type_ordinal < START_TYPE_INGREDIENTS);
+
+:(before "End type_info Fields")
+map<string, type_ordinal> type_ingredient_names;
+
+//: Suppress unknown type checks in shape-shifting containers.
+
+:(before "Check Container Field Types(info)")
+if (!info.type_ingredient_names.empty()) continue;
+
+:(before "End container Name Refinements")
+if (name.find(':') != string::npos) {
+  trace(101, "parse") << "container has type ingredients; parsing" << end();
+  if (!read_type_ingredients(name, command)) {
+    // error; skip rest of the container definition and continue
+    slurp_balanced_bracket(in);
+    return;
+  }
+}
+
+:(code)
+bool read_type_ingredients(string& name, const string& command) {
+  string save_name = name;
+  istringstream in(save_name);
+  name = slurp_until(in, ':');
+  map<string, type_ordinal> type_ingredient_names;
+  if (!slurp_type_ingredients(in, type_ingredient_names, name)) {
+    return false;
+  }
+  if (contains_key(Type_ordinal, name)
+      && contains_key(Type, get(Type_ordinal, name))) {
+    const type_info& previous_info = get(Type, get(Type_ordinal, name));
+    // we've already seen this container; make sure type ingredients match
+    if (!type_ingredients_match(type_ingredient_names, previous_info.type_ingredient_names)) {
+      raise << "headers of " << command << " '" << name << "' must use identical type ingredients\n" << end();
+      return false;
+    }
+    return true;
+  }
+  // we haven't seen this container before
+  if (!contains_key(Type_ordinal, name) || get(Type_ordinal, name) == 0)
+    put(Type_ordinal, name, Next_type_ordinal++);
+  type_info& info = get_or_insert(Type, get(Type_ordinal, name));
+  info.type_ingredient_names.swap(type_ingredient_names);
+  return true;
+}
+
+bool slurp_type_ingredients(istream& in, map<string, type_ordinal>& out, const string& container_name) {
+  int next_type_ordinal = START_TYPE_INGREDIENTS;
+  while (has_data(in)) {
+    string curr = slurp_until(in, ':');
+    if (curr.empty()) {
+      raise << container_name << ": empty type ingredients not permitted\n" << end();
+      return false;
+    }
+    if (!starts_with(curr, "_")) {
+      raise << container_name << ": type ingredient '" << curr << "' must begin with an underscore\n" << end();
+      return false;
+    }
+    if (out.find(curr) != out.end()) {
+      raise << container_name << ": can't repeat type ingredient name'" << curr << "' in a single container definition\n" << end();
+      return false;
+    }
+    put(out, curr, next_type_ordinal++);
+  }
+  return true;
+}
+
+bool type_ingredients_match(const map<string, type_ordinal>& a, const map<string, type_ordinal>& b) {
+  if (SIZE(a) != SIZE(b)) return false;
+  for (map<string, type_ordinal>::const_iterator p = a.begin();  p != a.end();  ++p) {
+    if (!contains_key(b, p->first)) return false;
+    if (p->second != get(b, p->first)) return false;
+  }
+  return true;
+}
+
+:(before "End insert_container Special-cases")
+// check for use of type ingredients
+else if (is_type_ingredient_name(type->name)) {
+  type->value = get(info.type_ingredient_names, type->name);
+}
+:(code)
+bool is_type_ingredient_name(const string& type) {
+  return starts_with(type, "_");
+}
+
+:(before "End Container Type Checks")
+if (type->value >= START_TYPE_INGREDIENTS
+    && (type->value - START_TYPE_INGREDIENTS) < SIZE(get(Type, type->value).type_ingredient_names))
+  return;
+
+:(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");
+
+:(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");
+
+:(before "End size_of(type) Non-atom Special-cases")
+assert(type->left->atom);
+if (!contains_key(Type, type->left->value)) {
+  raise << "no such type " << type->left->value << '\n' << end();
+  return 0;
+}
+type_info t = get(Type, type->left->value);
+if (t.kind == CONTAINER) {
+  // size of a container is the sum of the sizes of its elements
+  int result = 0;
+  for (int i = 0; i < SIZE(t.elements); ++i) {
+    // todo: strengthen assertion to disallow mutual type recursion
+    if (get_base_type(t.elements.at(i).type)->value == get_base_type(type)->value) {
+      raise << "container " << t.name << " can't include itself as a member\n" << end();
+      return 0;
+    }
+    result += size_of(element_type(type, i));
+  }
+  return result;
+}
+if (t.kind == EXCLUSIVE_CONTAINER) {
+  // size of an exclusive container is the size of its largest variant
+  // (So like containers, it can't contain arrays.)
+  int result = 0;
+  for (int i = 0; i < SIZE(t.elements); ++i) {
+    reagent tmp;
+    tmp.type = new type_tree(*type);
+    int size = size_of(variant_type(tmp, i));
+    if (size > result) result = size;
+  }
+  // ...+1 for its tag.
+  return result+1;
+}
+
+:(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);
+}
+
+bool contains_type_ingredient(const type_tree* type) {
+  if (!type) return false;
+  if (type->atom) return type->value >= START_TYPE_INGREDIENTS;
+  return contains_type_ingredient(type->left) || contains_type_ingredient(type->right);
+}
+
+void replace_type_ingredients(reagent& element, const type_tree* caller_type, const type_info& info, const string& location_for_error_messages) {
+  if (contains_type_ingredient(element)) {
+    if (!caller_type->right)
+      raise << "illegal type " << names_to_string(caller_type) << " seems to be missing a type ingredient or three" << location_for_error_messages << '\n' << end();
+    replace_type_ingredients(element.type, caller_type->right, info, location_for_error_messages);
+  }
+}
+
+// replace all type_ingredients in element_type with corresponding elements of callsite_type
+void replace_type_ingredients(type_tree* element_type, const type_tree* callsite_type, const type_info& container_info, const string& location_for_error_messages) {
+  if (!callsite_type) return;  // error but it's already been raised above
+  if (!element_type) return;
+  if (!element_type->atom) {
+    if (element_type->right == NULL && is_type_ingredient(element_type->left)) {
+      int type_ingredient_index = to_type_ingredient_index(element_type->left);
+      if (corresponding(callsite_type, type_ingredient_index, is_final_type_ingredient(type_ingredient_index, container_info))->right) {
+        // replacing type ingredient at end of list, and replacement is a non-degenerate compound type -- (a b) but not (a)
+        replace_type_ingredient_at(type_ingredient_index, element_type, callsite_type, container_info, location_for_error_messages);
+        return;
+      }
+    }
+    replace_type_ingredients(element_type->left, callsite_type, container_info, location_for_error_messages);
+    replace_type_ingredients(element_type->right, callsite_type, container_info, location_for_error_messages);
+    return;
+  }
+  if (is_type_ingredient(element_type))
+    replace_type_ingredient_at(to_type_ingredient_index(element_type), element_type, callsite_type, container_info, location_for_error_messages);
+}
+
+const type_tree* corresponding(const type_tree* type, int index, bool final) {
+  for (const type_tree* curr = type;  curr;  curr = curr->right, --index) {
+    assert_for_now(!curr->atom);
+    if (index == 0)
+      return final ? curr : curr->left;
+  }
+  assert_for_now(false);
+}
+
+bool is_type_ingredient(const type_tree* type) {
+  return type->atom && type->value >= START_TYPE_INGREDIENTS;
+}
+
+int to_type_ingredient_index(const type_tree* type) {
+  assert(type->atom);
+  return type->value-START_TYPE_INGREDIENTS;
+}
+
+void replace_type_ingredient_at(const int type_ingredient_index, type_tree* element_type, const type_tree* callsite_type, const type_info& container_info, const string& location_for_error_messages) {
+  if (!has_nth_type(callsite_type, type_ingredient_index)) {
+    raise << "illegal type " << names_to_string(callsite_type) << " seems to be missing a type ingredient or three" << location_for_error_messages << '\n' << end();
+    return;
+  }
+  *element_type = *nth_type_ingredient(callsite_type, type_ingredient_index, container_info);
+}
+
+const type_tree* nth_type_ingredient(const type_tree* callsite_type, int type_ingredient_index, const type_info& container_info) {
+  bool final = is_final_type_ingredient(type_ingredient_index, container_info);
+  const type_tree* curr = callsite_type;
+  for (int i = 0;  i < type_ingredient_index;  ++i) {
+    assert(curr);
+    assert(!curr->atom);
+//?     cerr << "type ingredient " << i << " is " << to_string(curr->left) << '\n';
+    curr = curr->right;
+  }
+  assert(curr);
+  if (curr->atom) return curr;
+  if (!final) return curr->left;
+  if (!curr->right) return curr->left;
+  return curr;
+}
+
+bool is_final_type_ingredient(int type_ingredient_index, const type_info& container_info) {
+  for (map<string, type_ordinal>::const_iterator p = container_info.type_ingredient_names.begin();
+       p != container_info.type_ingredient_names.end();
+       ++p) {
+    if (p->second > START_TYPE_INGREDIENTS+type_ingredient_index) return false;
+  }
+  return true;
+}
+
+:(before "End Unit Tests")
+void test_replace_type_ingredients_entire() {
+  run("container foo:_elem [\n"
+      "  x:_elem\n"
+      "  y:num\n"
+      "]\n");
+  reagent callsite("x:foo:point");
+  reagent element = element_type(callsite.type, 0);
+  CHECK_EQ(to_string(element), "{x: \"point\"}");
+}
+
+void test_replace_type_ingredients_tail() {
+  run("container foo:_elem [\n"
+      "  x:_elem\n"
+      "]\n"
+      "container bar:_elem [\n"
+      "  x:foo:_elem\n"
+      "]\n");
+  reagent callsite("x:bar:point");
+  reagent element = element_type(callsite.type, 0);
+  CHECK_EQ(to_string(element), "{x: (\"foo\" \"point\")}");
+}
+
+void test_replace_type_ingredients_head_tail_multiple() {
+  run("container foo:_elem [\n"
+      "  x:_elem\n"
+      "]\n"
+      "container bar:_elem [\n"
+      "  x:foo:_elem\n"
+      "]\n");
+  reagent callsite("x:bar:address:array:character");
+  reagent element = element_type(callsite.type, 0);
+  CHECK_EQ(to_string(element), "{x: (\"foo\" \"address\" \"array\" \"character\")}");
+}
+
+void test_replace_type_ingredients_head_middle() {
+  run("container foo:_elem [\n"
+      "  x:_elem\n"
+      "]\n"
+      "container bar:_elem [\n"
+      "  x:foo:_elem:num\n"
+      "]\n");
+  reagent callsite("x:bar:address");
+  reagent element = element_type(callsite.type, 0);
+  CHECK_EQ(to_string(element), "{x: (\"foo\" \"address\" \"number\")}");
+}
+
+void test_replace_last_type_ingredient_with_multiple() {
+  run("container foo:_a:_b [\n"
+      "  x:_a\n"
+      "  y:_b\n"
+      "]\n");
+  reagent callsite("{f: (foo number (address array character))}");
+  reagent element1 = element_type(callsite.type, 0);
+  CHECK_EQ(to_string(element1), "{x: \"number\"}");
+  reagent element2 = element_type(callsite.type, 1);
+  CHECK_EQ(to_string(element2), "{y: (\"address\" \"array\" \"character\")}");
+}
+
+void test_replace_last_type_ingredient_inside_compound() {
+  run("container foo:_a:_b [\n"
+      "  {x: (bar _a (address _b))}\n"
+      "]\n");
+  reagent callsite("f:foo:number:array:character");
+  reagent element = element_type(callsite.type, 0);
+  CHECK_EQ(names_to_string_without_quotes(element.type), "(bar number (address array character))");
+}
+
+void test_replace_middle_type_ingredient_with_multiple() {
+  run("container foo:_a:_b:_c [\n"
+      "  x:_a\n"
+      "  y:_b\n"
+      "  z:_c\n"
+      "]\n");
+  reagent callsite("{f: (foo number (address array character) boolean)}");
+  reagent element1 = element_type(callsite.type, 0);
+  CHECK_EQ(to_string(element1), "{x: \"number\"}");
+  reagent element2 = element_type(callsite.type, 1);
+  CHECK_EQ(to_string(element2), "{y: (\"address\" \"array\" \"character\")}");
+  reagent element3 = element_type(callsite.type, 2);
+  CHECK_EQ(to_string(element3), "{z: \"boolean\"}");
+}
+
+void test_replace_middle_type_ingredient_with_multiple2() {
+  run("container foo:_key:_value [\n"
+      "  key:_key\n"
+      "  value:_value\n"
+      "]\n");
+  reagent callsite("{f: (foo (address array character) number)}");
+  reagent element = element_type(callsite.type, 0);
+  CHECK_EQ(to_string(element), "{key: (\"address\" \"array\" \"character\")}");
+}
+
+void test_replace_middle_type_ingredient_with_multiple3() {
+  run("container foo_table:_key:_value [\n"
+      "  data:&:@:foo_table_row:_key:_value\n"
+      "]\n"
+      "\n"
+      "container foo_table_row:_key:_value [\n"
+      "  key:_key\n"
+      "  value:_value\n"
+      "]\n");
+  reagent callsite("{f: (foo_table (address array character) number)}");
+  reagent element = element_type(callsite.type, 0);
+  CHECK_EQ(to_string(element), "{data: (\"address\" \"array\" \"foo_table_row\" (\"address\" \"array\" \"character\") \"number\")}");
+}
+
+:(code)
+bool has_nth_type(const type_tree* base, int n) {
+  assert(n >= 0);
+  if (!base) return false;
+  if (n == 0) return true;
+  return has_nth_type(base->right, n-1);
+}
+
+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
+
+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/archive/2.vm/056shape_shifting_recipe.cc b/archive/2.vm/056shape_shifting_recipe.cc
new file mode 100644
index 00000000..3e3d9a8d
--- /dev/null
+++ b/archive/2.vm/056shape_shifting_recipe.cc
@@ -0,0 +1,1307 @@
+//:: Like container definitions, recipes too can contain type parameters.
+
+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
+//: be rewriting such instructions to *specializations* with the type
+//: ingredients filled in.
+
+//: One exception (and this makes things very ugly): we need to expand type
+//: abbreviations in shape-shifting recipes because we need them types for
+//: deciding which variant to specialize.
+
+:(before "End Transform Checks")
+r.transformed_until = t;
+if (Transform.at(t) != static_cast<transform_fn>(expand_type_abbreviations) && any_type_ingredient_in_header(/*recipe_ordinal*/p->first)) continue;
+
+:(after "Running One Instruction")
+if (Current_routine->calls.front().running_step_index == 0
+    && any_type_ingredient_in_header(Current_routine->calls.front().running_recipe)) {
+//?   DUMP("");
+  raise << "ran into unspecialized shape-shifting recipe " << current_recipe_name() << '\n' << end();
+//?   exit(0);
+}
+
+//: Make sure we don't match up literals with type ingredients without
+//: specialization.
+:(before "End Matching Types For Literal(to)")
+if (contains_type_ingredient_name(to)) return false;
+
+:(after "Static Dispatch Phase 2")
+candidates = strictly_matching_shape_shifting_variants(inst, variants);
+if (!candidates.empty()) {
+  recipe_ordinal exemplar = best_shape_shifting_variant(inst, candidates);
+  trace(102, "transform") << "found variant to specialize: " << exemplar << ' ' << get(Recipe, exemplar).name << end();
+  string new_recipe_name = insert_new_variant(exemplar, inst, caller_recipe);
+  if (new_recipe_name != "") {
+    trace(102, "transform") << "new specialization: " << new_recipe_name << end();
+    return new_recipe_name;
+  }
+}
+
+//: before running Mu programs, make sure no unspecialized shape-shifting
+//: recipes can be called
+
+:(before "End Instruction Operation Checks")
+if (contains_key(Recipe, inst.operation) && !is_primitive(inst.operation)
+    && any_type_ingredient_in_header(inst.operation)) {
+  raise << maybe(caller.name) << "instruction '" << inst.name << "' has no valid specialization\n" << end();
+  return;
+}
+
+:(code)
+// phase 3 of static dispatch
+vector<recipe_ordinal> strictly_matching_shape_shifting_variants(const instruction& inst, const vector<recipe_ordinal>& variants) {
+  vector<recipe_ordinal> result;
+  for (int i = 0;  i < SIZE(variants);  ++i) {
+    if (variants.at(i) == -1) continue;
+    if (!any_type_ingredient_in_header(variants.at(i))) continue;
+    if (!all_concrete_header_reagents_strictly_match(inst, get(Recipe, variants.at(i)))) continue;
+    result.push_back(variants.at(i));
+  }
+  return result;
+}
+
+bool all_concrete_header_reagents_strictly_match(const instruction& inst, const recipe& variant) {
+  for (int i = 0;  i < min(SIZE(inst.ingredients), SIZE(variant.ingredients));  ++i) {
+    if (!concrete_type_names_strictly_match(variant.ingredients.at(i), inst.ingredients.at(i))) {
+      trace(103, "transform") << "concrete-type match failed: ingredient " << i << end();
+      return false;
+    }
+  }
+  for (int i = 0;  i < min(SIZE(inst.products), SIZE(variant.products));  ++i) {
+    if (is_dummy(inst.products.at(i))) continue;
+    if (!concrete_type_names_strictly_match(variant.products.at(i), inst.products.at(i))) {
+      trace(103, "transform") << "concrete-type match failed: product " << i << end();
+      return false;
+    }
+  }
+  return true;
+}
+
+// manual prototype
+vector<recipe_ordinal> keep_max(const instruction&, const vector<recipe_ordinal>&,
+                                int (*)(const instruction&, recipe_ordinal));
+
+// tie-breaker for phase 3
+recipe_ordinal best_shape_shifting_variant(const instruction& inst, const vector<recipe_ordinal>& candidates) {
+  assert(!candidates.empty());
+  if (SIZE(candidates) == 1) return candidates.at(0);
+//?   cerr << "A picking shape-shifting variant:\n";
+  vector<recipe_ordinal> result1 = keep_max(inst, candidates, number_of_concrete_type_names);
+  assert(!result1.empty());
+  if (SIZE(result1) == 1) return result1.at(0);
+//?   cerr << "B picking shape-shifting variant:\n";
+  vector<recipe_ordinal> result2 = keep_max(inst, result1, arity_fit);
+  assert(!result2.empty());
+  if (SIZE(result2) == 1) return result2.at(0);
+//?   cerr << "C picking shape-shifting variant:\n";
+  vector<recipe_ordinal> result3 = keep_max(inst, result2, number_of_type_ingredients);
+  if (SIZE(result3) > 1) {
+    raise << "\nCouldn't decide the best shape-shifting variant for instruction '" << to_original_string(inst) << "'\n" << end();
+    cerr << "This is a hole in Mu. Please copy the following candidates into an email to Kartik Agaram <mu@akkartik.com>\n";
+    for (int i = 0;  i < SIZE(candidates);  ++i)
+      cerr << "  " << header_label(get(Recipe, candidates.at(i))) << '\n';
+  }
+  return result3.at(0);
+}
+
+vector<recipe_ordinal> keep_max(const instruction& inst, const vector<recipe_ordinal>& in,
+                                int (*scorer)(const instruction&, recipe_ordinal)) {
+  assert(!in.empty());
+  vector<recipe_ordinal> out;
+  out.push_back(in.at(0));
+  int best_score = (*scorer)(inst, in.at(0));
+//?   cerr << best_score << " " << header_label(get(Recipe, in.at(0))) << '\n';
+  for (int i = 1;  i < SIZE(in);  ++i) {
+    int score = (*scorer)(inst, in.at(i));
+//?     cerr << score << " " << header_label(get(Recipe, in.at(i))) << '\n';
+    if (score == best_score) {
+      out.push_back(in.at(i));
+    }
+    else if (score > best_score) {
+      best_score = score;
+      out.clear();
+      out.push_back(in.at(i));
+    }
+  }
+  return out;
+}
+
+int arity_fit(const instruction& inst, recipe_ordinal candidate) {
+  const recipe& r = get(Recipe, candidate);
+  return (SIZE(inst.products) - SIZE(r.products))
+       + (SIZE(r.ingredients) - SIZE(inst.ingredients));
+}
+
+bool any_type_ingredient_in_header(recipe_ordinal variant) {
+  const recipe& caller = get(Recipe, variant);
+  for (int i = 0;  i < SIZE(caller.ingredients);  ++i) {
+    if (contains_type_ingredient_name(caller.ingredients.at(i)))
+      return true;
+  }
+  for (int i = 0;  i < SIZE(caller.products);  ++i) {
+    if (contains_type_ingredient_name(caller.products.at(i)))
+      return true;
+  }
+  return false;
+}
+
+bool concrete_type_names_strictly_match(reagent/*copy*/ to, reagent/*copy*/ from) {
+  canonize_type(to);
+  canonize_type(from);
+  return concrete_type_names_strictly_match(to.type, from.type, from);
+}
+
+bool concrete_type_names_strictly_match(const type_tree* to, const type_tree* from, const reagent& rhs_reagent) {
+  if (!to) return !from;
+  if (!from) return !to;
+  if (to->atom && is_type_ingredient_name(to->name)) return true;  // type ingredient matches anything
+  if (!to->atom && to->right == NULL && to->left != NULL && to->left->atom && is_type_ingredient_name(to->left->name)) return true;
+  if (from->atom && is_mu_address(to))
+    return from->name == "literal-address" && rhs_reagent.name == "null";
+  if (!from->atom && !to->atom)
+    return concrete_type_names_strictly_match(to->left, from->left, rhs_reagent)
+        && concrete_type_names_strictly_match(to->right, from->right, rhs_reagent);
+  if (from->atom != to->atom) return false;
+  // both from and to are atoms
+  if (from->name == "literal")
+    return Literal_type_names.find(to->name) != Literal_type_names.end();
+  if (to->name == "literal")
+    return Literal_type_names.find(from->name) != Literal_type_names.end();
+  return to->name == from->name;
+}
+
+bool contains_type_ingredient_name(const reagent& x) {
+  return contains_type_ingredient_name(x.type);
+}
+
+bool contains_type_ingredient_name(const type_tree* type) {
+  if (!type) return false;
+  if (is_type_ingredient_name(type->name)) return true;
+  return contains_type_ingredient_name(type->left) || contains_type_ingredient_name(type->right);
+}
+
+int number_of_concrete_type_names(const instruction& /*unused*/, recipe_ordinal r) {
+  const recipe& caller = get(Recipe, r);
+  int result = 0;
+  for (int i = 0;  i < SIZE(caller.ingredients);  ++i)
+    result += number_of_concrete_type_names(caller.ingredients.at(i).type);
+  for (int i = 0;  i < SIZE(caller.products);  ++i)
+    result += number_of_concrete_type_names(caller.products.at(i).type);
+  return result;
+}
+
+int number_of_concrete_type_names(const type_tree* type) {
+  if (!type) return 0;
+  if (type->atom)
+    return is_type_ingredient_name(type->name) ? 0 : 1;
+  return number_of_concrete_type_names(type->left)
+       + number_of_concrete_type_names(type->right);
+}
+
+int number_of_type_ingredients(const instruction& /*unused*/, recipe_ordinal r) {
+  const recipe& caller = get(Recipe, r);
+  int result = 0;
+  for (int i = 0;  i < SIZE(caller.ingredients);  ++i)
+    result += number_of_type_ingredients(caller.ingredients.at(i).type);
+  for (int i = 0;  i < SIZE(caller.products);  ++i)
+    result += number_of_type_ingredients(caller.products.at(i).type);
+  return result;
+}
+
+int number_of_type_ingredients(const type_tree* type) {
+  if (!type) return 0;
+  if (type->atom)
+    return is_type_ingredient_name(type->name) ? 1 : 0;
+  return number_of_type_ingredients(type->left)
+       + number_of_type_ingredients(type->right);
+}
+
+// returns name of new variant
+string insert_new_variant(recipe_ordinal exemplar, const instruction& inst, const recipe& caller_recipe) {
+  string new_name = next_unused_recipe_name(inst.name);
+  assert(!contains_key(Recipe_ordinal, new_name));
+  recipe_ordinal new_recipe_ordinal = put(Recipe_ordinal, new_name, Next_recipe_ordinal++);
+  // make a copy
+  assert(contains_key(Recipe, exemplar));
+  assert(!contains_key(Recipe, new_recipe_ordinal));
+  put(Recipe, new_recipe_ordinal, /*copy*/get(Recipe, exemplar));
+  recipe& new_recipe = get(Recipe, new_recipe_ordinal);
+  new_recipe.name = new_name;
+  new_recipe.ordinal = new_recipe_ordinal;
+  new_recipe.is_autogenerated = true;
+  trace(103, "transform") << "switching " << inst.name << " to specialized " << header_label(new_recipe) << end();
+
+  trace(102, "transform") << "transforming new specialization: " << new_recipe.name << end();
+  trace(102, "transform") << new_recipe.name << ": performing transforms until check_or_set_types_by_name" << end();
+  int transform_index = 0;
+  for (transform_index = 0;  transform_index < SIZE(Transform);  ++transform_index) {
+    if (Transform.at(transform_index) == check_or_set_types_by_name) break;
+    (*Transform.at(transform_index))(new_recipe_ordinal);
+  }
+  new_recipe.transformed_until = transform_index-1;
+
+  trace(102, "transform") << new_recipe.name << ": performing type-ingredient-aware version of transform check_or_set_types_by_name" << end();
+  compute_type_names(new_recipe);
+  new_recipe.transformed_until++;
+
+  trace(102, "transform") << new_recipe.name << ": replacing type ingredients" << end();
+  {
+    map<string, const type_tree*> mappings;
+    bool error = false;
+    compute_type_ingredient_mappings(get(Recipe, exemplar), inst, mappings, caller_recipe, &error);
+    if (!error) error = (SIZE(mappings) != type_ingredient_count_in_header(exemplar));
+    if (!error) replace_type_ingredients(new_recipe, mappings);
+    for (map<string, const type_tree*>::iterator p = mappings.begin();  p != mappings.end();  ++p)
+      delete p->second;
+    if (error) return "";
+  }
+  ensure_all_concrete_types(new_recipe, get(Recipe, exemplar));
+
+  trace(102, "transform") << new_recipe.name << ": recording the new variant before recursively calling resolve_ambiguous_calls" << end();
+  get(Recipe_variants, inst.name).push_back(new_recipe_ordinal);
+  trace(102, "transform") << new_recipe.name << ": performing remaining transforms (including resolve_ambiguous_calls)" << end();
+  for (/*nada*/;  transform_index < SIZE(Transform);  ++transform_index)
+    (*Transform.at(transform_index))(new_recipe_ordinal);
+  new_recipe.transformed_until = SIZE(Transform)-1;
+  return new_recipe.name;
+}
+
+void compute_type_names(recipe& variant) {
+  trace(103, "transform") << "-- compute type names: " << variant.name << end();
+  map<string, type_tree*> type_names;
+  for (int i = 0;  i < SIZE(variant.ingredients);  ++i)
+    save_or_deduce_type_name(variant.ingredients.at(i), type_names, variant, "");
+  for (int i = 0;  i < SIZE(variant.products);  ++i)
+    save_or_deduce_type_name(variant.products.at(i), type_names, variant, "");
+  for (int i = 0;  i < SIZE(variant.steps);  ++i) {
+    instruction& inst = variant.steps.at(i);
+    trace(103, "transform") << "  instruction: " << to_string(inst) << end();
+    for (int in = 0;  in < SIZE(inst.ingredients);  ++in)
+      save_or_deduce_type_name(inst.ingredients.at(in), type_names, variant, " in '" + to_original_string(inst) + "'");
+    for (int out = 0;  out < SIZE(inst.products);  ++out)
+      save_or_deduce_type_name(inst.products.at(out), type_names, variant, " in '" + to_original_string(inst) + "'");
+  }
+}
+
+void save_or_deduce_type_name(reagent& x, map<string, type_tree*>& type, const recipe& variant, const string& context) {
+  trace(104, "transform") << "    checking " << to_string(x) << ": " << names_to_string(x.type) << end();
+  if (!x.type && contains_key(type, x.name)) {
+    x.type = new type_tree(*get(type, x.name));
+    trace(104, "transform") << "    deducing type to " << names_to_string(x.type) << end();
+    return;
+  }
+  // Type Check in Type-ingredient-aware check_or_set_types_by_name
+  // This is different from check_or_set_types_by_name.
+  // We've found it useful in the past for tracking down bugs in
+  // specialization.
+  if (!x.type) {
+    raise << maybe(variant.original_name) << "unknown type for '" << x.original_string << "'" << context << " (check the name for typos)\n" << end();
+    return;
+  }
+  if (contains_key(type, x.name)) return;
+  if (x.type->name == "offset" || x.type->name == "variant") return;  // special-case for container-access instructions
+  put(type, x.name, x.type);
+  trace(103, "transform") << "type of '" << x.name << "' is " << names_to_string(x.type) << end();
+}
+
+void compute_type_ingredient_mappings(const recipe& exemplar, const instruction& inst, map<string, const type_tree*>& mappings, const recipe& caller_recipe, bool* error) {
+  int limit = min(SIZE(inst.ingredients), SIZE(exemplar.ingredients));
+  for (int i = 0;  i < limit;  ++i) {
+    const reagent& exemplar_reagent = exemplar.ingredients.at(i);
+    reagent/*copy*/ ingredient = inst.ingredients.at(i);
+    canonize_type(ingredient);
+    if (is_mu_address(exemplar_reagent) && ingredient.name == "null") continue;  // assume it matches
+    accumulate_type_ingredients(exemplar_reagent, ingredient, mappings, exemplar, inst, caller_recipe, error);
+  }
+  limit = min(SIZE(inst.products), SIZE(exemplar.products));
+  for (int i = 0;  i < limit;  ++i) {
+    const reagent& exemplar_reagent = exemplar.products.at(i);
+    reagent/*copy*/ product = inst.products.at(i);
+    if (is_dummy(product)) continue;
+    canonize_type(product);
+    accumulate_type_ingredients(exemplar_reagent, product, mappings, exemplar, inst, caller_recipe, error);
+  }
+}
+
+void accumulate_type_ingredients(const reagent& exemplar_reagent, reagent& refinement, map<string, const type_tree*>& mappings, const recipe& exemplar, const instruction& call_instruction, const recipe& caller_recipe, bool* error) {
+  assert(refinement.type);
+  accumulate_type_ingredients(exemplar_reagent.type, refinement.type, mappings, exemplar, exemplar_reagent, call_instruction, caller_recipe, error);
+}
+
+void accumulate_type_ingredients(const type_tree* exemplar_type, const type_tree* refinement_type, map<string, const type_tree*>& mappings, const recipe& exemplar, const reagent& exemplar_reagent, const instruction& call_instruction, const recipe& caller_recipe, bool* error) {
+  if (!exemplar_type) return;
+  if (!refinement_type) {
+    // probably a bug in mu
+    // todo: make this smarter; only flag an error if exemplar_type contains some *new* type ingredient
+    raise << maybe(exemplar.name) << "missing type ingredient for " << exemplar_reagent.original_string << '\n' << end();
+    raise << "  (called from '" << to_original_string(call_instruction) << "')\n" << end();
+    return;
+  }
+  if (!exemplar_type->atom && exemplar_type->right == NULL && !refinement_type->atom && refinement_type->right != NULL) {
+    exemplar_type = exemplar_type->left;
+    assert_for_now(exemplar_type->atom);
+  }
+  if (exemplar_type->atom) {
+    if (is_type_ingredient_name(exemplar_type->name)) {
+      const type_tree* curr_refinement_type = NULL;  // temporary heap allocation; must always be deleted before it goes out of scope
+      if (exemplar_type->atom)
+        curr_refinement_type = new type_tree(*refinement_type);
+      else {
+        assert(!refinement_type->atom);
+        curr_refinement_type = new type_tree(*refinement_type->left);
+      }
+      if (!contains_key(mappings, exemplar_type->name)) {
+        trace(103, "transform") << "adding mapping from " << exemplar_type->name << " to " << to_string(curr_refinement_type) << end();
+        put(mappings, exemplar_type->name, new type_tree(*curr_refinement_type));
+      }
+      else {
+        if (!deeply_equal_type_names(get(mappings, exemplar_type->name), curr_refinement_type)) {
+          raise << maybe(caller_recipe.name) << "no call found for '" << to_original_string(call_instruction) << "'\n" << end();
+          *error = true;
+          delete curr_refinement_type;
+          return;
+        }
+        if (get(mappings, exemplar_type->name)->name == "literal") {
+          delete get(mappings, exemplar_type->name);
+          put(mappings, exemplar_type->name, new type_tree(*curr_refinement_type));
+        }
+      }
+      delete curr_refinement_type;
+    }
+  }
+  else {
+    accumulate_type_ingredients(exemplar_type->left, refinement_type->left, mappings, exemplar, exemplar_reagent, call_instruction, caller_recipe, error);
+    accumulate_type_ingredients(exemplar_type->right, refinement_type->right, mappings, exemplar, exemplar_reagent, call_instruction, caller_recipe, error);
+  }
+}
+
+void replace_type_ingredients(recipe& new_recipe, const map<string, const type_tree*>& mappings) {
+  // update its header
+  if (mappings.empty()) return;
+  trace(103, "transform") << "replacing in recipe header ingredients" << end();
+  for (int i = 0;  i < SIZE(new_recipe.ingredients);  ++i)
+    replace_type_ingredients(new_recipe.ingredients.at(i), mappings, new_recipe);
+  trace(103, "transform") << "replacing in recipe header products" << end();
+  for (int i = 0;  i < SIZE(new_recipe.products);  ++i)
+    replace_type_ingredients(new_recipe.products.at(i), mappings, new_recipe);
+  // update its body
+  for (int i = 0;  i < SIZE(new_recipe.steps);  ++i) {
+    instruction& inst = new_recipe.steps.at(i);
+    trace(103, "transform") << "replacing in instruction '" << to_string(inst) << "'" << end();
+    for (int j = 0;  j < SIZE(inst.ingredients);  ++j)
+      replace_type_ingredients(inst.ingredients.at(j), mappings, new_recipe);
+    for (int j = 0;  j < SIZE(inst.products);  ++j)
+      replace_type_ingredients(inst.products.at(j), mappings, new_recipe);
+    // special-case for new: replace type ingredient in first ingredient *value*
+    if (inst.name == "new" && inst.ingredients.at(0).type->name != "literal-string") {
+      type_tree* type = parse_type_tree(inst.ingredients.at(0).name);
+      replace_type_ingredients(type, mappings);
+      inst.ingredients.at(0).name = inspect(type);
+      delete type;
+    }
+  }
+}
+
+void replace_type_ingredients(reagent& x, const map<string, const type_tree*>& mappings, const recipe& caller) {
+  string before = to_string(x);
+  trace(103, "transform") << "replacing in ingredient " << x.original_string << end();
+  if (!x.type) {
+    raise << "specializing " << caller.original_name << ": missing type for '" << x.original_string << "'\n" << end();
+    return;
+  }
+  replace_type_ingredients(x.type, mappings);
+}
+
+void replace_type_ingredients(type_tree* type, const map<string, const type_tree*>& mappings) {
+  if (!type) return;
+  if (!type->atom) {
+    if (type->right == NULL && type->left != NULL && type->left->atom && contains_key(mappings, type->left->name) && !get(mappings, type->left->name)->atom && get(mappings, type->left->name)->right != NULL) {
+      *type = *get(mappings, type->left->name);
+      return;
+    }
+    replace_type_ingredients(type->left, mappings);
+    replace_type_ingredients(type->right, mappings);
+    return;
+  }
+  if (contains_key(Type_ordinal, type->name))  // todo: ugly side effect
+    type->value = get(Type_ordinal, type->name);
+  if (!contains_key(mappings, type->name))
+    return;
+  const type_tree* replacement = get(mappings, type->name);
+  trace(103, "transform") << type->name << " => " << names_to_string(replacement) << end();
+  if (replacement->atom) {
+    if (!contains_key(Type_ordinal, replacement->name)) {
+      // error in program; should be reported elsewhere
+      return;
+    }
+    type->name = (replacement->name == "literal") ? "number" : replacement->name;
+    type->value = get(Type_ordinal, type->name);
+  }
+  else {
+    *type = *replacement;
+  }
+}
+
+int type_ingredient_count_in_header(recipe_ordinal variant) {
+  const recipe& caller = get(Recipe, variant);
+  set<string> type_ingredients;
+  for (int i = 0;  i < SIZE(caller.ingredients);  ++i)
+    accumulate_type_ingredients(caller.ingredients.at(i).type, type_ingredients);
+  for (int i = 0;  i < SIZE(caller.products);  ++i)
+    accumulate_type_ingredients(caller.products.at(i).type, type_ingredients);
+  return SIZE(type_ingredients);
+}
+
+void accumulate_type_ingredients(const type_tree* type, set<string>& out) {
+  if (!type) return;
+  if (is_type_ingredient_name(type->name)) out.insert(type->name);
+  accumulate_type_ingredients(type->left, out);
+  accumulate_type_ingredients(type->right, out);
+}
+
+type_tree* parse_type_tree(const string& s) {
+  string_tree* s2 = parse_string_tree(s);
+  type_tree* result = new_type_tree(s2);
+  delete s2;
+  return result;
+}
+
+string inspect(const type_tree* x) {
+  ostringstream out;
+  dump_inspect(x, out);
+  return out.str();
+}
+
+void dump_inspect(const type_tree* x, ostream& out) {
+  if (!x->left && !x->right) {
+    out << x->name;
+    return;
+  }
+  out << '(';
+  for (const type_tree* curr = x;  curr;  curr = curr->right) {
+    if (curr != x) out << ' ';
+    if (curr->left)
+      dump_inspect(curr->left, out);
+    else
+      out << curr->name;
+  }
+  out << ')';
+}
+
+void ensure_all_concrete_types(/*const*/ recipe& new_recipe, const recipe& exemplar) {
+  trace(103, "transform") << "-- ensure all concrete types in recipe " << new_recipe.name << end();
+  for (int i = 0;  i < SIZE(new_recipe.ingredients);  ++i)
+    ensure_all_concrete_types(new_recipe.ingredients.at(i), exemplar);
+  for (int i = 0;  i < SIZE(new_recipe.products);  ++i)
+    ensure_all_concrete_types(new_recipe.products.at(i), exemplar);
+  for (int i = 0;  i < SIZE(new_recipe.steps);  ++i) {
+    instruction& inst = new_recipe.steps.at(i);
+    for (int j = 0;  j < SIZE(inst.ingredients);  ++j)
+      ensure_all_concrete_types(inst.ingredients.at(j), exemplar);
+    for (int j = 0;  j < SIZE(inst.products);  ++j)
+      ensure_all_concrete_types(inst.products.at(j), exemplar);
+  }
+}
+
+void ensure_all_concrete_types(/*const*/ reagent& x, const recipe& exemplar) {
+  if (!x.type || contains_type_ingredient_name(x.type)) {
+    raise << maybe(exemplar.name) << "failed to map a type to " << x.original_string << '\n' << end();
+    if (!x.type) x.type = new type_tree("added_by_ensure_all_concrete_types", 0);  // just to prevent crashes later
+    return;
+  }
+  if (x.type->value == -1) {
+    raise << maybe(exemplar.name) << "failed to map a type to the unknown " << x.original_string << '\n' << end();
+    return;
+  }
+}
+
+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);
+}
+
+// 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..
+  transform("def barz x:_elem [\n"
+            "  local-scope\n"
+            "  load-ingredients\n"
+            "  y:&:num <- new _elem:type\n"
+            "]\n"
+            "def fooz [\n"
+            "  local-scope\n"
+            "  barz 34\n"
+            "]\n");
+  // ..and if you then try to load a new shape-shifting container with that
+  // type-ingredient
+  run("container foo:_elem [\n"
+      "  x:_elem\n"
+      "  y:num\n"
+      "]\n");
+  // then it should work as usual
+  reagent callsite("x:foo:point");
+  reagent element = element_type(callsite.type, 0);
+  CHECK_EQ(element.name, "x");
+  CHECK_EQ(element.type->name, "point");
+  CHECK(!element.type->right);
+}
+
+//: specializing a type ingredient with a compound type
+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
+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/archive/2.vm/057immutable.cc b/archive/2.vm/057immutable.cc
new file mode 100644
index 00000000..16c2a6ab
--- /dev/null
+++ b/archive/2.vm/057immutable.cc
@@ -0,0 +1,715 @@
+//: Ingredients of a recipe are meant to be immutable unless they're also
+//: products. This layer will start enforcing this check.
+//:
+//: One hole for now: variables in surrounding spaces are implicitly mutable.
+//: [tag: todo]
+
+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
+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
+
+:(code)
+void check_immutable_ingredients(const recipe_ordinal r) {
+  // to ensure an address reagent isn't modified, it suffices to show that
+  //   a) we never write to its contents directly,
+  //   b) we never call 'put' or 'put-index' on it, and
+  //   c) any non-primitive recipe calls in the body aren't returning it as a product
+  const recipe& caller = get(Recipe, r);
+  trace(101, "transform") << "--- check mutability of ingredients in recipe " << caller.name << end();
+  if (!caller.has_header) return;  // skip check for old-style recipes calling next-ingredient directly
+  for (int i = 0;  i < SIZE(caller.ingredients);  ++i) {
+    const reagent& current_ingredient = caller.ingredients.at(i);
+    if (is_present_in_products(caller, current_ingredient.name)) continue;  // not expected to be immutable
+    // End Immutable Ingredients Special-cases
+    set<reagent, name_and_space_lt> immutable_vars;
+    immutable_vars.insert(current_ingredient);
+    for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+      const instruction& inst = caller.steps.at(i);
+      check_immutable_ingredient_in_instruction(inst, immutable_vars, current_ingredient.name, caller);
+      if (inst.operation == INDEX && SIZE(inst.ingredients) > 1 && inst.ingredients.at(1).name == current_ingredient.name) continue;
+      update_aliases(inst, immutable_vars);
+    }
+  }
+}
+
+void update_aliases(const instruction& inst, set<reagent, name_and_space_lt>& current_ingredient_and_aliases) {
+  set<int> current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases);
+  if (!contains_key(Recipe, inst.operation)) {
+    // primitive recipe
+    switch (inst.operation) {
+      case COPY:
+        for (set<int>::iterator p = current_ingredient_indices.begin();  p != current_ingredient_indices.end();  ++p)
+          current_ingredient_and_aliases.insert(inst.products.at(*p).name);
+        break;
+      case GET:
+      case INDEX:
+      case MAYBE_CONVERT:
+        // current_ingredient_indices can only have 0 or one value
+        if (!current_ingredient_indices.empty() && !inst.products.empty()) {
+          if (is_mu_address(inst.products.at(0)) || is_mu_container(inst.products.at(0)) || is_mu_exclusive_container(inst.products.at(0)))
+            current_ingredient_and_aliases.insert(inst.products.at(0));
+        }
+        break;
+      default: break;
+    }
+  }
+  else {
+    // defined recipe
+    set<int> contained_in_product_indices = scan_contained_in_product_indices(inst, current_ingredient_indices);
+    for (set<int>::iterator p = contained_in_product_indices.begin();  p != contained_in_product_indices.end();  ++p) {
+      if (*p < SIZE(inst.products))
+        current_ingredient_and_aliases.insert(inst.products.at(*p));
+    }
+  }
+}
+
+set<int> scan_contained_in_product_indices(const instruction& inst, set<int>& ingredient_indices) {
+  set<reagent, name_and_space_lt> selected_ingredients;
+  const recipe& callee = get(Recipe, inst.operation);
+  for (set<int>::iterator p = ingredient_indices.begin();  p != ingredient_indices.end();  ++p) {
+    if (*p >= SIZE(callee.ingredients)) continue;  // optional immutable ingredient
+    selected_ingredients.insert(callee.ingredients.at(*p));
+  }
+  set<int> result;
+  for (int i = 0;  i < SIZE(callee.products);  ++i) {
+    const reagent& current_product = callee.products.at(i);
+    const string_tree* contained_in_name = property(current_product, "contained-in");
+    if (contained_in_name && selected_ingredients.find(contained_in_name->value) != selected_ingredients.end())
+      result.insert(i);
+  }
+  return result;
+}
+
+bool is_mu_container(const reagent& r) {
+  return is_mu_container(r.type);
+}
+bool is_mu_container(const type_tree* type) {
+  if (!type) return false;
+  if (!type->atom)
+    return is_mu_container(get_base_type(type));
+  if (type->value == 0) return false;
+  if (!contains_key(Type, type->value)) return false;  // error raised elsewhere
+  type_info& info = get(Type, type->value);
+  return info.kind == CONTAINER;
+}
+
+bool is_mu_exclusive_container(const reagent& r) {
+  return is_mu_exclusive_container(r.type);
+}
+bool is_mu_exclusive_container(const type_tree* type) {
+  if (!type) return false;
+  if (!type->atom)
+    return is_mu_exclusive_container(get_base_type(type));
+  if (type->value == 0) return false;
+  if (!contains_key(Type, type->value)) return false;  // error raised elsewhere
+  type_info& info = get(Type, type->value);
+  return info.kind == EXCLUSIVE_CONTAINER;
+}
+
+:(before "End Types")
+// reagent comparison -- only in the context of a single recipe
+struct name_and_space_lt {
+  bool operator()(const reagent& a, const reagent& b) const;
+};
+:(code)
+bool name_and_space_lt::operator()(const reagent& a, const reagent& b) const {
+  int aspace = 0, bspace = 0;
+  if (has_property(a, "space")) aspace = to_integer(property(a, "space")->value);
+  if (has_property(b, "space")) bspace = to_integer(property(b, "space")->value);
+  if (aspace != bspace) return aspace < bspace;
+  return a.name < b.name;
+}
+
+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"
+  );
+}
+
+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) {
+    if (has_property(inst.products.at(i), "lookup")
+        && current_ingredient_and_aliases.find(inst.products.at(i)) != current_ingredient_and_aliases.end()) {
+      string current_product_name = inst.products.at(i).name;
+      if (current_product_name == original_ingredient_name)
+        raise << maybe(caller.name) << "cannot modify '" << current_product_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end();
+      else
+        raise << maybe(caller.name) << "cannot modify '" << current_product_name << "' in instruction '" << to_original_string(inst) << "' because that would modify " << original_ingredient_name << " which is an ingredient of recipe " << caller.name << " but not also a product\n" << end();
+      return;
+    }
+  }
+  // check if there's any indirect modification going on
+  set<int> current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases);
+  if (current_ingredient_indices.empty()) return;  // ingredient not found in call
+  for (set<int>::iterator p = current_ingredient_indices.begin();  p != current_ingredient_indices.end();  ++p) {
+    const int current_ingredient_index = *p;
+    reagent current_ingredient = inst.ingredients.at(current_ingredient_index);
+    canonize_type(current_ingredient);
+    const string& current_ingredient_name = current_ingredient.name;
+    if (!contains_key(Recipe, inst.operation)) {
+      // primitive recipe
+      // we got here only because we got an instruction with an implicit product, and the instruction didn't explicitly spell it out
+      //    put x, y:offset, z
+      // instead of
+      //    x <- put x, y:offset, z
+      if (inst.operation == PUT || inst.operation == PUT_INDEX) {
+        if (current_ingredient_index == 0) {
+          if (current_ingredient_name == original_ingredient_name)
+            raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end();
+          else
+            raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because that would modify '" << original_ingredient_name << "' which is an ingredient of recipe " << caller.name << " but not also a product\n" << end();
+        }
+      }
+    }
+    else {
+      // defined recipe
+      if (is_modified_in_recipe(inst.operation, current_ingredient_index, caller)) {
+        if (current_ingredient_name == original_ingredient_name)
+          raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end();
+        else
+          raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because that would modify '" << original_ingredient_name << "' which is an ingredient of recipe " << caller.name << " but not also a product\n" << end();
+      }
+    }
+  }
+}
+
+bool is_modified_in_recipe(const recipe_ordinal r, const int ingredient_index, const recipe& caller) {
+  const recipe& callee = get(Recipe, r);
+  if (!callee.has_header) {
+    raise << maybe(caller.name) << "can't check mutability of ingredients in recipe " << callee.name << " because it uses 'next-ingredient' directly, rather than a recipe header.\n" << end();
+    return true;
+  }
+  if (ingredient_index >= SIZE(callee.ingredients)) return false;  // optional immutable ingredient
+  return is_present_in_products(callee, callee.ingredients.at(ingredient_index).name);
+}
+
+bool is_present_in_products(const recipe& callee, const string& ingredient_name) {
+  for (int i = 0;  i < SIZE(callee.products);  ++i) {
+    if (callee.products.at(i).name == ingredient_name)
+      return true;
+  }
+  return false;
+}
+
+set<int> ingredient_indices(const instruction& inst, const set<reagent, name_and_space_lt>& ingredient_names) {
+  set<int> result;
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (is_literal(inst.ingredients.at(i))) continue;
+    if (ingredient_names.find(inst.ingredients.at(i)) != ingredient_names.end())
+      result.insert(i);
+  }
+  return result;
+}
+
+//: Sometimes you want to pass in two addresses, one pointing inside the
+//: other. For example, you want to delete a node from a linked list. You
+//: can't pass both pointers back out, because if a caller tries to make both
+//: identical then you can't tell which value will be written on the way out.
+//:
+//: Experimental solution: just tell Mu that one points inside the other.
+//: This way we can return just one pointer as high up as necessary to capture
+//: all modifications performed by a recipe.
+//:
+//: We'll see if we end up wanting to abuse /contained-in for other reasons.
+
+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")) {
+  const string_tree* tmp = property(current_ingredient, "contained-in");
+  if (!tmp->atom
+      || (!is_present_in_ingredients(caller, tmp->value)
+          && !is_present_in_products(caller, tmp->value))) {
+    raise << maybe(caller.name) << "/contained-in can only point to another ingredient or product, but got '" << to_string(property(current_ingredient, "contained-in")) << "'\n" << end();
+  }
+  continue;
+}
+
+:(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/archive/2.vm/058to_text.cc b/archive/2.vm/058to_text.cc
new file mode 100644
index 00000000..9cb14e14
--- /dev/null
+++ b/archive/2.vm/058to_text.cc
@@ -0,0 +1,24 @@
+//: Primitive to convert any type to text (array of characters).
+//: Later layers will allow us to override this to do something smarter for
+//: specific types.
+
+:(before "End Primitive Recipe Declarations")
+TO_TEXT,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "to-text", TO_TEXT);
+:(before "End Primitive Recipe Checks")
+case TO_TEXT: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'to-text' requires a single ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  // can handle any type
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case TO_TEXT: {
+  products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
+  products.at(0).push_back(new_mu_text(inspect(current_instruction().ingredients.at(0), ingredients.at(0))));
+  break;
+}
diff --git a/archive/2.vm/059to_text.mu b/archive/2.vm/059to_text.mu
new file mode 100644
index 00000000..d45afb0a
--- /dev/null
+++ b/archive/2.vm/059to_text.mu
@@ -0,0 +1,48 @@
+# A couple of variants of 'to-text' that we'll use implicitly in stashes (see
+# later layers).
+#
+# Mu code might specialize them to be smarter, but I don't anticipate any need
+# beyond specializing 'to-text' itself.
+
+# 'shorter' variant of to-text, when you want to enable some sort of trimming
+# define it to be identical to 'to-text' by default
+def to-text-line x:_elem -> y:text [
+  local-scope
+  load-inputs
+  y <- to-text x
+]
+
+# variant for arrays (since we can't pass them around otherwise)
+def array-to-text-line x:&:@:_elem -> y:text [
+  local-scope
+  load-inputs
+  y <- to-text *x
+]
+
+scenario to-text-line-early-warning-for-static-dispatch [
+  x:text <- to-text-line 34
+  # just ensure there were no errors
+]
+
+scenario array-to-text-line-early-warning-for-static-dispatch [
+  n:&:@:num <- new number:type, 3
+  x:text <- array-to-text-line n
+  # just ensure there were no errors
+]
+
+# finally, a specialization for single characters
+def to-text c:char -> y:text [
+  local-scope
+  load-inputs
+  y <- new character:type, 1/capacity
+  *y <- put-index *y, 0, c
+]
+
+scenario character-to-text [
+  1:char <- copy 111/o
+  2:text <- to-text 1:char
+  3:@:char <- copy *2:text
+  memory-should-contain [
+    3:array:character <- [o]
+  ]
+]
diff --git a/archive/2.vm/060rewrite_literal_string.cc b/archive/2.vm/060rewrite_literal_string.cc
new file mode 100644
index 00000000..3e64fd7c
--- /dev/null
+++ b/archive/2.vm/060rewrite_literal_string.cc
@@ -0,0 +1,81 @@
+//: allow using literal strings anywhere that will accept immutable strings
+
+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();
+Transform.push_back(rewrite_literal_string_to_text);  // idempotent
+
+:(before "End Globals")
+set<string> recipes_taking_literal_strings;
+:(code)
+void initialize_transform_rewrite_literal_string_to_text() {
+  recipes_taking_literal_strings.insert("assert");
+  recipes_taking_literal_strings.insert("$print");
+  recipes_taking_literal_strings.insert("$dump-trace");
+  recipes_taking_literal_strings.insert("$system");
+  recipes_taking_literal_strings.insert("trace");
+  recipes_taking_literal_strings.insert("stash");
+  recipes_taking_literal_strings.insert("new");
+  recipes_taking_literal_strings.insert("run");
+  recipes_taking_literal_strings.insert("memory-should-contain");
+  recipes_taking_literal_strings.insert("trace-should-contain");
+  recipes_taking_literal_strings.insert("trace-should-not-contain");
+  recipes_taking_literal_strings.insert("check-trace-count-for-label");
+  recipes_taking_literal_strings.insert("check-trace-count-for-label-greater-than");
+  recipes_taking_literal_strings.insert("check-trace-count-for-label-lesser-than");
+  // End initialize_transform_rewrite_literal_string_to_text()
+}
+
+void rewrite_literal_string_to_text(const recipe_ordinal r) {
+  recipe& caller = get(Recipe, r);
+  trace(101, "transform") << "--- rewrite literal strings in recipe " << caller.name << end();
+  if (contains_numeric_locations(caller)) return;
+  vector<instruction> new_instructions;
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    instruction& inst = caller.steps.at(i);
+    if (recipes_taking_literal_strings.find(inst.name) == recipes_taking_literal_strings.end()) {
+      for (int j = 0;  j < SIZE(inst.ingredients);  ++j) {
+        if (!is_literal_text(inst.ingredients.at(j))) continue;
+        instruction def;
+        ostringstream ingredient_name;
+        ingredient_name << inst.name << '_' << i << '_' << j << ":text";
+        def.name = "new";
+        def.ingredients.push_back(inst.ingredients.at(j));
+        def.products.push_back(reagent(ingredient_name.str()));
+        new_instructions.push_back(def);
+        inst.ingredients.at(j).clear();  // reclaim old memory
+        inst.ingredients.at(j) = reagent(ingredient_name.str());
+      }
+    }
+    new_instructions.push_back(inst);
+  }
+  caller.steps.swap(new_instructions);
+}
+
+bool contains_numeric_locations(const recipe& caller) {
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    const instruction& inst = caller.steps.at(i);
+    for (int in = 0;  in < SIZE(inst.ingredients);  ++in)
+      if (is_numeric_location(inst.ingredients.at(in)))
+        return true;
+    for (int out = 0;  out < SIZE(inst.products);  ++out)
+      if (is_numeric_location(inst.products.at(out)))
+        return true;
+  }
+  return false;
+}
diff --git a/archive/2.vm/061text.mu b/archive/2.vm/061text.mu
new file mode 100644
index 00000000..4d46319b
--- /dev/null
+++ b/archive/2.vm/061text.mu
@@ -0,0 +1,1427 @@
+# Some useful helpers for dealing with text (arrays of characters)
+
+def equal a:text, b:text -> result:bool [
+  local-scope
+  load-inputs
+  an:num, bn:num <- deaddress a, b
+  address-equal?:boolean <- equal an, bn
+  return-if address-equal?, true
+  return-unless a, false
+  return-unless b, false
+  a-len:num <- length *a
+  b-len:num <- length *b
+  # compare lengths
+  trace 99, [text-equal], [comparing lengths]
+  length-equal?:bool <- equal a-len, b-len
+  return-unless length-equal?, false
+  # compare each corresponding character
+  trace 99, [text-equal], [comparing characters]
+  i:num <- copy 0
+  {
+    done?:bool <- greater-or-equal i, a-len
+    break-if done?
+    a2:char <- index *a, i
+    b2:char <- index *b, i
+    chars-match?:bool <- equal a2, b2
+    return-unless chars-match?, false
+    i <- add i, 1
+    loop
+  }
+  return true
+]
+
+scenario text-equal-reflexive [
+  local-scope
+  x:text <- new [abc]
+  run [
+    10:bool/raw <- equal x, x
+  ]
+  memory-should-contain [
+    10 <- 1  # x == x for all x
+  ]
+]
+
+scenario text-equal-identical [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [abc]
+  run [
+    10:bool/raw <- equal x, y
+  ]
+  memory-should-contain [
+    10 <- 1  # abc == abc
+  ]
+]
+
+scenario text-equal-distinct-lengths [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [abcd]
+  run [
+    10:bool/raw <- equal x, y
+  ]
+  memory-should-contain [
+    10 <- 0  # abc != abcd
+  ]
+  trace-should-contain [
+    text-equal: comparing lengths
+  ]
+  trace-should-not-contain [
+    text-equal: comparing characters
+  ]
+]
+
+scenario text-equal-with-empty [
+  local-scope
+  x:text <- new []
+  y:text <- new [abcd]
+  run [
+    10:bool/raw <- equal x, y
+  ]
+  memory-should-contain [
+    10 <- 0  # "" != abcd
+  ]
+]
+
+scenario text-equal-with-null [
+  local-scope
+  x:text <- new [abcd]
+  y:text <- copy null
+  run [
+    10:bool/raw <- equal x, null
+    11:bool/raw <- equal null, x
+    12:bool/raw <- equal x, y
+    13:bool/raw <- equal y, x
+    14:bool/raw <- equal y, y
+  ]
+  memory-should-contain [
+    10 <- 0
+    11 <- 0
+    12 <- 0
+    13 <- 0
+    14 <- 1
+  ]
+  check-trace-count-for-label 0, [error]
+]
+
+scenario text-equal-common-lengths-but-distinct [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [abd]
+  run [
+    10:bool/raw <- equal x, y
+  ]
+  memory-should-contain [
+    10 <- 0  # abc != abd
+  ]
+]
+
+# A new type to help incrementally construct texts.
+container buffer:_elem [
+  length:num
+  data:&:@:_elem
+]
+
+def new-buffer capacity:num -> result:&:buffer:_elem [
+  local-scope
+  load-inputs
+  result <- new {(buffer _elem): type}
+  *result <- put *result, length:offset, 0
+  {
+    break-if capacity
+    # capacity not provided
+    capacity <- copy 10
+  }
+  data:&:@:_elem <- new _elem:type, capacity
+  *result <- put *result, data:offset, data
+  return result
+]
+
+def grow-buffer buf:&:buffer:_elem -> buf:&:buffer:_elem [
+  local-scope
+  load-inputs
+  # double buffer size
+  olddata:&:@:_elem <- get *buf, data:offset
+  oldlen:num <- length *olddata
+  newlen:num <- multiply oldlen, 2
+  newdata:&:@:_elem <- new _elem:type, newlen
+  *buf <- put *buf, data:offset, newdata
+  # copy old contents
+  i:num <- copy 0
+  {
+    done?:bool <- greater-or-equal i, oldlen
+    break-if done?
+    src:_elem <- index *olddata, i
+    *newdata <- put-index *newdata, i, src
+    i <- add i, 1
+    loop
+  }
+]
+
+def buffer-full? in:&:buffer:_elem -> result:bool [
+  local-scope
+  load-inputs
+  len:num <- get *in, length:offset
+  s:&:@:_elem <- get *in, data:offset
+  capacity:num <- length *s
+  result <- greater-or-equal len, capacity
+]
+
+# most broadly applicable definition of append to a buffer
+def append buf:&:buffer:_elem, x:_elem -> buf:&:buffer:_elem [
+  local-scope
+  load-inputs
+  len:num <- get *buf, length:offset
+  {
+    # grow buffer if necessary
+    full?:bool <- buffer-full? buf
+    break-unless full?
+    buf <- grow-buffer buf
+  }
+  s:&:@:_elem <- get *buf, data:offset
+  *s <- put-index *s, len, x
+  len <- add len, 1
+  *buf <- put *buf, length:offset, len
+]
+
+# most broadly applicable definition of append to a buffer of characters: just
+# call to-text
+def append buf:&:buffer:char, x:_elem -> buf:&:buffer:char [
+  local-scope
+  load-inputs
+  text:text <- to-text x
+  buf <- append buf, text
+]
+
+# specialization for characters that is backspace-aware
+def append buf:&:buffer:char, c:char -> buf:&:buffer:char [
+  local-scope
+  load-inputs
+  len:num <- get *buf, length:offset
+  {
+    # backspace? just drop last character if it exists and return
+    backspace?:bool <- equal c, 8/backspace
+    break-unless backspace?
+    empty?:bool <- lesser-or-equal len, 0
+    return-if empty?
+    len <- subtract len, 1
+    *buf <- put *buf, length:offset, len
+    return
+  }
+  {
+    # grow buffer if necessary
+    full?:bool <- buffer-full? buf
+    break-unless full?
+    buf <- grow-buffer buf
+  }
+  s:text <- get *buf, data:offset
+  *s <- put-index *s, len, c
+  len <- add len, 1
+  *buf <- put *buf, length:offset, len
+]
+
+def append buf:&:buffer:_elem, t:&:@:_elem -> buf:&:buffer:_elem [
+  local-scope
+  load-inputs
+  len:num <- length *t
+  i:num <- copy 0
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    x:_elem <- index *t, i
+    buf <- append buf, x
+    i <- add i, 1
+    loop
+  }
+]
+
+scenario append-to-empty-buffer [
+  local-scope
+  x:&:buffer:char <- new-buffer
+  run [
+    c:char <- copy 97/a
+    x <- append x, c
+    10:num/raw <- get *x, length:offset
+    s:text <- get *x, data:offset
+    11:char/raw <- index *s, 0
+    12:char/raw <- index *s, 1
+  ]
+  memory-should-contain [
+    10 <- 1  # buffer length
+    11 <- 97  # a
+    12 <- 0  # rest of buffer is empty
+  ]
+]
+
+scenario append-to-buffer [
+  local-scope
+  x:&:buffer:char <- new-buffer
+  c:char <- copy 97/a
+  x <- append x, c
+  run [
+    c <- copy 98/b
+    x <- append x, c
+    10:num/raw <- get *x, length:offset
+    s:text <- get *x, data:offset
+    11:char/raw <- index *s, 0
+    12:char/raw <- index *s, 1
+    13:char/raw <- index *s, 2
+  ]
+  memory-should-contain [
+    10 <- 2  # buffer length
+    11 <- 97  # a
+    12 <- 98  # b
+    13 <- 0  # rest of buffer is empty
+  ]
+]
+
+scenario append-grows-buffer [
+  local-scope
+  x:&:buffer:char <- new-buffer 3
+  s1:text <- get *x, data:offset
+  x <- append x, [abc]  # buffer is now full
+  s2:text <- get *x, data:offset
+  run [
+    10:bool/raw <- equal s1, s2
+    11:@:char/raw <- copy *s2
+    +buffer-filled
+    c:char <- copy 100/d
+    x <- append x, c
+    s3:text <- get *x, data:offset
+    20:bool/raw <- equal s1, s3
+    21:num/raw <- get *x, length:offset
+    30:@:char/raw <- copy *s3
+  ]
+  memory-should-contain [
+    # before +buffer-filled
+    10 <- 1   # no change in data pointer after original append
+    11 <- 3   # size of data
+    12 <- 97  # data
+    13 <- 98
+    14 <- 99
+    # in the end
+    20 <- 0   # data pointer has grown after second append
+    21 <- 4   # final length
+    30 <- 6   # but data's capacity has doubled
+    31 <- 97  # data
+    32 <- 98
+    33 <- 99
+    34 <- 100
+    35 <- 0
+    36 <- 0
+  ]
+]
+
+scenario buffer-append-handles-backspace [
+  local-scope
+  x:&:buffer:char <- new-buffer
+  x <- append x, [ab]
+  run [
+    c:char <- copy 8/backspace
+    x <- append x, c
+    s:text <- buffer-to-array x
+    10:@:char/raw <- copy *s
+  ]
+  memory-should-contain [
+    10 <- 1   # length
+    11 <- 97  # contents
+    12 <- 0
+  ]
+]
+
+scenario append-to-buffer-of-non-characters [
+  local-scope
+  x:&:buffer:text <- new-buffer 1/capacity
+  # no errors
+]
+
+def buffer-to-array in:&:buffer:_elem -> result:&:@:_elem [
+  local-scope
+  load-inputs
+  # propagate null buffer
+  return-unless in, null
+  len:num <- get *in, length:offset
+  s:&:@:_elem <- get *in, data:offset
+  # we can't just return s because it is usually the wrong length
+  result <- new _elem:type, len
+  i:num <- copy 0
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    src:_elem <- index *s, i
+    *result <- put-index *result, i, src
+    i <- add i, 1
+    loop
+  }
+]
+
+def blank? x:&:@:_elem -> result:bool [
+  local-scope
+  load-inputs
+  return-unless x, true
+  len:num <- length *x
+  result <- equal len, 0
+]
+
+# Append any number of texts together.
+# A later layer also translates calls to this to implicitly call to-text, so
+# append to string becomes effectively dynamically typed.
+#
+# Beware though: this hack restricts how much 'append' can be overridden. Any
+# new variants that match:
+#   append _:text, ___
+# will never ever get used.
+def append first:text -> result:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 30
+  # append first input
+  {
+    break-unless first
+    buf <- append buf, first
+  }
+  # append remaining inputs
+  {
+    arg:text, arg-found?:bool <- next-input
+    break-unless arg-found?
+    loop-unless arg
+    buf <- append buf, arg
+    loop
+  }
+  result <- buffer-to-array buf
+]
+
+scenario text-append-1 [
+  local-scope
+  x:text <- new [hello,]
+  y:text <- new [ world!]
+  run [
+    z:text <- append x, y
+    10:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [hello, world!]
+  ]
+]
+
+scenario text-append-null [
+  local-scope
+  x:text <- copy null
+  y:text <- new [ world!]
+  run [
+    z:text <- append x, y
+    10:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [ world!]
+  ]
+]
+
+scenario text-append-null-2 [
+  local-scope
+  x:text <- new [hello,]
+  y:text <- copy null
+  run [
+    z:text <- append x, y
+    10:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [hello,]
+  ]
+]
+
+scenario text-append-multiary [
+  local-scope
+  x:text <- new [hello, ]
+  y:text <- new [world]
+  z:text <- new [!]
+  run [
+    z:text <- append x, y, z
+    10:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [hello, world!]
+  ]
+]
+
+scenario replace-character-in-text [
+  local-scope
+  x:text <- new [abc]
+  run [
+    x <- replace x, 98/b, 122/z
+    10:@:char/raw <- copy *x
+  ]
+  memory-should-contain [
+    10:array:character <- [azc]
+  ]
+]
+
+def replace s:text, oldc:char, newc:char, from:num/optional -> s:text [
+  local-scope
+  load-inputs
+  len:num <- length *s
+  i:num <- find-next s, oldc, from
+  done?:bool <- greater-or-equal i, len
+  return-if done?
+  *s <- put-index *s, i, newc
+  i <- add i, 1
+  s <- replace s, oldc, newc, i
+]
+
+scenario replace-character-at-start [
+  local-scope
+  x:text <- new [abc]
+  run [
+    x <- replace x, 97/a, 122/z
+    10:@:char/raw <- copy *x
+  ]
+  memory-should-contain [
+    10:array:character <- [zbc]
+  ]
+]
+
+scenario replace-character-at-end [
+  local-scope
+  x:text <- new [abc]
+  run [
+    x <- replace x, 99/c, 122/z
+    10:@:char/raw <- copy *x
+  ]
+  memory-should-contain [
+    10:array:character <- [abz]
+  ]
+]
+
+scenario replace-character-missing [
+  local-scope
+  x:text <- new [abc]
+  run [
+    x <- replace x, 100/d, 122/z
+    10:@:char/raw <- copy *x
+  ]
+  memory-should-contain [
+    10:array:character <- [abc]
+  ]
+]
+
+scenario replace-all-characters [
+  local-scope
+  x:text <- new [banana]
+  run [
+    x <- replace x, 97/a, 122/z
+    10:@:char/raw <- copy *x
+  ]
+  memory-should-contain [
+    10:array:character <- [bznznz]
+  ]
+]
+
+# replace underscores in first with remaining args
+def interpolate template:text -> result:text [
+  local-scope
+  load-inputs  # consume just the template
+  # compute result-len, space to allocate for result
+  tem-len:num <- length *template
+  result-len:num <- copy tem-len
+  {
+    # while inputs remain
+    a:text, arg-received?:bool <- next-input
+    break-unless arg-received?
+    # result-len = result-len + arg.length - 1 (for the 'underscore' being replaced)
+    a-len:num <- length *a
+    result-len <- add result-len, a-len
+    result-len <- subtract result-len, 1
+    loop
+  }
+  rewind-inputs
+  _ <- next-input  # skip template
+  result <- new character:type, result-len
+  # repeatedly copy sections of template and 'holes' into result
+  result-idx:num <- copy 0
+  i:num <- copy 0
+  {
+    # while arg received
+    a:text, arg-received?:bool <- next-input
+    break-unless arg-received?
+    # copy template into result until '_'
+    {
+      # while i < template.length
+      tem-done?:bool <- greater-or-equal i, tem-len
+      break-if tem-done?, +done
+      # while template[i] != '_'
+      in:char <- index *template, i
+      underscore?:bool <- equal in, 95/_
+      break-if underscore?
+      # result[result-idx] = template[i]
+      *result <- put-index *result, result-idx, in
+      i <- add i, 1
+      result-idx <- add result-idx, 1
+      loop
+    }
+    # copy 'a' into result
+    j:num <- copy 0
+    {
+      # while j < a.length
+      arg-done?:bool <- greater-or-equal j, a-len
+      break-if arg-done?
+      # result[result-idx] = a[j]
+      in:char <- index *a, j
+      *result <- put-index *result, result-idx, in
+      j <- add j, 1
+      result-idx <- add result-idx, 1
+      loop
+    }
+    # skip '_' in template
+    i <- add i, 1
+    loop  # interpolate next arg
+  }
+  +done
+  # done with holes; copy rest of template directly into result
+  {
+    # while i < template.length
+    tem-done?:bool <- greater-or-equal i, tem-len
+    break-if tem-done?
+    # result[result-idx] = template[i]
+    in:char <- index *template, i
+    *result <- put-index *result, result-idx, in
+    i <- add i, 1
+    result-idx <- add result-idx, 1
+    loop
+  }
+]
+
+scenario interpolate-works [
+  local-scope
+  x:text <- new [abc_ghi]
+  y:text <- new [def]
+  run [
+    z:text <- interpolate x, y
+    10:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [abcdefghi]
+  ]
+]
+
+scenario interpolate-at-start [
+  local-scope
+  x:text <- new [_, hello!]
+  y:text <- new [abc]
+  run [
+    z:text <- interpolate x, y
+    10:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [abc, hello!]
+    22 <- 0  # out of bounds
+  ]
+]
+
+scenario interpolate-at-end [
+  local-scope
+  x:text <- new [hello, _]
+  y:text <- new [abc]
+  run [
+    z:text <- interpolate x, y
+    10:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [hello, abc]
+  ]
+]
+
+# result:bool <- space? c:char
+def space? c:char -> result:bool [
+  local-scope
+  load-inputs
+  # most common case first
+  result <- equal c, 32/space
+  return-if result
+  result <- equal c, 10/newline
+  return-if result
+  result <- equal c, 9/tab
+  return-if result
+  result <- equal c, 13/carriage-return
+  return-if result
+  # remaining uncommon cases in sorted order
+  # http://unicode.org code-points in unicode-set Z and Pattern_White_Space
+  result <- equal c, 11/ctrl-k
+  return-if result
+  result <- equal c, 12/ctrl-l
+  return-if result
+  result <- equal c, 133/ctrl-0085
+  return-if result
+  result <- equal c, 160/no-break-space
+  return-if result
+  result <- equal c, 5760/ogham-space-mark
+  return-if result
+  result <- equal c, 8192/en-quad
+  return-if result
+  result <- equal c, 8193/em-quad
+  return-if result
+  result <- equal c, 8194/en-space
+  return-if result
+  result <- equal c, 8195/em-space
+  return-if result
+  result <- equal c, 8196/three-per-em-space
+  return-if result
+  result <- equal c, 8197/four-per-em-space
+  return-if result
+  result <- equal c, 8198/six-per-em-space
+  return-if result
+  result <- equal c, 8199/figure-space
+  return-if result
+  result <- equal c, 8200/punctuation-space
+  return-if result
+  result <- equal c, 8201/thin-space
+  return-if result
+  result <- equal c, 8202/hair-space
+  return-if result
+  result <- equal c, 8206/left-to-right
+  return-if result
+  result <- equal c, 8207/right-to-left
+  return-if result
+  result <- equal c, 8232/line-separator
+  return-if result
+  result <- equal c, 8233/paragraph-separator
+  return-if result
+  result <- equal c, 8239/narrow-no-break-space
+  return-if result
+  result <- equal c, 8287/medium-mathematical-space
+  return-if result
+  result <- equal c, 12288/ideographic-space
+]
+
+def trim s:text -> result:text [
+  local-scope
+  load-inputs
+  len:num <- length *s
+  # left trim: compute start
+  start:num <- copy 0
+  {
+    {
+      at-end?:bool <- greater-or-equal start, len
+      break-unless at-end?
+      result <- new character:type, 0
+      return
+    }
+    curr:char <- index *s, start
+    whitespace?:bool <- space? curr
+    break-unless whitespace?
+    start <- add start, 1
+    loop
+  }
+  # right trim: compute end
+  end:num <- subtract len, 1
+  {
+    not-at-start?:bool <- greater-than end, start
+    assert not-at-start?, [end ran up against start]
+    curr:char <- index *s, end
+    whitespace?:bool <- space? curr
+    break-unless whitespace?
+    end <- subtract end, 1
+    loop
+  }
+  # result = new character[end+1 - start]
+  new-len:num <- subtract end, start, -1
+  result:text <- new character:type, new-len
+  # copy the untrimmed parts between start and end
+  i:num <- copy start
+  j:num <- copy 0
+  {
+    # while i <= end
+    done?:bool <- greater-than i, end
+    break-if done?
+    # result[j] = s[i]
+    src:char <- index *s, i
+    *result <- put-index *result, j, src
+    i <- add i, 1
+    j <- add j, 1
+    loop
+  }
+]
+
+scenario trim-unmodified [
+  local-scope
+  x:text <- new [abc]
+  run [
+    y:text <- trim x
+    1:@:char/raw <- copy *y
+  ]
+  memory-should-contain [
+    1:array:character <- [abc]
+  ]
+]
+
+scenario trim-left [
+  local-scope
+  x:text <- new [  abc]
+  run [
+    y:text <- trim x
+    1:@:char/raw <- copy *y
+  ]
+  memory-should-contain [
+    1:array:character <- [abc]
+  ]
+]
+
+scenario trim-right [
+  local-scope
+  x:text <- new [abc  ]
+  run [
+    y:text <- trim x
+    1:@:char/raw <- copy *y
+  ]
+  memory-should-contain [
+    1:array:character <- [abc]
+  ]
+]
+
+scenario trim-left-right [
+  local-scope
+  x:text <- new [  abc   ]
+  run [
+    y:text <- trim x
+    1:@:char/raw <- copy *y
+  ]
+  memory-should-contain [
+    1:array:character <- [abc]
+  ]
+]
+
+scenario trim-newline-tab [
+  local-scope
+  x:text <- new [	abc
+]
+  run [
+    y:text <- trim x
+    1:@:char/raw <- copy *y
+  ]
+  memory-should-contain [
+    1:array:character <- [abc]
+  ]
+]
+
+def find-next text:text, pattern:char, idx:num -> next-index:num [
+  local-scope
+  load-inputs
+  len:num <- length *text
+  {
+    eof?:bool <- greater-or-equal idx, len
+    break-if eof?
+    curr:char <- index *text, idx
+    found?:bool <- equal curr, pattern
+    break-if found?
+    idx <- add idx, 1
+    loop
+  }
+  return idx
+]
+
+scenario text-find-next [
+  local-scope
+  x:text <- new [a/b]
+  run [
+    10:num/raw <- find-next x, 47/slash, 0/start-index
+  ]
+  memory-should-contain [
+    10 <- 1
+  ]
+]
+
+scenario text-find-next-empty [
+  local-scope
+  x:text <- new []
+  run [
+    10:num/raw <- find-next x, 47/slash, 0/start-index
+  ]
+  memory-should-contain [
+    10 <- 0
+  ]
+]
+
+scenario text-find-next-initial [
+  local-scope
+  x:text <- new [/abc]
+  run [
+    10:num/raw <- find-next x, 47/slash, 0/start-index
+  ]
+  memory-should-contain [
+    10 <- 0  # prefix match
+  ]
+]
+
+scenario text-find-next-final [
+  local-scope
+  x:text <- new [abc/]
+  run [
+    10:num/raw <- find-next x, 47/slash, 0/start-index
+  ]
+  memory-should-contain [
+    10 <- 3  # suffix match
+  ]
+]
+
+scenario text-find-next-missing [
+  local-scope
+  x:text <- new [abcd]
+  run [
+    10:num/raw <- find-next x, 47/slash, 0/start-index
+  ]
+  memory-should-contain [
+    10 <- 4  # no match
+  ]
+]
+
+scenario text-find-next-invalid-index [
+  local-scope
+  x:text <- new [abc]
+  run [
+    10:num/raw <- find-next x, 47/slash, 4/start-index
+  ]
+  memory-should-contain [
+    10 <- 4  # no change
+  ]
+]
+
+scenario text-find-next-first [
+  local-scope
+  x:text <- new [ab/c/]
+  run [
+    10:num/raw <- find-next x, 47/slash, 0/start-index
+  ]
+  memory-should-contain [
+    10 <- 2  # first '/' of multiple
+  ]
+]
+
+scenario text-find-next-second [
+  local-scope
+  x:text <- new [ab/c/]
+  run [
+    10:num/raw <- find-next x, 47/slash, 3/start-index
+  ]
+  memory-should-contain [
+    10 <- 4  # second '/' of multiple
+  ]
+]
+
+# search for a pattern of multiple characters
+# fairly dumb algorithm
+def find-next text:text, pattern:text, idx:num -> next-index:num [
+  local-scope
+  load-inputs
+  first:char <- index *pattern, 0
+  # repeatedly check for match at current idx
+  len:num <- length *text
+  {
+    # does some unnecessary work checking even when there isn't enough of text left
+    done?:bool <- greater-or-equal idx, len
+    break-if done?
+    found?:bool <- match-at text, pattern, idx
+    break-if found?
+    idx <- add idx, 1
+    # optimization: skip past indices that definitely won't match
+    idx <- find-next text, first, idx
+    loop
+  }
+  return idx
+]
+
+scenario find-next-text-1 [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [bc]
+  run [
+    10:num/raw <- find-next x, y, 0
+  ]
+  memory-should-contain [
+    10 <- 1
+  ]
+]
+
+scenario find-next-text-2 [
+  local-scope
+  x:text <- new [abcd]
+  y:text <- new [bc]
+  run [
+    10:num/raw <- find-next x, y, 1
+  ]
+  memory-should-contain [
+    10 <- 1
+  ]
+]
+
+scenario find-next-no-match [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [bd]
+  run [
+    10:num/raw <- find-next x, y, 0
+  ]
+  memory-should-contain [
+    10 <- 3  # not found
+  ]
+]
+
+scenario find-next-suffix-match [
+  local-scope
+  x:text <- new [abcd]
+  y:text <- new [cd]
+  run [
+    10:num/raw <- find-next x, y, 0
+  ]
+  memory-should-contain [
+    10 <- 2
+  ]
+]
+
+scenario find-next-suffix-match-2 [
+  local-scope
+  x:text <- new [abcd]
+  y:text <- new [cde]
+  run [
+    10:num/raw <- find-next x, y, 0
+  ]
+  memory-should-contain [
+    10 <- 4  # not found
+  ]
+]
+
+# checks if pattern matches at index 'idx'
+def match-at text:text, pattern:text, idx:num -> result:bool [
+  local-scope
+  load-inputs
+  pattern-len:num <- length *pattern
+  # check that there's space left for the pattern
+  x:num <- length *text
+  x <- subtract x, pattern-len
+  enough-room?:bool <- lesser-or-equal idx, x
+  return-unless enough-room?, false/not-found
+  # check each character of pattern
+  pattern-idx:num <- copy 0
+  {
+    done?:bool <- greater-or-equal pattern-idx, pattern-len
+    break-if done?
+    c:char <- index *text, idx
+    exp:char <- index *pattern, pattern-idx
+    match?:bool <- equal c, exp
+    return-unless match?, false/not-found
+    idx <- add idx, 1
+    pattern-idx <- add pattern-idx, 1
+    loop
+  }
+  return true/found
+]
+
+scenario match-at-checks-pattern-at-index [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [ab]
+  run [
+    10:bool/raw <- match-at x, y, 0
+  ]
+  memory-should-contain [
+    10 <- 1  # match found
+  ]
+]
+
+scenario match-at-reflexive [
+  local-scope
+  x:text <- new [abc]
+  run [
+    10:bool/raw <- match-at x, x, 0
+  ]
+  memory-should-contain [
+    10 <- 1  # match found
+  ]
+]
+
+scenario match-at-outside-bounds [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [a]
+  run [
+    10:bool/raw <- match-at x, y, 4
+  ]
+  memory-should-contain [
+    10 <- 0  # never matches
+  ]
+]
+
+scenario match-at-empty-pattern [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new []
+  run [
+    10:bool/raw <- match-at x, y, 0
+  ]
+  memory-should-contain [
+    10 <- 1  # always matches empty pattern given a valid index
+  ]
+]
+
+scenario match-at-empty-pattern-outside-bound [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new []
+  run [
+    10:bool/raw <- match-at x, y, 4
+  ]
+  memory-should-contain [
+    10 <- 0  # no match
+  ]
+]
+
+scenario match-at-empty-text [
+  local-scope
+  x:text <- new []
+  y:text <- new [abc]
+  run [
+    10:bool/raw <- match-at x, y, 0
+  ]
+  memory-should-contain [
+    10 <- 0  # no match
+  ]
+]
+
+scenario match-at-empty-against-empty [
+  local-scope
+  x:text <- new []
+  run [
+    10:bool/raw <- match-at x, x, 0
+  ]
+  memory-should-contain [
+    10 <- 1  # matches because pattern is also empty
+  ]
+]
+
+scenario match-at-inside-bounds [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [bc]
+  run [
+    10:bool/raw <- match-at x, y, 1
+  ]
+  memory-should-contain [
+    10 <- 1  # match
+  ]
+]
+
+scenario match-at-inside-bounds-2 [
+  local-scope
+  x:text <- new [abc]
+  y:text <- new [bc]
+  run [
+    10:bool/raw <- match-at x, y, 0
+  ]
+  memory-should-contain [
+    10 <- 0  # no match
+  ]
+]
+
+def split s:text, delim:char -> result:&:@:text [
+  local-scope
+  load-inputs
+  # empty text? return empty array
+  len:num <- length *s
+  {
+    empty?:bool <- equal len, 0
+    break-unless empty?
+    result <- new {(address array character): type}, 0
+    return
+  }
+  # count #pieces we need room for
+  count:num <- copy 1  # n delimiters = n+1 pieces
+  idx:num <- copy 0
+  {
+    idx <- find-next s, delim, idx
+    done?:bool <- greater-or-equal idx, len
+    break-if done?
+    idx <- add idx, 1
+    count <- add count, 1
+    loop
+  }
+  # allocate space
+  result <- new {(address array character): type}, count
+  # repeatedly copy slices start..end until delimiter into result[curr-result]
+  curr-result:num <- copy 0
+  start:num <- copy 0
+  {
+    # while next delim exists
+    done?:bool <- greater-or-equal start, len
+    break-if done?
+    end:num <- find-next s, delim, start
+    # copy start..end into result[curr-result]
+    dest:text <- copy-range s, start, end
+    *result <- put-index *result, curr-result, dest
+    # slide over to next slice
+    start <- add end, 1
+    curr-result <- add curr-result, 1
+    loop
+  }
+]
+
+scenario text-split-1 [
+  local-scope
+  x:text <- new [a/b]
+  run [
+    y:&:@:text <- split x, 47/slash
+    10:num/raw <- length *y
+    a:text <- index *y, 0
+    b:text <- index *y, 1
+    20:@:char/raw <- copy *a
+    30:@:char/raw <- copy *b
+  ]
+  memory-should-contain [
+    10 <- 2  # length of result
+    20:array:character <- [a]
+    30:array:character <- [b]
+  ]
+]
+
+scenario text-split-2 [
+  local-scope
+  x:text <- new [a/b/c]
+  run [
+    y:&:@:text <- split x, 47/slash
+    10:num/raw <- length *y
+    a:text <- index *y, 0
+    b:text <- index *y, 1
+    c:text <- index *y, 2
+    20:@:char/raw <- copy *a
+    30:@:char/raw <- copy *b
+    40:@:char/raw <- copy *c
+  ]
+  memory-should-contain [
+    10 <- 3  # length of result
+    20:array:character <- [a]
+    30:array:character <- [b]
+    40:array:character <- [c]
+  ]
+]
+
+scenario text-split-missing [
+  local-scope
+  x:text <- new [abc]
+  run [
+    y:&:@:text <- split x, 47/slash
+    10:num/raw <- length *y
+    a:text <- index *y, 0
+    20:@:char/raw <- copy *a
+  ]
+  memory-should-contain [
+    10 <- 1  # length of result
+    20:array:character <- [abc]
+  ]
+]
+
+scenario text-split-empty [
+  local-scope
+  x:text <- new []
+  run [
+    y:&:@:text <- split x, 47/slash
+    10:num/raw <- length *y
+  ]
+  memory-should-contain [
+    10 <- 0  # empty result
+  ]
+]
+
+scenario text-split-empty-piece [
+  local-scope
+  x:text <- new [a/b//c]
+  run [
+    y:&:@:text <- split x:text, 47/slash
+    10:num/raw <- length *y
+    a:text <- index *y, 0
+    b:text <- index *y, 1
+    c:text <- index *y, 2
+    d:text <- index *y, 3
+    20:@:char/raw <- copy *a
+    30:@:char/raw <- copy *b
+    40:@:char/raw <- copy *c
+    50:@:char/raw <- copy *d
+  ]
+  memory-should-contain [
+    10 <- 4  # length of result
+    20:array:character <- [a]
+    30:array:character <- [b]
+    40:array:character <- []
+    50:array:character <- [c]
+  ]
+]
+
+def split-first text:text, delim:char -> x:text, y:text [
+  local-scope
+  load-inputs
+  # empty text? return empty texts
+  len:num <- length *text
+  {
+    empty?:bool <- equal len, 0
+    break-unless empty?
+    x:text <- new []
+    y:text <- new []
+    return
+  }
+  idx:num <- find-next text, delim, 0
+  x:text <- copy-range text, 0, idx
+  idx <- add idx, 1
+  y:text <- copy-range text, idx, len
+]
+
+scenario text-split-first [
+  local-scope
+  x:text <- new [a/b]
+  run [
+    y:text, z:text <- split-first x, 47/slash
+    10:@:char/raw <- copy *y
+    20:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [a]
+    20:array:character <- [b]
+  ]
+]
+
+def copy-range buf:text, start:num, end:num -> result:text [
+  local-scope
+  load-inputs
+  # if end is out of bounds, trim it
+  len:num <- length *buf
+  end:num <- min len, end
+  # allocate space for result
+  len <- subtract end, start
+  result:text <- new character:type, len
+  # copy start..end into result[curr-result]
+  src-idx:num <- copy start
+  dest-idx:num <- copy 0
+  {
+    done?:bool <- greater-or-equal src-idx, end
+    break-if done?
+    src:char <- index *buf, src-idx
+    *result <- put-index *result, dest-idx, src
+    src-idx <- add src-idx, 1
+    dest-idx <- add dest-idx, 1
+    loop
+  }
+]
+
+scenario copy-range-works [
+  local-scope
+  x:text <- new [abc]
+  run [
+    y:text <- copy-range x, 1, 3
+    1:@:char/raw <- copy *y
+  ]
+  memory-should-contain [
+    1:array:character <- [bc]
+  ]
+]
+
+scenario copy-range-out-of-bounds [
+  local-scope
+  x:text <- new [abc]
+  run [
+    y:text <- copy-range x, 2, 4
+    1:@:char/raw <- copy *y
+  ]
+  memory-should-contain [
+    1:array:character <- [c]
+  ]
+]
+
+scenario copy-range-out-of-bounds-2 [
+  local-scope
+  x:text <- new [abc]
+  run [
+    y:text <- copy-range x, 3, 3
+    1:@:char/raw <- copy *y
+  ]
+  memory-should-contain [
+    1:array:character <- []
+  ]
+]
+
+def parse-whole-number in:text -> out:num, error?:bool [
+  local-scope
+  load-inputs
+  out <- copy 0
+  result:num <- copy 0  # temporary location
+  i:num <- copy 0
+  len:num <- length *in
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    c:char <- index *in, i
+    x:num <- character-to-code c
+    digit:num, error?:bool <- character-code-to-digit x
+    return-if error?
+    result <- multiply result, 10
+    result <- add result, digit
+    i <- add i, 1
+    loop
+  }
+  # no error; all digits were valid
+  out <- copy result
+]
+
+# (contributed by Ella Couch)
+recipe character-code-to-digit character-code:number -> result:number, error?:boolean [
+  local-scope
+  load-inputs
+  result <- copy 0
+  error? <- lesser-than character-code, 48  # '0'
+  return-if error?
+  error? <- greater-than character-code, 57  # '9'
+  return-if error?
+  result <- subtract character-code, 48
+]
+
+scenario character-code-to-digit-contain-only-digit [
+  local-scope
+  a:number <- copy 48  # character code for '0'
+  run [
+    10:number/raw, 11:boolean/raw <- character-code-to-digit a
+  ]
+  memory-should-contain [
+    10 <- 0
+    11 <- 0  # no error
+  ]
+]
+
+scenario character-code-to-digit-contain-only-digit-2 [
+  local-scope
+  a:number <- copy 57  # character code for '9'
+  run [
+    1:number/raw, 2:boolean/raw <- character-code-to-digit a
+  ]
+  memory-should-contain [
+    1 <- 9
+    2 <- 0  # no error
+  ]
+]
+
+scenario character-code-to-digit-handles-codes-lower-than-zero [
+  local-scope
+  a:number <- copy 47
+  run [
+    10:number/raw, 11:boolean/raw <- character-code-to-digit a
+  ]
+  memory-should-contain [
+    10 <- 0
+    11 <- 1  # error
+  ]
+]
+
+scenario character-code-to-digit-handles-codes-larger-than-nine [
+  local-scope
+  a:number <- copy 58
+  run [
+    10:number/raw, 11:boolean/raw <- character-code-to-digit a
+  ]
+  memory-should-contain [
+    10 <- 0
+    11 <- 1  # error
+  ]
+]
diff --git a/archive/2.vm/062convert_ingredients_to_text.cc b/archive/2.vm/062convert_ingredients_to_text.cc
new file mode 100644
index 00000000..a0a74bd4
--- /dev/null
+++ b/archive/2.vm/062convert_ingredients_to_text.cc
@@ -0,0 +1,212 @@
+//: make some recipes more friendly by trying to auto-convert their ingredients to text
+
+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
+
+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
+:(after "Transform.push_back(deduce_types_from_header)")
+Transform.push_back(convert_ingredients_to_text);  // idempotent
+
+:(code)
+void convert_ingredients_to_text(const recipe_ordinal r) {
+  recipe& caller = get(Recipe, r);
+  trace(101, "transform") << "--- convert some ingredients to text in recipe " << caller.name << end();
+  // in recipes without named locations, 'stash' is still not extensible
+  if (contains_numeric_locations(caller)) return;
+  convert_ingredients_to_text(caller);
+}
+
+void convert_ingredients_to_text(recipe& caller) {
+  vector<instruction> new_instructions;
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    instruction& inst = caller.steps.at(i);
+    // all these cases are getting hairy. how can we make this extensible?
+    if (inst.name == "stash") {
+      for (int j = 0;  j < SIZE(inst.ingredients);  ++j) {
+        if (is_literal_text(inst.ingredients.at(j))) continue;
+        ostringstream ingredient_name;
+        ingredient_name << "stash_" << i << '_' << j << ":address:array:character";
+        convert_ingredient_to_text(inst.ingredients.at(j), new_instructions, ingredient_name.str());
+      }
+    }
+    else if (inst.name == "trace") {
+      for (int j = /*skip*/2;  j < SIZE(inst.ingredients);  ++j) {
+        if (is_literal_text(inst.ingredients.at(j))) continue;
+        ostringstream ingredient_name;
+        ingredient_name << "trace_" << i << '_' << j << ":address:array:character";
+        convert_ingredient_to_text(inst.ingredients.at(j), new_instructions, ingredient_name.str());
+      }
+    }
+    else if (inst.name_before_rewrite == "append") {
+      // override only variants that try to append to a string
+      // Beware: this hack restricts how much 'append' can be overridden. Any
+      // new variants that match:
+      //   append _:text, ___
+      // will never ever get used.
+      if (is_literal_text(inst.ingredients.at(0)) || is_mu_text(inst.ingredients.at(0))) {
+        for (int j = /*skip base*/1;  j < SIZE(inst.ingredients);  ++j) {
+          ostringstream ingredient_name;
+          ingredient_name << "append_" << i << '_' << j << ":address:array:character";
+          convert_ingredient_to_text(inst.ingredients.at(j), new_instructions, ingredient_name.str());
+        }
+      }
+    }
+    trace(103, "transform") << to_string(inst) << end();
+    new_instructions.push_back(inst);
+  }
+  caller.steps.swap(new_instructions);
+}
+
+// add an instruction to convert reagent 'r' to text in list 'out', then
+// replace r with converted text
+void convert_ingredient_to_text(reagent& r, vector<instruction>& out, const string& tmp_var) {
+  if (!r.type) return;  // error; will be handled elsewhere
+  if (is_mu_text(r)) return;
+  // don't try to extend static arrays
+  if (is_static_array(r)) return;
+  instruction def;
+  if (is_lookup_of_address_of_array(r)) {
+    def.name = "array-to-text-line";
+    reagent/*copy*/ tmp = r;
+    drop_one_lookup(tmp);
+    def.ingredients.push_back(tmp);
+  }
+  else {
+    def.name = "to-text-line";
+    def.ingredients.push_back(r);
+  }
+  def.products.push_back(reagent(tmp_var));
+  trace(103, "transform") << to_string(def) << end();
+  out.push_back(def);
+  r.clear();  // reclaim old memory
+  r = reagent(tmp_var);
+}
+
+bool is_lookup_of_address_of_array(reagent/*copy*/ x) {
+  if (x.type->atom) return false;
+  if (x.type->left->name != "address") return false;
+  if (!canonize_type(x)) return false;
+  return is_mu_array(x);
+}
+
+bool is_static_array(const reagent& x) {
+  // no canonize_type()
+  return !x.type->atom && x.type->left->atom && x.type->left->name == "array";
+}
+
+//: Supporting 'append' above requires remembering what name an instruction
+//: had before any rewrites or transforms.
+:(before "End instruction Fields")
+string name_before_rewrite;
+:(before "End instruction Clear")
+name_before_rewrite.clear();
+:(before "End next_instruction(curr)")
+curr->name_before_rewrite = curr->name;
+
+:(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.
+
+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/archive/2.vm/063array.mu b/archive/2.vm/063array.mu
new file mode 100644
index 00000000..a56e87f0
--- /dev/null
+++ b/archive/2.vm/063array.mu
@@ -0,0 +1,181 @@
+scenario array-from-args [
+  run [
+    local-scope
+    x:&:@:num <- new-array 0, 1, 2
+    10:@:num/raw <- copy *x
+  ]
+  memory-should-contain [
+    10 <- 3  # array length
+    11 <- 0
+    12 <- 1
+    13 <- 2
+  ]
+]
+
+# create an array out of a list of args
+def new-array -> result:&:@:_elem [
+  local-scope
+  capacity:num <- copy 0
+  {
+    # while read curr-value
+    curr-value:_elem, exists?:bool <- next-input
+    break-unless exists?
+    capacity <- add capacity, 1
+    loop
+  }
+  result <- new _elem:type, capacity
+  rewind-inputs
+  i:num <- copy 0
+  {
+    # while read curr-value
+    done?:bool <- greater-or-equal i, capacity
+    break-if done?
+    curr-value:_elem, exists?:bool <- next-input
+    assert exists?, [error in rewinding inputs to new-array]
+    *result <- put-index *result, i, curr-value
+    i <- add i, 1
+    loop
+  }
+  return result
+]
+
+# fill an existing array with a set of numbers
+# (contributed by Caleb Couch)
+def fill array:&:@:num -> array:&:@:num [
+  local-scope
+  load-inputs
+  loopn:num <- copy 0
+  length:num <- length *array
+  {
+    length?:bool <- equal loopn, length
+    break-if length?
+    object:num, arg-received?:bool <- next-input
+    break-unless arg-received?
+    *array <- put-index *array, loopn, object
+    loopn <- add loopn, 1
+    loop
+  }
+]
+
+scenario fill-on-an-empty-array [
+  local-scope
+  array:&:@:num <- new number:type, 3
+  run [
+    array <- fill array, 1 2 3
+    10:@:num/raw <- copy *array
+  ]
+  memory-should-contain [
+    10 <- 3
+    11 <- 1
+    12 <- 2
+    13 <- 3
+  ]
+]
+
+scenario fill-overwrites-existing-values [
+  local-scope
+  array:&:@:num <- new number:type, 3
+  *array <- put-index *array, 0, 4
+  run [
+    array <- fill array, 1 2 3
+    10:@:num/raw <- copy *array
+  ]
+  memory-should-contain [
+    10 <- 3
+    11 <- 1
+    12 <- 2
+    13 <- 3
+  ]
+]
+
+scenario fill-exits-gracefully-when-given-no-inputs [
+  local-scope
+  array:&:@:num <- new number:type, 3
+  run [
+    array <- fill array
+    10:@:num/raw <- copy *array
+  ]
+  memory-should-contain [
+    10 <- 3
+    11 <- 0
+    12 <- 0
+    13 <- 0
+  ]
+]
+
+# swap two elements of an array
+# (contributed by Caleb Couch)
+def swap array:&:@:num, index1:num, index2:num -> array:&:@:num [
+  local-scope
+  load-inputs
+  object1:num <- index *array, index1
+  object2:num <- index *array, index2
+  *array <- put-index *array, index1, object2
+  *array <- put-index *array, index2, object1
+]
+
+scenario swap-works [
+  local-scope
+  array:&:@:num <- new number:type, 4
+  array <- fill array, 4 3 2 1
+  run [
+    array <- swap array, 0, 2
+    10:num/raw <- index *array, 0
+    11:num/raw <- index *array, 2
+  ]
+  memory-should-contain [
+    10 <- 2
+    11 <- 4
+  ]
+]
+
+# reverse the elements of an array
+# (contributed by Caleb Couch)
+def reverse array:&:@:_elem -> array:&:@:_elem [
+  local-scope
+  load-inputs
+  start:num <- copy 0
+  length:num <- length *array
+  end:num <- subtract length, 1
+  {
+    done?:bool <- greater-or-equal start, end
+    break-if done?
+    array <- swap array, start, end
+    start <- add start, 1
+    end <- subtract end, 1
+    loop
+  }
+]
+
+scenario reverse-array-odd-length [
+  local-scope
+  array:&:@:num <- new number:type, 3
+  array <- fill array, 3 2 1
+  run [
+    array <- reverse array
+    10:@:num/raw <- copy *array
+  ]
+  memory-should-contain [
+    10 <- 3
+    11 <- 1
+    12 <- 2
+    13 <- 3
+  ]
+]
+
+scenario reverse-array-even-length [
+  local-scope
+  array:&:@:num <- new number:type, 4
+  array <- fill array, 4 3 2 1
+  run [
+    array <- reverse array
+    10:@:num/raw <- copy *array
+  ]
+  memory-should-contain [
+    10 <- 4
+    11 <- 1
+    12 <- 2
+    13 <- 3
+    14 <- 4
+  ]
+]
diff --git a/archive/2.vm/064list.mu b/archive/2.vm/064list.mu
new file mode 100644
index 00000000..d669ec2c
--- /dev/null
+++ b/archive/2.vm/064list.mu
@@ -0,0 +1,366 @@
+# A list links up multiple objects together to make them easier to manage.
+#
+# The objects must be of the same type. If you want to store multiple types in
+# a single list, use an exclusive-container.
+
+container list:_elem [
+  value:_elem
+  next:&:list:_elem
+]
+
+def push x:_elem, l:&:list:_elem -> result:&:list:_elem/contained-in:l [
+  local-scope
+  load-inputs
+  result <- new {(list _elem): type}
+  *result <- merge x, l
+]
+
+def first in:&:list:_elem -> result:_elem [
+  local-scope
+  load-inputs
+  result <- get *in, value:offset
+]
+
+def rest in:&:list:_elem -> result:&:list:_elem/contained-in:in [
+  local-scope
+  load-inputs
+  result <- get *in, next:offset
+]
+
+scenario list-handling [
+  run [
+    local-scope
+    x:&:list:num <- push 3, null
+    x <- push 4, x
+    x <- push 5, x
+    10:num/raw <- first x
+    x <- rest x
+    11:num/raw <- first x
+    x <- rest x
+    12:num/raw <- first x
+    20:&:list:num/raw <- rest x
+  ]
+  memory-should-contain [
+    10 <- 5
+    11 <- 4
+    12 <- 3
+    20 <- 0  # nothing left
+  ]
+]
+
+def length l:&:list:_elem -> result:num [
+  local-scope
+  load-inputs
+  result <- copy 0
+  {
+    break-unless l
+    result <- add result, 1
+    l <- rest l
+    loop
+  }
+]
+
+# insert 'x' after 'in'
+def insert x:_elem, in:&:list:_elem -> in:&:list:_elem [
+  local-scope
+  load-inputs
+  new-node:&:list:_elem <- new {(list _elem): type}
+  *new-node <- put *new-node, value:offset, x
+  next-node:&:list:_elem <- get *in, next:offset
+  *in <- put *in, next:offset, new-node
+  *new-node <- put *new-node, next:offset, next-node
+]
+
+scenario inserting-into-list [
+  local-scope
+  list:&:list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list2:&:list:num <- rest list  # inside list
+    list2 <- insert 6, list2
+    # check structure
+    list2 <- copy list
+    10:num/raw <- first list2
+    list2 <- rest list2
+    11:num/raw <- first list2
+    list2 <- rest list2
+    12:num/raw <- first list2
+    list2 <- rest list2
+    13:num/raw <- first list2
+  ]
+  memory-should-contain [
+    10 <- 5  # scanning next
+    11 <- 4
+    12 <- 6  # inserted element
+    13 <- 3
+  ]
+]
+
+scenario inserting-at-end-of-list [
+  local-scope
+  list:&:list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list2:&:list:num <- rest list  # inside list
+    list2 <- rest list2  # now at end of list
+    list2 <- insert 6, list2
+    # check structure like before
+    list2 <- copy list
+    10:num/raw <- first list2
+    list2 <- rest list2
+    11:num/raw <- first list2
+    list2 <- rest list2
+    12:num/raw <- first list2
+    list2 <- rest list2
+    13:num/raw <- first list2
+  ]
+  memory-should-contain [
+    10 <- 5  # scanning next
+    11 <- 4
+    12 <- 3
+    13 <- 6  # inserted element
+  ]
+]
+
+scenario inserting-after-start-of-list [
+  local-scope
+  list:&:list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list <- insert 6, list
+    # check structure like before
+    list2:&:list:num <- copy list
+    10:num/raw <- first list2
+    list2 <- rest list2
+    11:num/raw <- first list2
+    list2 <- rest list2
+    12:num/raw <- first list2
+    list2 <- rest list2
+    13:num/raw <- first list2
+  ]
+  memory-should-contain [
+    10 <- 5  # scanning next
+    11 <- 6  # inserted element
+    12 <- 4
+    13 <- 3
+  ]
+]
+
+# remove 'x' from its surrounding list 'in'
+#
+# Returns null if and only if list is empty. Beware: in that case any other
+# pointers to the head are now invalid.
+def remove x:&:list:_elem/contained-in:in, in:&:list:_elem -> in:&:list:_elem [
+  local-scope
+  load-inputs
+  # if 'x' is null, return
+  return-unless x
+  next-node:&:list:_elem <- rest x
+  # clear next pointer of 'x'
+  *x <- put *x, next:offset, null
+  # if 'x' is at the head of 'in', return the new head
+  at-head?:bool <- equal x, in
+  return-if at-head?, next-node
+  # compute prev-node
+  prev-node:&:list:_elem <- copy in
+  curr:&:list:_elem <- rest prev-node
+  {
+    return-unless curr
+    found?:bool <- equal curr, x
+    break-if found?
+    prev-node <- copy curr
+    curr <- rest curr
+  }
+  # set its next pointer to skip 'x'
+  *prev-node <- put *prev-node, next:offset, next-node
+]
+
+scenario removing-from-list [
+  local-scope
+  list:&:list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list2:&:list:num <- rest list  # second element
+    list <- remove list2, list
+    10:bool/raw <- equal list2, null
+    # check structure like before
+    list2 <- copy list
+    11:num/raw <- first list2
+    list2 <- rest list2
+    12:num/raw <- first list2
+    20:&:list:num/raw <- rest list2
+  ]
+  memory-should-contain [
+    10 <- 0  # remove returned non-null
+    11 <- 5  # scanning next, skipping deleted element
+    12 <- 3
+    20 <- 0  # no more elements
+  ]
+]
+
+scenario removing-from-start-of-list [
+  local-scope
+  list:&:list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list <- remove list, list
+    # check structure like before
+    list2:&:list:num <- copy list
+    10:num/raw <- first list2
+    list2 <- rest list2
+    11:num/raw <- first list2
+    20:&:list:num/raw <- rest list2
+  ]
+  memory-should-contain [
+    10 <- 4  # scanning next, skipping deleted element
+    11 <- 3
+    20 <- 0  # no more elements
+  ]
+]
+
+scenario removing-from-end-of-list [
+  local-scope
+  list:&:list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    # delete last element
+    list2:&:list:num <- rest list
+    list2 <- rest list2
+    list <- remove list2, list
+    10:bool/raw <- equal list2, null
+    # check structure like before
+    list2 <- copy list
+    11:num/raw <- first list2
+    list2 <- rest list2
+    12:num/raw <- first list2
+    20:&:list:num/raw <- rest list2
+  ]
+  memory-should-contain [
+    10 <- 0  # remove returned non-null
+    11 <- 5  # scanning next, skipping deleted element
+    12 <- 4
+    20 <- 0  # no more elements
+  ]
+]
+
+scenario removing-from-singleton-list [
+  local-scope
+  list:&:list:num <- push 3, null
+  run [
+    list <- remove list, list
+    1:num/raw <- deaddress list
+  ]
+  memory-should-contain [
+    1 <- 0  # back to an empty list
+  ]
+]
+
+# reverse the elements of a list
+# (contributed by Caleb Couch)
+def reverse list:&:list:_elem temp:&:list:_elem/contained-in:result -> result:&:list:_elem [
+  local-scope
+  load-inputs
+  return-unless list, temp
+  object:_elem <- first, list
+  list <- rest list
+  temp <- push object, temp
+  result <- reverse list, temp
+]
+
+scenario reverse-list [
+  local-scope
+  list:&:list:num <- push 1, null
+  list <- push 2, list
+  list <- push 3, list
+  run [
+    stash [list:], list
+    list <- reverse list
+    stash [reversed:], list
+  ]
+  trace-should-contain [
+    app: list: 3 -> 2 -> 1
+    app: reversed: 1 -> 2 -> 3
+  ]
+]
+
+scenario stash-list [
+  local-scope
+  list:&:list:num <- push 1, null
+  list <- push 2, list
+  list <- push 3, list
+  run [
+    stash [list:], list
+  ]
+  trace-should-contain [
+    app: list: 3 -> 2 -> 1
+  ]
+]
+
+def to-text in:&:list:_elem -> result:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 80
+  buf <- to-buffer in, buf
+  result <- buffer-to-array buf
+]
+
+# variant of 'to-text' which stops printing after a few elements (and so is robust to cycles)
+def to-text-line in:&:list:_elem -> result:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 80
+  buf <- to-buffer in, buf, 6  # max elements to display
+  result <- buffer-to-array buf
+]
+
+def to-buffer in:&:list:_elem, buf:&:buffer:char -> buf:&:buffer:char [
+  local-scope
+  load-inputs
+  {
+    break-if in
+    buf <- append buf, [[]]
+    return
+  }
+  # append in.value to buf
+  val:_elem <- get *in, value:offset
+  buf <- append buf, val
+  # now prepare next
+  next:&:list:_elem <- rest in
+  nextn:num <- deaddress next
+  return-unless next
+  buf <- append buf, [ -> ]
+  # and recurse
+  remaining:num, optional-input-found?:bool <- next-input
+  {
+    break-if optional-input-found?
+    # unlimited recursion
+    buf <- to-buffer next, buf
+    return
+  }
+  {
+    break-unless remaining
+    # limited recursion
+    remaining <- subtract remaining, 1
+    buf <- to-buffer next, buf, remaining
+    return
+  }
+  # past recursion depth; insert ellipses and stop
+  append buf, [...]
+]
+
+scenario stash-empty-list [
+  local-scope
+  x:&:list:num <- copy null
+  run [
+    stash x
+  ]
+  trace-should-contain [
+    app: []
+  ]
+]
diff --git a/archive/2.vm/065duplex_list.mu b/archive/2.vm/065duplex_list.mu
new file mode 100644
index 00000000..3a7de8f6
--- /dev/null
+++ b/archive/2.vm/065duplex_list.mu
@@ -0,0 +1,781 @@
+# A doubly linked list permits bidirectional traversal.
+
+container duplex-list:_elem [
+  value:_elem
+  next:&:duplex-list:_elem
+  prev:&:duplex-list:_elem
+]
+
+def push x:_elem, in:&:duplex-list:_elem/contained-in:result -> result:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  result:&:duplex-list:_elem <- new {(duplex-list _elem): type}
+  *result <- merge x, in, null
+  return-unless in
+  put *in, prev:offset, result
+]
+
+def first in:&:duplex-list:_elem -> result:_elem [
+  local-scope
+  load-inputs
+  {
+    break-if in
+    zero:&:_elem <- new _elem:type
+    zero-result:_elem <- copy *zero
+    abandon zero
+    return zero-result
+  }
+  result <- get *in, value:offset
+]
+
+def next in:&:duplex-list:_elem -> result:&:duplex-list:_elem/contained-in:in [
+  local-scope
+  load-inputs
+  return-unless in, null
+  result <- get *in, next:offset
+]
+
+def prev in:&:duplex-list:_elem -> result:&:duplex-list:_elem/contained-in:in [
+  local-scope
+  load-inputs
+  return-unless in, null
+  result <- get *in, prev:offset
+  return result
+]
+
+scenario duplex-list-handling [
+  run [
+    local-scope
+    # reserve locations 0-9 to check for missing null check
+    10:num/raw <- copy 34
+    11:num/raw <- copy 35
+    list:&:duplex-list:num <- push 3, null
+    list <- push 4, list
+    list <- push 5, list
+    list2:&:duplex-list:num <- copy list
+    20:num/raw <- first list2
+    list2 <- next list2
+    21:num/raw <- first list2
+    list2 <- next list2
+    22:num/raw <- first list2
+    30:&:duplex-list:num/raw <- next list2
+    31:num/raw <- first 30:&:duplex-list:num/raw
+    32:&:duplex-list:num/raw <- next 30:&:duplex-list:num/raw
+    33:&:duplex-list:num/raw <- prev 30:&:duplex-list:num/raw
+    list2 <- prev list2
+    40:num/raw <- first list2
+    list2 <- prev list2
+    41:num/raw <- first list2
+    50:bool/raw <- equal list, list2
+  ]
+  memory-should-contain [
+    0 <- 0  # no modifications to null pointers
+    10 <- 34
+    11 <- 35
+    20 <- 5  # scanning next
+    21 <- 4
+    22 <- 3
+    30 <- 0  # null
+    31 <- 0  # first of null
+    32 <- 0  # next of null
+    33 <- 0  # prev of null
+    40 <- 4  # then start scanning prev
+    41 <- 5
+    50 <- 1  # list back at start
+  ]
+]
+
+def length l:&:duplex-list:_elem -> result:num [
+  local-scope
+  load-inputs
+  result <- copy 0
+  {
+    break-unless l
+    result <- add result, 1
+    l <- next l
+    loop
+  }
+]
+
+# insert 'x' after 'in'
+def insert x:_elem, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  new-node:&:duplex-list:_elem <- new {(duplex-list _elem): type}
+  *new-node <- put *new-node, value:offset, x
+  # save old next before changing it
+  next-node:&:duplex-list:_elem <- get *in, next:offset
+  *in <- put *in, next:offset, new-node
+  *new-node <- put *new-node, prev:offset, in
+  *new-node <- put *new-node, next:offset, next-node
+  return-unless next-node
+  *next-node <- put *next-node, prev:offset, new-node
+]
+
+scenario inserting-into-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list2:&:duplex-list:num <- next list  # inside list
+    list2 <- insert 6, list2
+    # check structure like before
+    list2 <- copy list
+    10:num/raw <- first list2
+    list2 <- next list2
+    11:num/raw <- first list2
+    list2 <- next list2
+    12:num/raw <- first list2
+    list2 <- next list2
+    13:num/raw <- first list2
+    list2 <- prev list2
+    20:num/raw <- first list2
+    list2 <- prev list2
+    21:num/raw <- first list2
+    list2 <- prev list2
+    22:num/raw <- first list2
+    30:bool/raw <- equal list, list2
+  ]
+  memory-should-contain [
+    10 <- 5  # scanning next
+    11 <- 4
+    12 <- 6  # inserted element
+    13 <- 3
+    20 <- 6  # then prev
+    21 <- 4
+    22 <- 5
+    30 <- 1  # list back at start
+  ]
+]
+
+scenario inserting-at-end-of-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list2:&:duplex-list:num <- next list  # inside list
+    list2 <- next list2  # now at end of list
+    list2 <- insert 6, list2
+    # check structure like before
+    list2 <- copy list
+    10:num/raw <- first list2
+    list2 <- next list2
+    11:num/raw <- first list2
+    list2 <- next list2
+    12:num/raw <- first list2
+    list2 <- next list2
+    13:num/raw <- first list2
+    list2 <- prev list2
+    20:num/raw <- first list2
+    list2 <- prev list2
+    21:num/raw <- first list2
+    list2 <- prev list2
+    22:num/raw <- first list2
+    30:bool/raw <- equal list, list2
+  ]
+  memory-should-contain [
+    10 <- 5  # scanning next
+    11 <- 4
+    12 <- 3
+    13 <- 6  # inserted element
+    20 <- 3  # then prev
+    21 <- 4
+    22 <- 5
+    30 <- 1  # list back at start
+  ]
+]
+
+scenario inserting-after-start-of-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list <- insert 6, list
+    # check structure like before
+    list2:&:duplex-list:num <- copy list
+    10:num/raw <- first list2
+    list2 <- next list2
+    11:num/raw <- first list2
+    list2 <- next list2
+    12:num/raw <- first list2
+    list2 <- next list2
+    13:num/raw <- first list2
+    list2 <- prev list2
+    20:num/raw <- first list2
+    list2 <- prev list2
+    21:num/raw <- first list2
+    list2 <- prev list2
+    22:num/raw <- first list2
+    30:bool/raw <- equal list, list2
+  ]
+  memory-should-contain [
+    10 <- 5  # scanning next
+    11 <- 6  # inserted element
+    12 <- 4
+    13 <- 3
+    20 <- 4  # then prev
+    21 <- 6
+    22 <- 5
+    30 <- 1  # list back at start
+  ]
+]
+
+# remove 'x' from its surrounding list 'in'
+#
+# Returns null if and only if list is empty. Beware: in that case any other
+# pointers to the head are now invalid.
+def remove x:&:duplex-list:_elem/contained-in:in, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  # if 'x' is null, return
+  return-unless x
+  next-node:&:duplex-list:_elem <- get *x, next:offset
+  prev-node:&:duplex-list:_elem <- get *x, prev:offset
+  # null x's pointers
+  *x <- put *x, next:offset, null
+  *x <- put *x, prev:offset, null
+  # if next-node is not null, set its prev pointer
+  {
+    break-unless next-node
+    *next-node <- put *next-node, prev:offset, prev-node
+  }
+  # if prev-node is not null, set its next pointer and return
+  {
+    break-unless prev-node
+    *prev-node <- put *prev-node, next:offset, next-node
+    return
+  }
+  # if prev-node is null, then we removed the head node at 'in'
+  # return the new head rather than the old 'in'
+  return next-node
+]
+
+scenario removing-from-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list2:&:duplex-list:num <- next list  # second element
+    list <- remove list2, list
+    10:bool/raw <- equal list2, null
+    # check structure like before
+    list2 <- copy list
+    11:num/raw <- first list2
+    list2 <- next list2
+    12:num/raw <- first list2
+    20:&:duplex-list:num/raw <- next list2
+    list2 <- prev list2
+    30:num/raw <- first list2
+    40:bool/raw <- equal list, list2
+  ]
+  memory-should-contain [
+    10 <- 0  # remove returned non-null
+    11 <- 5  # scanning next, skipping deleted element
+    12 <- 3
+    20 <- 0  # no more elements
+    30 <- 5  # prev of final element
+    40 <- 1  # list back at start
+  ]
+]
+
+scenario removing-from-start-of-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list <- remove list, list
+    # check structure like before
+    list2:&:duplex-list:num <- copy list
+    10:num/raw <- first list2
+    list2 <- next list2
+    11:num/raw <- first list2
+    20:&:duplex-list:num/raw <- next list2
+    list2 <- prev list2
+    30:num/raw <- first list2
+    40:bool/raw <- equal list, list2
+  ]
+  memory-should-contain [
+    10 <- 4  # scanning next, skipping deleted element
+    11 <- 3
+    20 <- 0  # no more elements
+    30 <- 4  # prev of final element
+    40 <- 1  # list back at start
+  ]
+]
+
+scenario removing-from-end-of-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    # delete last element
+    list2:&:duplex-list:num <- next list
+    list2 <- next list2
+    list <- remove list2, list
+    10:bool/raw <- equal list2, null
+    # check structure like before
+    list2 <- copy list
+    11:num/raw <- first list2
+    list2 <- next list2
+    12:num/raw <- first list2
+    20:&:duplex-list:num/raw <- next list2
+    list2 <- prev list2
+    30:num/raw <- first list2
+    40:bool/raw <- equal list, list2
+  ]
+  memory-should-contain [
+    10 <- 0  # remove returned non-null
+    11 <- 5  # scanning next, skipping deleted element
+    12 <- 4
+    20 <- 0  # no more elements
+    30 <- 5  # prev of final element
+    40 <- 1  # list back at start
+  ]
+]
+
+scenario removing-from-singleton-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 3, null
+  run [
+    list <- remove list, list
+    1:num/raw <- deaddress list
+  ]
+  memory-should-contain [
+    1 <- 0  # back to an empty list
+  ]
+]
+
+def remove x:&:duplex-list:_elem/contained-in:in, n:num, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  i:num <- copy 0
+  curr:&:duplex-list:_elem <- copy x
+  {
+    done?:bool <- greater-or-equal i, n
+    break-if done?
+    break-unless curr
+    next:&:duplex-list:_elem <- next curr
+    in <- remove curr, in
+    curr <- copy next
+    i <- add i, 1
+    loop
+  }
+]
+
+scenario removing-multiple-from-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 3, null
+  list <- push 4, list
+  list <- push 5, list
+  run [
+    list2:&:duplex-list:num <- next list  # second element
+    list <- remove list2, 2, list
+    stash list
+  ]
+  trace-should-contain [
+    app: 5
+  ]
+]
+
+# remove values between 'start' and 'end' (both exclusive).
+# also clear pointers back out from start/end for hygiene.
+# set end to 0 to delete everything past start.
+# can't set start to 0 to delete everything before end, because there's no
+# clean way to return the new head pointer.
+def remove-between start:&:duplex-list:_elem, end:&:duplex-list:_elem/contained-in:start -> start:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  next:&:duplex-list:_elem <- get *start, next:offset
+  nothing-to-delete?:bool <- equal next, end
+  return-if nothing-to-delete?
+  assert next, [malformed duplex list]
+  # start->next->prev = 0
+  # start->next = end
+  *next <- put *next, prev:offset, null
+  *start <- put *start, next:offset, end
+  {
+    break-if end
+    stash [spliced:] next
+    return
+  }
+  # end->prev->next = 0
+  # end->prev = start
+  prev:&:duplex-list:_elem <- get *end, prev:offset
+  assert prev, [malformed duplex list - 2]
+  *prev <- put *prev, next:offset, null
+  stash [spliced:] next
+  *end <- put *end, prev:offset, start
+]
+
+scenario remove-range [
+  # construct a duplex list with six elements [13, 14, 15, 16, 17, 18]
+  local-scope
+  list:&:duplex-list:num <- push 18, null
+  list <- push 17, list
+  list <- push 16, list
+  list <- push 15, list
+  list <- push 14, list
+  list <- push 13, list
+  run [
+    # delete 16 onwards
+    # first pointer: to the third element
+    list2:&:duplex-list:num <- next list
+    list2 <- next list2
+    list2 <- remove-between list2, null
+    # now check the list
+    10:num/raw <- get *list, value:offset
+    list <- next list
+    11:num/raw <- get *list, value:offset
+    list <- next list
+    12:num/raw <- get *list, value:offset
+    20:&:duplex-list:num/raw <- next list
+  ]
+  memory-should-contain [
+    10 <- 13
+    11 <- 14
+    12 <- 15
+    20 <- 0
+  ]
+  trace-should-contain [
+    app: spliced: 16 <-> 17 <-> 18
+  ]
+]
+
+scenario remove-range-to-final [
+  local-scope
+  # construct a duplex list with six elements [13, 14, 15, 16, 17, 18]
+  list:&:duplex-list:num <- push 18, null
+  list <- push 17, list
+  list <- push 16, list
+  list <- push 15, list
+  list <- push 14, list
+  list <- push 13, list
+  run [
+    # delete 15, 16 and 17
+    # start pointer: to the second element
+    list2:&:duplex-list:num <- next list
+    # end pointer: to the last (sixth) element
+    end:&:duplex-list:num <- next list2
+    end <- next end
+    end <- next end
+    end <- next end
+    remove-between list2, end
+    # now check the list
+    10:num/raw <- get *list, value:offset
+    list <- next list
+    11:num/raw <- get *list, value:offset
+    list <- next list
+    12:num/raw <- get *list, value:offset
+    20:&:duplex-list:num/raw <- next list
+  ]
+  memory-should-contain [
+    10 <- 13
+    11 <- 14
+    12 <- 18
+    20 <- 0  # no more elements
+  ]
+  trace-should-contain [
+    app: spliced: 15 <-> 16 <-> 17
+  ]
+]
+
+scenario remove-range-to-penultimate [
+  local-scope
+  # construct a duplex list with six elements [13, 14, 15, 16, 17, 18]
+  list:&:duplex-list:num <- push 18, null
+  list <- push 17, list
+  list <- push 16, list
+  list <- push 15, list
+  list <- push 14, list
+  list <- push 13, list
+  run [
+    # delete 15 and 16
+    # start pointer: to the second element
+    list2:&:duplex-list:num <- next list
+    # end pointer: to the last (sixth) element
+    end:&:duplex-list:num <- next list2
+    end <- next end
+    end <- next end
+    remove-between list2, end
+    # now check the list
+    10:num/raw <- get *list, value:offset
+    list <- next list
+    11:num/raw <- get *list, value:offset
+    list <- next list
+    12:num/raw <- get *list, value:offset
+    list <- next list
+    13:num/raw <- get *list, value:offset
+    20:&:duplex-list:num/raw <- next list
+  ]
+  memory-should-contain [
+    10 <- 13
+    11 <- 14
+    12 <- 17
+    13 <- 18
+    20 <- 0  # no more elements
+  ]
+  trace-should-contain [
+    app: spliced: 15 <-> 16
+  ]
+]
+
+scenario remove-range-empty [
+  local-scope
+  # construct a duplex list with three elements [13, 14, 15]
+  list:&:duplex-list:num <- push 15, null
+  list <- push 14, list
+  list <- push 13, list
+  run [
+    # delete between first and second element (i.e. nothing)
+    list2:&:duplex-list:num <- next list
+    remove-between list, list2
+    # now check the list
+    10:num/raw <- get *list, value:offset
+    list <- next list
+    11:num/raw <- get *list, value:offset
+    list <- next list
+    12:num/raw <- get *list, value:offset
+    20:&:duplex-list:num/raw <- next list
+  ]
+  # no change
+  memory-should-contain [
+    10 <- 13
+    11 <- 14
+    12 <- 15
+    20 <- 0
+  ]
+]
+
+scenario remove-range-to-end [
+  local-scope
+  # construct a duplex list with six elements [13, 14, 15, 16, 17, 18]
+  list:&:duplex-list:num <- push 18, null
+  list <- push 17, list
+  list <- push 16, list
+  list <- push 15, list
+  list <- push 14, list
+  list <- push 13, list
+  run [
+    # remove the third element and beyond
+    list2:&:duplex-list:num <- next list
+    remove-between list2, null
+    # now check the list
+    10:num/raw <- get *list, value:offset
+    list <- next list
+    11:num/raw <- get *list, value:offset
+    20:&:duplex-list:num/raw <- next list
+  ]
+  memory-should-contain [
+    10 <- 13
+    11 <- 14
+    20 <- 0
+  ]
+]
+
+# insert list beginning at 'start' after 'in'
+def splice in:&:duplex-list:_elem, start:&:duplex-list:_elem/contained-in:in -> in:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  return-unless in
+  return-unless start
+  end:&:duplex-list:_elem <- last start
+  next:&:duplex-list:_elem <- next in
+  {
+    break-unless next
+    *end <- put *end, next:offset, next
+    *next <- put *next, prev:offset, end
+  }
+  *in <- put *in, next:offset, start
+  *start <- put *start, prev:offset, in
+]
+
+# insert contents of 'new' after 'in'
+def insert in:&:duplex-list:_elem, new:&:@:_elem -> in:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  return-unless in
+  return-unless new
+  len:num <- length *new
+  return-unless len
+  curr:&:duplex-list:_elem <- copy in
+  idx:num <- copy 0
+  {
+    done?:bool <- greater-or-equal idx, len
+    break-if done?
+    c:_elem <- index *new, idx
+    insert c, curr
+    # next iter
+    curr <- next curr
+    idx <- add idx, 1
+    loop
+  }
+]
+
+def append in:&:duplex-list:_elem, new:&:duplex-list:_elem/contained-in:in -> in:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  last:&:duplex-list:_elem <- last in
+  *last <- put *last, next:offset, new
+  return-unless new
+  *new <- put *new, prev:offset, last
+]
+
+def last in:&:duplex-list:_elem -> result:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  result <- copy in
+  {
+    next:&:duplex-list:_elem <- next result
+    break-unless next
+    result <- copy next
+    loop
+  }
+]
+
+# does a duplex list start with a certain sequence of elements?
+def match x:&:duplex-list:_elem, y:&:@:_elem -> result:bool [
+  local-scope
+  load-inputs
+  i:num <- copy 0
+  max:num <- length *y
+  {
+    done?:bool <- greater-or-equal i, max
+    break-if done?
+    expected:_elem <- index *y, i
+    return-unless x, false/no-match
+    curr:_elem <- first x
+    curr-matches?:bool <- equal curr, expected
+    return-unless curr-matches?, false/no-match
+    x <- next x
+    i <- add i, 1
+    loop
+  }
+  return true/successful-match
+]
+
+scenario duplex-list-match [
+  local-scope
+  list:&:duplex-list:char <- push 97/a, null
+  list <- push 98/b, list
+  list <- push 99/c, list
+  list <- push 100/d, list
+  run [
+    10:bool/raw <- match list, []
+    11:bool/raw <- match list, [d]
+    12:bool/raw <- match list, [dc]
+    13:bool/raw <- match list, [dcba]
+    14:bool/raw <- match list, [dd]
+    15:bool/raw <- match list, [dcbax]
+  ]
+  memory-should-contain [
+    10 <- 1  # matches []
+    11 <- 1  # matches [d]
+    12 <- 1  # matches [dc]
+    13 <- 1  # matches [dcba]
+    14 <- 0  # does not match [dd]
+    15 <- 0  # does not match [dcbax]
+  ]
+]
+
+# helper for debugging
+def dump-from x:&:duplex-list:_elem [
+  local-scope
+  load-inputs
+  $print x, [: ]
+  {
+    break-unless x
+    c:_elem <- get *x, value:offset
+    $print c, [ ]
+    x <- next x
+    {
+      is-newline?:bool <- equal c, 10/newline
+      break-unless is-newline?
+      $print 10/newline
+      $print x, [: ]
+    }
+    loop
+  }
+  $print 10/newline, [---], 10/newline
+]
+
+scenario stash-duplex-list [
+  local-scope
+  list:&:duplex-list:num <- push 1, null
+  list <- push 2, list
+  list <- push 3, list
+  run [
+    stash [list:], list
+  ]
+  trace-should-contain [
+    app: list: 3 <-> 2 <-> 1
+  ]
+]
+
+def to-text in:&:duplex-list:_elem -> result:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 80
+  buf <- to-buffer in, buf
+  result <- buffer-to-array buf
+]
+
+# variant of 'to-text' which stops printing after a few elements (and so is robust to cycles)
+def to-text-line in:&:duplex-list:_elem -> result:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 80
+  buf <- to-buffer in, buf, 6  # max elements to display
+  result <- buffer-to-array buf
+]
+
+def to-buffer in:&:duplex-list:_elem, buf:&:buffer:char -> buf:&:buffer:char [
+  local-scope
+  load-inputs
+  {
+    break-if in
+    buf <- append buf, [[]]
+    return
+  }
+  # append in.value to buf
+  val:_elem <- get *in, value:offset
+  buf <- append buf, val
+  # now prepare next
+  next:&:duplex-list:_elem <- next in
+  nextn:num <- deaddress next
+  return-unless next
+  buf <- append buf, [ <-> ]
+  # and recurse
+  remaining:num, optional-input-found?:bool <- next-input
+  {
+    break-if optional-input-found?
+    # unlimited recursion
+    buf <- to-buffer next, buf
+    return
+  }
+  {
+    break-unless remaining
+    # limited recursion
+    remaining <- subtract remaining, 1
+    buf <- to-buffer next, buf, remaining
+    return
+  }
+  # past recursion depth; insert ellipses and stop
+  append buf, [...]
+]
+
+scenario stash-empty-duplex-list [
+  local-scope
+  x:&:duplex-list:num <- copy null
+  run [
+    stash x
+  ]
+  trace-should-contain [
+    app: []
+  ]
+]
diff --git a/archive/2.vm/066stream.mu b/archive/2.vm/066stream.mu
new file mode 100644
index 00000000..b3202f65
--- /dev/null
+++ b/archive/2.vm/066stream.mu
@@ -0,0 +1,80 @@
+# new type to help incrementally scan arrays
+container stream:_elem [
+  index:num
+  data:&:@:_elem
+]
+
+def new-stream s:&:@:_elem -> result:&:stream:_elem [
+  local-scope
+  load-inputs
+  return-unless s, null
+  result <- new {(stream _elem): type}
+  *result <- put *result, index:offset, 0
+  *result <- put *result, data:offset, s
+]
+
+def rewind in:&:stream:_elem -> in:&:stream:_elem [
+  local-scope
+  load-inputs
+  return-unless in
+  *in <- put *in, index:offset, 0
+]
+
+def read in:&:stream:_elem -> result:_elem, empty?:bool, in:&:stream:_elem [
+  local-scope
+  load-inputs
+  assert in, [cannot read; stream has no data]
+  empty? <- copy false
+  idx:num <- get *in, index:offset
+  s:&:@:_elem <- get *in, data:offset
+  len:num <- length *s
+  at-end?:bool <- greater-or-equal idx len
+  {
+    break-unless at-end?
+    empty-result:&:_elem <- new _elem:type
+    return *empty-result, true
+  }
+  result <- index *s, idx
+  idx <- add idx, 1
+  *in <- put *in, index:offset, idx
+]
+
+def peek in:&:stream:_elem -> result:_elem, empty?:bool [
+  local-scope
+  load-inputs
+  assert in, [cannot peek; stream has no data]
+  empty?:bool <- copy false
+  idx:num <- get *in, index:offset
+  s:&:@:_elem <- get *in, data:offset
+  len:num <- length *s
+  at-end?:bool <- greater-or-equal idx len
+  {
+    break-unless at-end?
+    empty-result:&:_elem <- new _elem:type
+    return *empty-result, true
+  }
+  result <- index *s, idx
+]
+
+def read-line in:&:stream:char -> result:text, in:&:stream:char [
+  local-scope
+  load-inputs
+  assert in, [cannot read-line; stream has no data]
+  idx:num <- get *in, index:offset
+  s:text <- get *in, data:offset
+  next-idx:num <- find-next s, 10/newline, idx
+  result <- copy-range s, idx, next-idx
+  idx <- add next-idx, 1  # skip newline
+  # write back
+  *in <- put *in, index:offset, idx
+]
+
+def end-of-stream? in:&:stream:_elem -> result:bool [
+  local-scope
+  load-inputs
+  assert in, [cannot check end-of-stream?; stream has no data]
+  idx:num <- get *in, index:offset
+  s:&:@:_elem <- get *in, data:offset
+  len:num <- length *s
+  result <- greater-or-equal idx, len
+]
diff --git a/archive/2.vm/067random.cc b/archive/2.vm/067random.cc
new file mode 100644
index 00000000..d80adb4d
--- /dev/null
+++ b/archive/2.vm/067random.cc
@@ -0,0 +1,34 @@
+:(before "End Primitive Recipe Declarations")
+REAL_RANDOM,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "real-random", REAL_RANDOM);
+:(before "End Primitive Recipe Checks")
+case REAL_RANDOM: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case REAL_RANDOM: {
+  // todo: limited range of numbers, might be imperfectly random
+  // todo: thread state in extra ingredients and products
+  products.resize(1);
+  products.at(0).push_back(rand());
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+MAKE_RANDOM_NONDETERMINISTIC,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "make-random-nondeterministic", MAKE_RANDOM_NONDETERMINISTIC);
+:(before "End Primitive Recipe Checks")
+case MAKE_RANDOM_NONDETERMINISTIC: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MAKE_RANDOM_NONDETERMINISTIC: {
+  srand(time(NULL));
+  break;
+}
+
+// undo non-determinism in later tests
+:(before "End Reset")
+srand(0);
diff --git a/archive/2.vm/068random.mu b/archive/2.vm/068random.mu
new file mode 100644
index 00000000..3d73356c
--- /dev/null
+++ b/archive/2.vm/068random.mu
@@ -0,0 +1,75 @@
+def random generator:&:stream:num -> result:num, fail?:bool, generator:&:stream:num [
+  local-scope
+  load-inputs
+  {
+    break-if generator
+    # generator is 0? use real random-number generator
+    result <- real-random
+    return result, false
+  }
+  result, fail?, generator <- read generator
+]
+
+# helper for tests
+def assume-random-numbers -> result:&:stream:num [
+  local-scope
+  load-inputs
+  # compute result-len, space to allocate in result
+  result-len:num <- copy 0
+  {
+    _, arg-received?:bool <- next-input
+    break-unless arg-received?
+    result-len <- add result-len, 1
+    loop
+  }
+  rewind-inputs
+  result-data:&:@:num <- new number:type, result-len
+  idx:num <- copy 0
+  {
+    curr:num, arg-received?:bool <- next-input
+    break-unless arg-received?
+    *result-data <- put-index *result-data, idx, curr
+    idx <- add idx, 1
+    loop
+  }
+  result <- new-stream result-data
+]
+
+scenario random-numbers-in-scenario [
+  local-scope
+  source:&:stream:num <- assume-random-numbers 34, 35, 37
+  1:num/raw, 2:bool/raw <- random source
+  3:num/raw, 4:bool/raw <- random source
+  5:num/raw, 6:bool/raw <- random source
+  7:num/raw, 8:bool/raw <- random source
+  memory-should-contain [
+    1 <- 34
+    2 <- 0  # everything went well
+    3 <- 35
+    4 <- 0  # everything went well
+    5 <- 37
+    6 <- 0  # everything went well
+    7 <- 0  # empty result
+    8 <- 1  # end of stream
+  ]
+]
+
+# generate a random integer in the semi-open interval [start, end)
+def random-in-range generator:&:stream:num, start:num, end:num -> result:num, fail?:bool, generator:&:stream:num [
+  local-scope
+  load-inputs
+  result, fail?, generator <- random generator
+  return-if fail?
+  delta:num <- subtract end, start
+  _, result <- divide-with-remainder result, delta
+  result <- add result, start
+]
+
+scenario random-in-range [
+  local-scope
+  source:&:stream:num <- assume-random-numbers 91
+  1:num/raw <- random-in-range source, 40, 50
+  memory-should-contain [
+    1 <- 41
+  ]
+]
diff --git a/archive/2.vm/069hash.cc b/archive/2.vm/069hash.cc
new file mode 100644
index 00000000..67897d00
--- /dev/null
+++ b/archive/2.vm/069hash.cc
@@ -0,0 +1,422 @@
+// Compute a hash for objects of any type.
+//
+// The way it's currently implemented, two objects will have the same hash if
+// all their non-address fields (all the way down) expand to the same sequence
+// of scalar values. In particular, a container with all zero addresses hashes
+// to 0. Hopefully this won't be an issue because we are usually hashing
+// objects of a single type in any given hash table.
+//
+// Based on http://burtleburtle.net/bob/hash/hashfaq.html
+
+:(before "End Primitive Recipe Declarations")
+HASH,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "hash", HASH);
+:(before "End Primitive Recipe Checks")
+case HASH: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'hash' takes exactly one ingredient rather than '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case HASH: {
+  const reagent& input = current_instruction().ingredients.at(0);
+  products.resize(1);
+  products.at(0).push_back(hash(0, input));
+  break;
+}
+
+//: in all the code below, the intermediate results of hashing are threaded through 'h'
+
+:(code)
+size_t hash(size_t h, reagent/*copy*/ r) {
+  canonize(r);
+  if (is_mu_text(r))  // optimization
+    return hash_mu_text(h, r);
+  else if (is_mu_address(r))
+    return hash_mu_address(h, r);
+  else if (is_mu_scalar(r))
+    return hash_mu_scalar(h, r);
+  else if (is_mu_array(r))
+    return hash_mu_array(h, r);
+  else if (is_mu_container(r))
+    return hash_mu_container(h, r);
+  else if (is_mu_exclusive_container(r))
+    return hash_mu_exclusive_container(h, r);
+  assert(false);
+}
+
+size_t hash_mu_scalar(size_t h, const reagent& r) {
+  double input = is_literal(r) ? r.value : get_or_insert(Memory, r.value);
+  return hash_iter(h, static_cast<size_t>(input));
+}
+
+size_t hash_mu_address(size_t h, reagent& r) {
+  if (r.value == 0) return 0;
+  trace(Callstack_depth+1, "mem") << "location " << r.value << " is " << no_scientific(get_or_insert(Memory, r.value)) << end();
+  r.set_value(get_or_insert(Memory, r.value));
+  drop_from_type(r, "address");
+  return hash(h, r);
+}
+
+size_t hash_mu_text(size_t h, const reagent& r) {
+  string input = read_mu_text(get_or_insert(Memory, r.value+/*skip alloc id*/1));
+  for (int i = 0;  i < SIZE(input);  ++i) {
+    h = hash_iter(h, static_cast<size_t>(input.at(i)));
+//?     cerr << i << ": " << h << '\n';
+  }
+  return h;
+}
+
+size_t hash_mu_array(size_t h, const reagent& r) {
+  int size = get_or_insert(Memory, r.value);
+  reagent/*copy*/ elem = r;
+  delete elem.type;
+  elem.type = copy_array_element(r.type);
+  for (int i=0, address = r.value+1;  i < size;  ++i, address += size_of(elem)) {
+    reagent/*copy*/ tmp = elem;
+    tmp.set_value(address);
+    h = hash(h, tmp);
+//?     cerr << i << " (" << address << "): " << h << '\n';
+  }
+  return h;
+}
+
+size_t hash_mu_container(size_t h, const reagent& r) {
+  type_info& info = get(Type, get_base_type(r.type)->value);
+  int address = r.value;
+  int offset = 0;
+  for (int i = 0;  i < SIZE(info.elements);  ++i) {
+    reagent/*copy*/ element = element_type(r.type, i);
+    if (has_property(element, "ignore-for-hash")) continue;
+    element.set_value(address+offset);
+    h = hash(h, element);
+//?     cerr << i << ": " << h << '\n';
+    offset += size_of(info.elements.at(i).type);
+  }
+  return h;
+}
+
+size_t hash_mu_exclusive_container(size_t h, const reagent& r) {
+  const type_tree* type = get_base_type(r.type);
+  assert(type->value);
+  int tag = get(Memory, r.value);
+  reagent/*copy*/ variant = variant_type(r, tag);
+  // todo: move this error to container definition time
+  if (has_property(variant, "ignore-for-hash"))
+    raise << get(Type, type->value).name << ": /ignore-for-hash won't work in exclusive containers\n" << end();
+  variant.set_value(r.value + /*skip tag*/1);
+  h = hash(h, variant);
+  return h;
+}
+
+size_t hash_iter(size_t h, size_t input) {
+  h += input;
+  h += (h<<10);
+  h ^= (h>>6);
+
+  h += (h<<3);
+  h ^= (h>>11);
+  h += (h<<15);
+  return h;
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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.
+
+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.
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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.
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "hash_old", HASH_OLD);
+:(before "End Primitive Recipe Checks")
+case HASH_OLD: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'hash_old' takes exactly one ingredient rather than '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_text(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'hash_old' currently only supports texts (address array character), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case HASH_OLD: {
+  string input = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
+  size_t h = 0 ;
+
+  for (int i = 0;  i < SIZE(input);  ++i) {
+    h += static_cast<size_t>(input.at(i));
+    h += (h<<10);
+    h ^= (h>>6);
+
+    h += (h<<3);
+    h ^= (h>>11);
+    h += (h<<15);
+  }
+
+  products.resize(1);
+  products.at(0).push_back(h);
+  break;
+}
diff --git a/archive/2.vm/070table.mu b/archive/2.vm/070table.mu
new file mode 100644
index 00000000..21184084
--- /dev/null
+++ b/archive/2.vm/070table.mu
@@ -0,0 +1,109 @@
+# A table is like an array, except that you can index it with arbitrary types
+# and not just non-negative whole numbers.
+
+# incomplete; doesn't handle hash conflicts
+
+scenario table-read-write [
+  local-scope
+  tab:&:table:num:num <- new-table 30
+  run [
+    put-index tab, 12, 34
+    60:num/raw, 61:bool/raw <- index tab, 12
+  ]
+  memory-should-contain [
+    60 <- 34
+    61 <- 1  # found
+  ]
+]
+
+scenario table-read-write-non-integer [
+  local-scope
+  tab:&:table:text:num <- new-table 30
+  run [
+    put-index tab, [abc def], 34
+    1:num/raw, 2:bool/raw <- index tab, [abc def]
+  ]
+  memory-should-contain [
+    1 <- 34
+    2 <- 1  # found
+  ]
+]
+
+scenario table-read-not-found [
+  local-scope
+  tab:&:table:text:num <- new-table 30
+  run [
+    1:num/raw, 2:bool/raw <- index tab, [abc def]
+  ]
+  memory-should-contain [
+    1 <- 0
+    2 <- 0  # not found
+  ]
+]
+
+container table:_key:_value [
+  length:num
+  capacity:num
+  data:&:@:table-row:_key:_value
+]
+
+container table-row:_key:_value [
+  occupied?:bool
+  key:_key
+  value:_value
+]
+
+def new-table capacity:num -> result:&:table:_key:_value [
+  local-scope
+  load-inputs
+  result <- new {(table _key _value): type}
+  data:&:@:table-row:_key:_value <- new {(table-row _key _value): type}, capacity
+  *result <- merge 0/length, capacity, data
+]
+
+# todo: tag results as /required so that call-sites are forbidden from ignoring them
+# then we could handle conflicts simply by resizing the table
+def put-index table:&:table:_key:_value, key:_key, value:_value -> table:&:table:_key:_value [
+  local-scope
+  load-inputs
+  hash:num <- hash key
+  hash <- abs hash
+  capacity:num <- get *table, capacity:offset
+  _, hash-key:num <- divide-with-remainder hash, capacity
+  hash-key <- abs hash-key  # in case hash overflows from a double into a negative integer inside 'divide-with-remainder' above
+  table-data:&:@:table-row:_key:_value <- get *table, data:offset
+  x:table-row:_key:_value <- index *table-data, hash-key
+  occupied?:bool <- get x, occupied?:offset
+  not-occupied?:bool <- not occupied?:bool
+  assert not-occupied?, [can't handle collisions yet]
+  new-row:table-row:_key:_value <- merge true, key, value
+  *table-data <- put-index *table-data, hash-key, new-row
+]
+
+def index table:&:table:_key:_value, key:_key -> result:_value, found?:bool [
+  local-scope
+  load-inputs
+  hash:num <- hash key
+  hash <- abs hash
+  capacity:num <- get *table, capacity:offset
+  _, hash-key:num <- divide-with-remainder hash, capacity
+  hash-key <- abs hash-key  # in case hash overflows from a double into a negative integer inside 'divide-with-remainder' above
+  table-data:&:@:table-row:_key:_value <- get *table, data:offset
+  x:table-row:_key:_value <- index *table-data, hash-key
+  empty:&:_value <- new _value:type
+  result <- copy *empty
+  found?:bool <- get x, occupied?:offset
+  return-unless found?
+  key2:_key <- get x, key:offset
+  found?:bool <- equal key, key2
+  return-unless found?
+  result <- get x, value:offset
+]
+
+def abs n:num -> result:num [
+  local-scope
+  load-inputs
+  positive?:bool <- greater-or-equal n, 0
+  return-if positive?, n
+  result <- multiply n, -1
+]
diff --git a/archive/2.vm/072recipe.cc b/archive/2.vm/072recipe.cc
new file mode 100644
index 00000000..a3bb00bf
--- /dev/null
+++ b/archive/2.vm/072recipe.cc
@@ -0,0 +1,711 @@
+//: So far we've been calling a fixed recipe in each instruction, but we'd
+//: also like to make the recipe a variable, pass recipes to "higher-order"
+//: recipes, return recipes from recipes and so on.
+
+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);
+// 'recipe' variables can store recipe-literal
+type_ordinal recipe = put(Type_ordinal, "recipe", Next_type_ordinal++);
+get_or_insert(Type, recipe).name = "recipe";
+
+:(after "Deduce Missing Type(x, caller)")
+if (!x.type)
+  try_initialize_recipe_literal(x, caller);
+:(before "Type Check in Type-ingredient-aware check_or_set_types_by_name")
+if (!x.type)
+  try_initialize_recipe_literal(x, variant);
+:(code)
+void try_initialize_recipe_literal(reagent& x, const recipe& caller) {
+  if (x.type) return;
+  if (!contains_key(Recipe_ordinal, x.name)) return;
+  if (contains_reagent_with_non_recipe_literal_type(caller, x.name)) return;
+  x.type = new type_tree("recipe-literal");
+  x.set_value(get(Recipe_ordinal, x.name));
+}
+bool contains_reagent_with_non_recipe_literal_type(const recipe& caller, const string& name) {
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    const instruction& inst = caller.steps.at(i);
+    for (int i = 0;  i < SIZE(inst.ingredients);  ++i)
+      if (is_matching_non_recipe_literal(inst.ingredients.at(i), name)) return true;
+    for (int i = 0;  i < SIZE(inst.products);  ++i)
+      if (is_matching_non_recipe_literal(inst.products.at(i), name)) return true;
+  }
+  return false;
+}
+bool is_matching_non_recipe_literal(const reagent& x, const string& name) {
+  if (x.name != name) return false;
+  if (!x.type) return false;
+  return !x.type->atom || x.type->name != "recipe-literal";
+}
+
+//: It's confusing to use variable names that are also recipe names. Always
+//: assume variable types override recipe literals.
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "call", CALL);
+:(before "End Primitive Recipe Checks")
+case CALL: {
+  if (inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'call' requires at least one ingredient (the recipe to call)\n" << end();
+    break;
+  }
+  if (!is_mu_recipe(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'call' should be a recipe, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CALL: {
+  // Begin Call
+  trace(Callstack_depth+1, "trace") << "indirect 'call': incrementing callstack depth to " << Callstack_depth << end();
+  ++Callstack_depth;
+  assert(Callstack_depth < Max_depth);
+  if (!ingredients.at(0).at(0)) {
+    raise << maybe(current_recipe_name()) << "tried to call empty recipe in '" << to_string(current_instruction()) << "'" << end();
+    break;
+  }
+  const call& caller_frame = current_call();
+  instruction/*copy*/ call_instruction = to_instruction(caller_frame);
+  call_instruction.operation = ingredients.at(0).at(0);
+  call_instruction.ingredients.erase(call_instruction.ingredients.begin());
+  Current_routine->calls.push_front(call(ingredients.at(0).at(0)));
+  ingredients.erase(ingredients.begin());  // drop the callee
+  finish_call_housekeeping(call_instruction, ingredients);
+  // not done with caller
+  write_products = false;
+  fall_through_to_next_instruction = false;
+  break;
+}
+
+:(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
+
+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))) {
+  resolve_indirect_ambiguous_call(r, index, inst, caller_recipe);
+  return;
+}
+:(code)
+bool is_recipe_literal(const reagent& x) {
+  return x.type && x.type->atom && x.type->name == "recipe-literal";
+}
+void resolve_indirect_ambiguous_call(const recipe_ordinal r, int index, instruction& inst, const recipe& caller_recipe) {
+  instruction inst2;
+  inst2.name = inst.ingredients.at(0).name;
+  for (int i = /*skip recipe*/1;  i < SIZE(inst.ingredients);  ++i)
+    inst2.ingredients.push_back(inst.ingredients.at(i));
+  for (int i = 0;  i < SIZE(inst.products);  ++i)
+    inst2.products.push_back(inst.products.at(i));
+  resolve_ambiguous_call(r, index, inst2, caller_recipe);
+  inst.ingredients.at(0).name = inst2.name;
+  inst.ingredients.at(0).set_value(get(Recipe_ordinal, inst2.name));
+}
+
+:(after "Transform.push_back(check_instruction)")
+Transform.push_back(check_indirect_calls_against_header);  // idempotent
+:(code)
+void check_indirect_calls_against_header(const recipe_ordinal r) {
+  trace(101, "transform") << "--- type-check 'call' instructions inside recipe " << get(Recipe, r).name << end();
+  const recipe& caller = get(Recipe, r);
+  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
+    const instruction& inst = caller.steps.at(i);
+    if (!is_indirect_call(inst.operation)) continue;
+    if (inst.ingredients.empty()) continue;  // error raised above
+    const reagent& callee = inst.ingredients.at(0);
+    if (!is_mu_recipe(callee)) continue;  // error raised above
+    const recipe callee_header = is_literal(callee) ? get(Recipe, callee.value) : from_reagent(inst.ingredients.at(0));
+    if (!callee_header.has_header) continue;
+    if (is_indirect_call_with_ingredients(inst.operation)) {
+      for (long int i = /*skip callee*/1;  i < min(SIZE(inst.ingredients), SIZE(callee_header.ingredients)+/*skip callee*/1);  ++i) {
+        if (!types_coercible(callee_header.ingredients.at(i-/*skip callee*/1), inst.ingredients.at(i)))
+          raise << maybe(caller.name) << "ingredient " << i-/*skip callee*/1 << " has the wrong type at '" << to_original_string(inst) << "'\n" << end();
+      }
+    }
+    if (is_indirect_call_with_products(inst.operation)) {
+      for (long int i = 0;  i < min(SIZE(inst.products), SIZE(callee_header.products));  ++i) {
+        if (is_dummy(inst.products.at(i))) continue;
+        if (!types_coercible(callee_header.products.at(i), inst.products.at(i)))
+          raise << maybe(caller.name) << "product " << i << " has the wrong type at '" << to_original_string(inst) << "'\n" << end();
+      }
+    }
+  }
+}
+
+bool is_indirect_call(const recipe_ordinal r) {
+  return is_indirect_call_with_ingredients(r) || is_indirect_call_with_products(r);
+}
+
+bool is_indirect_call_with_ingredients(const recipe_ordinal r) {
+  if (r == CALL) return true;
+  // End is_indirect_call_with_ingredients Special-cases
+  return false;
+}
+bool is_indirect_call_with_products(const recipe_ordinal r) {
+  if (r == CALL) return true;
+  // End is_indirect_call_with_products Special-cases
+  return false;
+}
+
+recipe from_reagent(const reagent& r) {
+  assert(r.type);
+  recipe result_header;  // will contain only ingredients and products, nothing else
+  result_header.has_header = true;
+  // Begin Reagent->Recipe(r, recipe_header)
+  if (r.type->atom) {
+    assert(r.type->name == "recipe");
+    return result_header;
+  }
+  const type_tree* root_type = r.type->atom ? r.type : r.type->left;
+  assert(root_type->atom);
+  assert(root_type->name == "recipe");
+  const type_tree* curr = r.type->right;
+  for (/*nada*/;  curr && !curr->atom;  curr = curr->right) {
+    if (curr->left->atom && curr->left->name == "->") {
+      curr = curr->right;  // skip delimiter
+      goto read_products;
+    }
+    result_header.ingredients.push_back(next_recipe_reagent(curr->left));
+  }
+  if (curr) {
+    assert(curr->atom);
+    result_header.ingredients.push_back(next_recipe_reagent(curr));
+    return result_header;  // no products
+  }
+  read_products:
+  for (/*nada*/;  curr && !curr->atom;  curr = curr->right)
+    result_header.products.push_back(next_recipe_reagent(curr->left));
+  if (curr) {
+    assert(curr->atom);
+    result_header.products.push_back(next_recipe_reagent(curr));
+  }
+  return result_header;
+}
+
+:(before "End Unit Tests")
+void test_from_reagent_atomic() {
+  reagent a("{f: recipe}");
+  recipe r_header = from_reagent(a);
+  CHECK(r_header.ingredients.empty());
+  CHECK(r_header.products.empty());
+}
+void test_from_reagent_non_atomic() {
+  reagent a("{f: (recipe number -> number)}");
+  recipe r_header = from_reagent(a);
+  CHECK_EQ(SIZE(r_header.ingredients), 1);
+  CHECK_EQ(SIZE(r_header.products), 1);
+}
+void test_from_reagent_reads_ingredient_at_end() {
+  reagent a("{f: (recipe number number)}");
+  recipe r_header = from_reagent(a);
+  CHECK_EQ(SIZE(r_header.ingredients), 2);
+  CHECK(r_header.products.empty());
+}
+void test_from_reagent_reads_sole_ingredient_at_end() {
+  reagent a("{f: (recipe number)}");
+  recipe r_header = from_reagent(a);
+  CHECK_EQ(SIZE(r_header.ingredients), 1);
+  CHECK(r_header.products.empty());
+}
+
+:(code)
+reagent next_recipe_reagent(const type_tree* curr) {
+  if (!curr->left) return reagent("recipe:"+curr->name);
+  return reagent(new type_tree(*curr));
+}
+
+bool is_mu_recipe(const reagent& r) {
+  if (!r.type) return false;
+  if (r.type->atom) {
+    // End is_mu_recipe Atom Cases(r)
+    return r.type->name == "recipe-literal";
+  }
+  return r.type->left->atom && r.type->left->name == "recipe";
+}
+
+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)) {
+  if (!contains_key(Recipe, from.value)) {
+    raise << "trying to store recipe " << from.name << " into " << to_string(to) << " but there's no such recipe\n" << end();
+    return false;
+  }
+  const recipe& rrhs = get(Recipe, from.value);
+  const recipe& rlhs = from_reagent(to);
+  for (long int i = 0;  i < min(SIZE(rlhs.ingredients), SIZE(rrhs.ingredients));  ++i) {
+    if (!types_match(rlhs.ingredients.at(i), rrhs.ingredients.at(i)))
+      return false;
+  }
+  for (long int i = 0;  i < min(SIZE(rlhs.products), SIZE(rrhs.products));  ++i) {
+    if (!types_match(rlhs.products.at(i), rrhs.products.at(i)))
+      return false;
+  }
+  return true;
+}
+
+:(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
+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));
+:(before "End JUMP_UNLESS Checks")
+check_for_recipe_literals(inst, get(Recipe, r));
+:(code)
+void check_for_recipe_literals(const instruction& inst, const recipe& caller) {
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (is_mu_recipe(inst.ingredients.at(i))) {
+      raise << maybe(caller.name) << "missing type for '" << inst.ingredients.at(i).original_string << "' in '" << to_original_string(inst) << "'\n" << end();
+      if (is_present_in_ingredients(caller, inst.ingredients.at(i).name))
+        raise << "  did you forget 'load-ingredients'?\n" << end();
+    }
+  }
+}
+
+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"));
+put(Type_abbreviations, "fn", new_type_tree("recipe"));
+
+//: copying functions to variables
+
+:(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") {
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_recipe_literal(inst.ingredients.at(i))) continue;
+    if (non_ghost_size(get_or_insert(Recipe_variants, inst.ingredients.at(i).name)) < 1) continue;
+    // potentially overloaded recipe
+    string new_name = resolve_ambiguous_call(inst.ingredients.at(i).name, inst.products.at(i), r, index, caller_recipe);
+    if (new_name == "") continue;
+    inst.ingredients.at(i).name = new_name;
+    inst.ingredients.at(i).value = get(Recipe_ordinal, new_name);
+  }
+  return;
+}
+:(code)
+string resolve_ambiguous_call(const string& recipe_name, const reagent& call_types, const recipe_ordinal r, int index, const recipe& caller_recipe) {
+  instruction inst;
+  inst.name = recipe_name;
+  if (!is_mu_recipe(call_types)) return "";  // error raised elsewhere
+  if (is_recipe_literal(call_types)) return "";  // error raised elsewhere
+  construct_fake_call(call_types, inst);
+  resolve_ambiguous_call(r, index, inst, caller_recipe);
+  return inst.name;
+}
+void construct_fake_call(const reagent& recipe_var, instruction& out) {
+  assert(recipe_var.type->left->name == "recipe");
+  type_tree* stem = NULL;
+  for (stem = recipe_var.type->right;  stem && stem->left->name != "->";  stem = stem->right)
+    out.ingredients.push_back(copy(stem->left));
+  if (stem == NULL) return;
+  for (/*skip '->'*/stem = stem->right;  stem;  stem = stem->right)
+    out.products.push_back(copy(stem->left));
+}
+
+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
+
+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) {
+  if (!is_mu_recipe(inst.ingredients.at(i))) continue;
+  if (non_ghost_size(get_or_insert(Recipe_variants, inst.ingredients.at(i).name)) < 1) continue;
+  if (get(Recipe_ordinal, inst.name) < MAX_PRIMITIVE_RECIPES) continue;
+  if (non_ghost_size(get_or_insert(Recipe_variants, inst.name)) > 1) {
+    raise << maybe(caller_recipe.name) << "sorry, we're not yet smart enough to simultaneously guess which overloads you want for '" << inst.name << "' and '" << inst.ingredients.at(i).name << "'\n" << end();
+    return;
+  }
+  const recipe& callee = get(Recipe, get(Recipe_ordinal, inst.name));
+  if (!callee.has_header) {
+    raise << maybe(caller_recipe.name) << "sorry, we're not yet smart enough to guess which variant of '" << inst.ingredients.at(i).name << "' you want, when the caller '" << inst.name << "' doesn't have a header\n" << end();
+    return;
+  }
+  string new_name = resolve_ambiguous_call(inst.ingredients.at(i).name, callee.ingredients.at(i), r, index, caller_recipe);
+  if (new_name != "") {
+    inst.ingredients.at(i).name = new_name;
+    inst.ingredients.at(i).value = get(Recipe_ordinal, new_name);
+  }
+}
+
+:(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") {
+  for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+    if (!is_recipe_literal(inst.ingredients.at(i))) continue;
+    if (non_ghost_size(get_or_insert(Recipe_variants, inst.ingredients.at(i).name)) < 1) continue;
+    // potentially overloaded recipe
+    if (!caller_recipe.has_header) {
+      raise << maybe(caller_recipe.name) << "sorry, we're not yet smart enough to guess which variant of '" << inst.ingredients.at(i).name << "' you want, without a recipe header\n" << end();
+      return;
+    }
+    string new_name = resolve_ambiguous_call(inst.ingredients.at(i).name, caller_recipe.products.at(i), r, index, caller_recipe);
+    if (new_name == "") continue;
+    inst.ingredients.at(i).name = new_name;
+    inst.ingredients.at(i).value = get(Recipe_ordinal, new_name);
+  }
+  return;
+}
diff --git a/archive/2.vm/073scheduler.cc b/archive/2.vm/073scheduler.cc
new file mode 100644
index 00000000..5f97220b
--- /dev/null
+++ b/archive/2.vm/073scheduler.cc
@@ -0,0 +1,709 @@
+//: Run a second routine concurrently using 'start-running', without any
+//: guarantees on how the operations in each are interleaved with each other.
+
+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")
+int Scheduling_interval = 500;
+:(before "End routine Fields")
+int instructions_run_this_scheduling_slice;
+:(before "End routine Constructor")
+instructions_run_this_scheduling_slice = 0;
+:(after "Running One Instruction")
+ ++Current_routine->instructions_run_this_scheduling_slice;
+:(replace{} "bool should_continue_running(const routine* current_routine)")
+bool should_continue_running(const routine* current_routine) {
+  assert(current_routine == Current_routine);  // argument passed in just to make caller readable above
+  return Current_routine->state == RUNNING
+      && Current_routine->instructions_run_this_scheduling_slice < Scheduling_interval;
+}
+:(after "stop_running_current_routine:")
+// Reset instructions_run_this_scheduling_slice
+Current_routine->instructions_run_this_scheduling_slice = 0;
+
+//: now the rest of the scheduler is clean
+
+:(before "struct routine")
+enum routine_state {
+  RUNNING,
+  COMPLETED,
+  // End routine States
+};
+:(before "End routine Fields")
+enum routine_state state;
+:(before "End routine Constructor")
+state = RUNNING;
+
+:(before "End Globals")
+vector<routine*> Routines;
+int Current_routine_index = 0;
+:(before "End Reset")
+Scheduling_interval = 500;
+for (int i = 0;  i < SIZE(Routines);  ++i)
+  delete Routines.at(i);
+Routines.clear();
+Current_routine = NULL;
+:(replace{} "void run(const recipe_ordinal r)")
+void run(const recipe_ordinal r) {
+  run(new routine(r));
+}
+
+:(code)
+void run(routine* rr) {
+  Routines.push_back(rr);
+  Current_routine_index = 0, Current_routine = Routines.at(0);
+  while (!all_routines_done()) {
+    skip_to_next_routine();
+    assert(Current_routine);
+    assert(Current_routine->state == RUNNING);
+    trace(100, "schedule") << current_routine_label() << end();
+    run_current_routine();
+    // Scheduler State Transitions
+    if (Current_routine->completed())
+      Current_routine->state = COMPLETED;
+    // End Scheduler State Transitions
+
+    // Scheduler Cleanup
+    // End Scheduler Cleanup
+  }
+  // End Run Routine
+}
+
+bool all_routines_done() {
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->state == RUNNING)
+      return false;
+  }
+  return true;
+}
+
+// skip Current_routine_index past non-RUNNING routines
+void skip_to_next_routine() {
+  assert(!Routines.empty());
+  assert(Current_routine_index < SIZE(Routines));
+  for (int i = (Current_routine_index+1)%SIZE(Routines);  i != Current_routine_index;  i = (i+1)%SIZE(Routines)) {
+    if (Routines.at(i)->state == RUNNING) {
+      Current_routine_index = i;
+      Current_routine = Routines.at(i);
+      return;
+    }
+  }
+}
+
+string current_routine_label() {
+  return routine_label(Current_routine);
+}
+
+string routine_label(routine* r) {
+  ostringstream result;
+  const call_stack& calls = r->calls;
+  for (call_stack::const_iterator p = calls.begin();  p != calls.end();  ++p) {
+    if (p != calls.begin()) result << '/';
+    result << get(Recipe, p->running_recipe).name;
+  }
+  return result.str();
+}
+
+//: special case for the very first routine
+:(replace{} "void run_main(int argc, char* argv[])")
+void run_main(int argc, char* argv[]) {
+  recipe_ordinal r = get(Recipe_ordinal, "main");
+  assert(r);
+  routine* main_routine = new routine(r);
+  // pass in commandline args as ingredients to main
+  // todo: test this
+  Current_routine = main_routine;
+  for (int i = 1;  i < argc;  ++i) {
+    vector<double> arg;
+    arg.push_back(/*alloc id*/0);
+    arg.push_back(new_mu_text(argv[i]));
+    assert(get(Memory, arg.back()) == 0);
+    current_call().ingredient_atoms.push_back(arg);
+  }
+  run(main_routine);
+}
+
+//:: To schedule new routines to run, call 'start-running'.
+
+//: 'start-running' will return a unique id for the routine that was created.
+//: routine id is a number, but don't do any arithmetic on it
+:(before "End routine Fields")
+int id;
+:(before "End Globals")
+int Next_routine_id = 1;
+:(before "End Reset")
+Next_routine_id = 1;
+:(before "End routine Constructor")
+id = Next_routine_id;
+++Next_routine_id;
+
+//: routines save the routine that spawned them
+:(before "End routine Fields")
+// todo: really should be routine_id, but that's less efficient.
+int parent_index;  // only < 0 if there's no parent_index
+:(before "End routine Constructor")
+parent_index = -1;
+
+:(before "End Primitive Recipe Declarations")
+START_RUNNING,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "start-running", START_RUNNING);
+:(before "End Primitive Recipe Checks")
+case START_RUNNING: {
+  if (inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'start-running' requires at least one ingredient: the recipe to start running\n" << end();
+    break;
+  }
+  if (!is_mu_recipe(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'start-running' should be a recipe, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case START_RUNNING: {
+  routine* new_routine = new routine(ingredients.at(0).at(0));
+  new_routine->parent_index = Current_routine_index;
+  // populate ingredients
+  for (int i = /*skip callee*/1;  i < SIZE(current_instruction().ingredients);  ++i) {
+    new_routine->calls.front().ingredient_atoms.push_back(ingredients.at(i));
+    reagent/*copy*/ ingredient = current_instruction().ingredients.at(i);
+    new_routine->calls.front().ingredients.push_back(ingredient);
+    // End Populate start-running Ingredient
+  }
+  Routines.push_back(new_routine);
+  products.resize(1);
+  products.at(0).push_back(new_routine->id);
+  break;
+}
+
+:(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'
+
+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")
+if (r == START_RUNNING) return true;
+
+//: back to testing 'start-running'
+
+:(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.
+
+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) {
+    Current_routine->state = COMPLETED;
+  }
+
+//:: Routines are marked completed when their parent completes.
+
+:(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) {
+  if (Routines.at(i)->state == COMPLETED) continue;
+  if (Routines.at(i)->parent_index < 0) continue;  // root thread
+  // structured concurrency: http://250bpm.com/blog:71
+  if (has_completed_parent(i)) {
+    Routines.at(i)->state = COMPLETED;
+  }
+}
+
+:(code)
+bool has_completed_parent(int routine_index) {
+  for (int j = routine_index;  j >= 0;  j = Routines.at(j)->parent_index) {
+    if (Routines.at(j)->state == COMPLETED)
+      return true;
+  }
+  return false;
+}
+
+//:: 'routine-state' can tell if a given routine id is running
+
+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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "routine-state", ROUTINE_STATE);
+:(before "End Primitive Recipe Checks")
+case ROUTINE_STATE: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'routine-state' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'routine-state' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case ROUTINE_STATE: {
+  int id = ingredients.at(0).at(0);
+  int result = -1;
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->id == id) {
+      result = Routines.at(i)->state;
+      break;
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+//:: miscellaneous helpers
+
+:(before "End Primitive Recipe Declarations")
+STOP,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "stop", STOP);
+:(before "End Primitive Recipe Checks")
+case STOP: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'stop' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'stop' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case STOP: {
+  int id = ingredients.at(0).at(0);
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->id == id) {
+      Routines.at(i)->state = COMPLETED;
+      break;
+    }
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_DUMP_ROUTINES,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$dump-routines", _DUMP_ROUTINES);
+:(before "End Primitive Recipe Checks")
+case _DUMP_ROUTINES: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _DUMP_ROUTINES: {
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    cerr << i << ": " << Routines.at(i)->id << ' ' << Routines.at(i)->state << ' ' << Routines.at(i)->parent_index << '\n';
+  }
+  break;
+}
+
+//: support for stopping routines after some number of cycles
+
+:(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,
+:(before "End Scheduler State Transitions")
+if (Current_routine->limit >= 0) {
+  if (Current_routine->limit <= Scheduling_interval) {
+    trace(100, "schedule") << "discontinuing routine " << Current_routine->id << end();
+    Current_routine->state = DISCONTINUED;
+    Current_routine->limit = 0;
+  }
+  else {
+    Current_routine->limit -= Scheduling_interval;
+  }
+}
+
+:(before "End Test Teardown")
+if (Passed && any_routines_with_error())
+  raise << "some routines died with errors\n" << end();
+:(before "End Mu Test Teardown")
+if (Passed && any_routines_with_error())
+  raise << Current_scenario->name << ": some routines died with errors\n" << end();
+
+:(code)
+bool any_routines_with_error() {
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->state == DISCONTINUED)
+      return true;
+  }
+  return false;
+}
+
+:(before "End routine Fields")
+int limit;
+:(before "End routine Constructor")
+limit = -1;  /* no limit */
+
+:(before "End Primitive Recipe Declarations")
+LIMIT_TIME,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "limit-time", LIMIT_TIME);
+:(before "End Primitive Recipe Checks")
+case LIMIT_TIME: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'limit-time' requires exactly two ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'limit-time' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'limit-time' should be a number (of instructions to run for), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case LIMIT_TIME: {
+  int id = ingredients.at(0).at(0);
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->id == id) {
+      Routines.at(i)->limit = ingredients.at(1).at(0);
+      break;
+    }
+  }
+  break;
+}
+
+:(before "End routine Fields")
+int instructions_run;
+:(before "End routine Constructor")
+instructions_run = 0;
+:(before "Reset instructions_run_this_scheduling_slice")
+Current_routine->instructions_run += Current_routine->instructions_run_this_scheduling_slice;
+:(before "End Primitive Recipe Declarations")
+NUMBER_OF_INSTRUCTIONS,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "number-of-instructions", NUMBER_OF_INSTRUCTIONS);
+:(before "End Primitive Recipe Checks")
+case NUMBER_OF_INSTRUCTIONS: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'number-of-instructions' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'number-of-instructions' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case NUMBER_OF_INSTRUCTIONS: {
+  int id = ingredients.at(0).at(0);
+  int result = -1;
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->id == id) {
+      result = Routines.at(i)->instructions_run;
+      break;
+    }
+  }
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(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
+
+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/archive/2.vm/074wait.cc b/archive/2.vm/074wait.cc
new file mode 100644
index 00000000..8eb4b887
--- /dev/null
+++ b/archive/2.vm/074wait.cc
@@ -0,0 +1,664 @@
+//: Routines can be put in a 'waiting' state, from which it will be ready to
+//: run again when a specific memory location changes its value. This is Mu's
+//: basic technique for orchestrating the order in which different routines
+//: operate.
+
+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
+
+:(before "End routine States")
+WAITING,
+:(before "End routine Fields")
+// only if state == WAITING
+int waiting_on_location;
+:(before "End routine Constructor")
+waiting_on_location = 0;
+
+:(before "End Mu Test Teardown")
+if (Passed && any_routines_waiting())
+  raise << Current_scenario->name << ": deadlock!\n" << end();
+:(before "End Run Routine")
+if (any_routines_waiting()) {
+  raise << "deadlock!\n" << end();
+  dump_waiting_routines();
+}
+:(before "End Test Teardown")
+if (Passed && any_routines_with_error())
+  raise << "some routines died with errors\n" << end();
+:(code)
+bool any_routines_waiting() {
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->state == WAITING)
+      return true;
+  }
+  return false;
+}
+void dump_waiting_routines() {
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->state == WAITING)
+      cerr << i << ": " << routine_label(Routines.at(i)) << '\n';
+  }
+}
+
+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
+//: build locks.
+
+:(before "End Primitive Recipe Declarations")
+WAIT_FOR_RESET_THEN_SET,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "wait-for-reset-then-set", WAIT_FOR_RESET_THEN_SET);
+:(before "End Primitive Recipe Checks")
+case WAIT_FOR_RESET_THEN_SET: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'wait-for-reset-then-set' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_location(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'wait-for-reset-then-set' requires a location ingredient, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case WAIT_FOR_RESET_THEN_SET: {
+  int loc = static_cast<int>(ingredients.at(0).at(0));
+  trace(Callstack_depth+1, "run") << "wait: *" << loc << " = " << get_or_insert(Memory, loc) << end();
+  if (get_or_insert(Memory, loc) == 0) {
+    trace(Callstack_depth+1, "run") << "location " << loc << " is already 0; setting" << end();
+    put(Memory, loc, 1);
+    break;
+  }
+  trace(Callstack_depth+1, "run") << "waiting for location " << loc << " to reset" << end();
+  Current_routine->state = WAITING;
+  Current_routine->waiting_on_location = loc;
+  break;
+}
+
+//: Counterpart to unlock a lock.
+:(before "End Primitive Recipe Declarations")
+RESET,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "reset", RESET);
+:(before "End Primitive Recipe Checks")
+case RESET: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'reset' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_location(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'reset' requires a location ingredient, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case RESET: {
+  int loc = static_cast<int>(ingredients.at(0).at(0));
+  put(Memory, loc, 0);
+  trace(Callstack_depth+1, "run") << "reset: *" << loc << " = " << get_or_insert(Memory, loc) << end();
+  break;
+}
+
+//: scheduler tweak to get routines out of that state
+
+:(before "End Scheduler State Transitions")
+for (int i = 0;  i < SIZE(Routines);  ++i) {
+  if (Routines.at(i)->state != WAITING) continue;
+  int loc = Routines.at(i)->waiting_on_location;
+  if (loc && get_or_insert(Memory, loc) == 0) {
+    trace(100, "schedule") << "waking up routine " << Routines.at(i)->id << end();
+    put(Memory, loc, 1);
+    Routines.at(i)->state = RUNNING;
+    Routines.at(i)->waiting_on_location = 0;
+  }
+}
+
+//: Primitive to help compute locations to wait on.
+//: Only supports elements immediately inside containers; no arrays or
+//: containers within containers yet.
+
+:(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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "get-location", GET_LOCATION);
+:(before "End Primitive Recipe Checks")
+case GET_LOCATION: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'get-location' expects exactly 2 ingredients in '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  reagent/*copy*/ base = inst.ingredients.at(0);
+  if (!canonize_type(base)) break;
+  if (!base.type) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'get-location' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  const type_tree* base_root_type = base.type->atom ? base.type : base.type->left;
+  if (!base_root_type->atom || base_root_type->value == 0 || !contains_key(Type, base_root_type->value) || get(Type, base_root_type->value).kind != CONTAINER) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'get-location' should be a container, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  type_ordinal base_type = base.type->value;
+  const reagent& offset = inst.ingredients.at(1);
+  if (!is_literal(offset) || !is_mu_scalar(offset)) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'get-location' should have type 'offset', but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  int offset_value = 0;
+  //: later layers will permit non-integer offsets
+  if (is_integer(offset.name)) {
+    offset_value = to_integer(offset.name);
+    if (offset_value < 0 || offset_value >= SIZE(get(Type, base_type).elements)) {
+      raise << maybe(get(Recipe, r).name) << "invalid offset " << offset_value << " for '" << get(Type, base_type).name << "'\n" << end();
+      break;
+    }
+  }
+  else {
+    offset_value = offset.value;
+  }
+  if (inst.products.empty()) break;
+  if (!is_mu_location(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "'get-location " << base.original_string << ", " << offset.original_string << "' should write to type location but '" << inst.products.at(0).name << "' has type '" << names_to_string_without_quotes(inst.products.at(0).type) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case GET_LOCATION: {
+  reagent/*copy*/ base = current_instruction().ingredients.at(0);
+  canonize(base);
+  int base_address = base.value;
+  if (base_address == 0) {
+    raise << maybe(current_recipe_name()) << "tried to access location 0 in '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  const type_tree* base_type = get_base_type(base.type);
+  int offset = ingredients.at(1).at(0);
+  if (offset < 0 || offset >= SIZE(get(Type, base_type->value).elements)) break;  // copied from Check above
+  int result = base_address;
+  for (int i = 0;  i < offset;  ++i)
+    result += size_of(element_type(base.type, i));
+  trace(Callstack_depth+1, "run") << "address to copy is " << result << end();
+  products.resize(1);
+  products.at(0).push_back(result);
+  break;
+}
+
+:(code)
+bool is_mu_location(reagent/*copy*/ x) {
+  if (!canonize_type(x)) return false;
+  if (!x.type) return false;
+  if (!x.type->atom) return false;
+  return x.type->value == get(Type_ordinal, "location");
+}
+
+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
+
+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
+int waiting_on_routine;
+:(before "End routine Constructor")
+waiting_on_routine = 0;
+
+:(before "End Primitive Recipe Declarations")
+WAIT_FOR_ROUTINE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "wait-for-routine", WAIT_FOR_ROUTINE);
+:(before "End Primitive Recipe Checks")
+case WAIT_FOR_ROUTINE: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'wait-for-routine' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'wait-for-routine' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case WAIT_FOR_ROUTINE: {
+  if (ingredients.at(0).at(0) == Current_routine->id) {
+    raise << maybe(current_recipe_name()) << "routine can't wait for itself! '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  Current_routine->state = WAITING;
+  Current_routine->waiting_on_routine = ingredients.at(0).at(0);
+  trace(Callstack_depth+1, "run") << "waiting for routine " << ingredients.at(0).at(0) << end();
+  break;
+}
+
+:(before "End Scheduler State Transitions")
+// Wake up any routines waiting for other routines to complete.
+// Important: this must come after the scheduler loop above giving routines
+// waiting for locations to change a chance to wake up.
+for (int i = 0;  i < SIZE(Routines);  ++i) {
+  if (Routines.at(i)->state != WAITING) continue;
+  routine* waiter = Routines.at(i);
+  if (!waiter->waiting_on_routine) continue;
+  int id = waiter->waiting_on_routine;
+  assert(id != waiter->id);  // routine can't wait on itself
+  for (int j = 0;  j < SIZE(Routines);  ++j) {
+    const routine* waitee = Routines.at(j);
+    if (waitee->id == id && waitee->state != RUNNING && waitee->state != WAITING) {
+      // routine is COMPLETED or DISCONTINUED
+      trace(100, "schedule") << "waking up routine " << waiter->id << end();
+      waiter->state = RUNNING;
+      waiter->waiting_on_routine = 0;
+    }
+  }
+}
+
+//: yield voluntarily to let some other routine run
+
+:(before "End Primitive Recipe Declarations")
+SWITCH,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "switch", SWITCH);
+:(before "End Primitive Recipe Checks")
+case SWITCH: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SWITCH: {
+  ++current_step_index();
+  goto stop_running_current_routine;
+}
+
+:(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
+//:
+//: Managing arbitrary scenarios requires the ability to:
+//:   a) check if a routine is blocked
+//:   b) restart a blocked routine ('restart')
+//:
+//: A routine is blocked either if it's waiting or if it explicitly signals
+//: that it's blocked (even as it periodically wakes up and polls for some
+//: event).
+//:
+//: Signalling blockedness might well be a huge hack. But Mu doesn't have Unix
+//: signals to avoid polling with, because signals are also pretty hacky.
+
+:(before "End routine Fields")
+bool blocked;
+:(before "End routine Constructor")
+blocked = false;
+
+:(before "End Primitive Recipe Declarations")
+CURRENT_ROUTINE_IS_BLOCKED,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "current-routine-is-blocked", CURRENT_ROUTINE_IS_BLOCKED);
+:(before "End Primitive Recipe Checks")
+case CURRENT_ROUTINE_IS_BLOCKED: {
+  if (!inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'current-routine-is-blocked' should have no ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CURRENT_ROUTINE_IS_BLOCKED: {
+  Current_routine->blocked = true;
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CURRENT_ROUTINE_IS_UNBLOCKED,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "current-routine-is-unblocked", CURRENT_ROUTINE_IS_UNBLOCKED);
+:(before "End Primitive Recipe Checks")
+case CURRENT_ROUTINE_IS_UNBLOCKED: {
+  if (!inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'current-routine-is-unblocked' should have no ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CURRENT_ROUTINE_IS_UNBLOCKED: {
+  Current_routine->blocked = false;
+  break;
+}
+
+//: also allow waiting on a routine to block
+//: (just for tests; use wait_for_routine above wherever possible)
+
+:(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
+int waiting_on_routine_to_block;
+:(before "End routine Constructor")
+waiting_on_routine_to_block = 0;
+
+:(before "End Primitive Recipe Declarations")
+WAIT_FOR_ROUTINE_TO_BLOCK,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "wait-for-routine-to-block", WAIT_FOR_ROUTINE_TO_BLOCK);
+:(before "End Primitive Recipe Checks")
+case WAIT_FOR_ROUTINE_TO_BLOCK: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'wait-for-routine-to-block' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'wait-for-routine-to-block' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case WAIT_FOR_ROUTINE_TO_BLOCK: {
+  if (ingredients.at(0).at(0) == Current_routine->id) {
+    raise << maybe(current_recipe_name()) << "routine can't wait for itself! '" << to_original_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  Current_routine->state = WAITING;
+  Current_routine->waiting_on_routine_to_block = ingredients.at(0).at(0);
+  trace(Callstack_depth+1, "run") << "waiting for routine " << ingredients.at(0).at(0) << " to block" << end();
+  break;
+}
+
+:(before "End Scheduler State Transitions")
+// Wake up any routines waiting for other routines to stop running.
+for (int i = 0;  i < SIZE(Routines);  ++i) {
+  if (Routines.at(i)->state != WAITING) continue;
+  routine* waiter = Routines.at(i);
+  if (!waiter->waiting_on_routine_to_block) continue;
+  int id = waiter->waiting_on_routine_to_block;
+  assert(id != waiter->id);  // routine can't wait on itself
+  for (int j = 0;  j < SIZE(Routines);  ++j) {
+    const routine* waitee = Routines.at(j);
+    if (waitee->id != id) continue;
+    if (waitee->state != RUNNING || waitee->blocked) {
+      trace(100, "schedule") << "waking up routine " << waiter->id << " because routine " << waitee->id << " is blocked" << end();
+      waiter->state = RUNNING;
+      waiter->waiting_on_routine_to_block = 0;
+    }
+  }
+}
+
+//: helper for restarting blocking routines in tests
+
+:(before "End Primitive Recipe Declarations")
+RESTART,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "restart", RESTART);
+:(before "End Primitive Recipe Checks")
+case RESTART: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'restart' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'restart' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case RESTART: {
+  int id = ingredients.at(0).at(0);
+  for (int i = 0;  i < SIZE(Routines);  ++i) {
+    if (Routines.at(i)->id == id) {
+      if (Routines.at(i)->state == WAITING)
+        Routines.at(i)->state = RUNNING;
+      Routines.at(i)->blocked = false;
+      break;
+    }
+  }
+  break;
+}
+
+:(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/archive/2.vm/075channel.mu b/archive/2.vm/075channel.mu
new file mode 100644
index 00000000..c64492cb
--- /dev/null
+++ b/archive/2.vm/075channel.mu
@@ -0,0 +1,510 @@
+# Mu synchronizes between routines using channels rather than locks, like
+# Erlang and Go.
+#
+# The key property of channels: Writing to a full channel or reading from an
+# empty one will put the current routine in 'waiting' state until the
+# operation can be completed.
+#
+# Beware of addresses passed into channels. They can cause race conditions.
+
+scenario channel [
+  run [
+    local-scope
+    source:&:source:num, sink:&:sink:num <- new-channel 3/capacity
+    sink <- write sink, 34
+    10:num/raw, 11:bool/raw, source <- read source
+  ]
+  memory-should-contain [
+    10 <- 34
+    11 <- 0  # read was successful
+  ]
+]
+
+container channel:_elem [
+  lock:bool  # inefficient but simple: serialize all reads as well as writes
+  first-full:num  # for write
+  first-free:num  # for read
+  # A circular buffer contains values from index first-full up to (but not
+  # including) index first-free. The reader always modifies it at first-full,
+  # while the writer always modifies it at first-free.
+  data:&:@:_elem
+]
+
+# Since channels have two ends, and since it's an error to use either end from
+# multiple routines, let's distinguish the ends.
+
+container source:_elem [
+  chan:&:channel:_elem
+]
+
+container sink:_elem [
+  chan:&:channel:_elem
+]
+
+def new-channel capacity:num -> in:&:source:_elem, out:&:sink:_elem [
+  local-scope
+  load-inputs
+  result:&:channel:_elem <- new {(channel _elem): type}
+  *result <- put *result, first-full:offset, 0
+  *result <- put *result, first-free:offset, 0
+  capacity <- add capacity, 1  # unused slot for 'full?' below
+  data:&:@:_elem <- new _elem:type, capacity
+  *result <- put *result, data:offset, data
+  in <- new {(source _elem): type}
+  *in <- put *in, chan:offset, result
+  out <- new {(sink _elem): type}
+  *out <- put *out, chan:offset, result
+]
+
+# write a value to a channel
+def write out:&:sink:_elem, val:_elem -> out:&:sink:_elem [
+  local-scope
+  load-inputs
+  assert out, [write to null channel]
+  chan:&:channel:_elem <- get *out, chan:offset
+  <channel-write-initial>
+  # block until lock is acquired AND queue has room
+  lock:location <- get-location *chan, lock:offset
+#?   $print [write], 10/newline
+  {
+#?     $print [trying to acquire lock for writing], 10/newline
+    wait-for-reset-then-set lock
+#?     $print [lock acquired for writing], 10/newline
+    full?:bool <- channel-full? chan
+    break-unless full?
+#?     $print [but channel is full; relinquishing lock], 10/newline
+    # channel is full; relinquish lock and give a reader the opportunity to
+    # create room on it
+    reset lock
+    current-routine-is-blocked
+    switch  # avoid spinlocking
+    loop
+  }
+  current-routine-is-unblocked
+#?   $print [performing write], 10/newline
+  # store a deep copy of val
+  circular-buffer:&:@:_elem <- get *chan, data:offset
+  free:num <- get *chan, first-free:offset
+  *circular-buffer <- put-index *circular-buffer, free, val
+  # mark its slot as filled
+  free <- add free, 1
+  {
+    # wrap free around to 0 if necessary
+    len:num <- length *circular-buffer
+    at-end?:bool <- greater-or-equal free, len
+    break-unless at-end?
+    free <- copy 0
+  }
+  # write back
+  *chan <- put *chan, first-free:offset, free
+#?   $print [relinquishing lock after writing], 10/newline
+  reset lock
+]
+
+# read a value from a channel
+def read in:&:source:_elem -> result:_elem, eof?:bool, in:&:source:_elem [
+  local-scope
+  load-inputs
+  assert in, [read on null channel]
+  eof? <- copy false  # default result
+  chan:&:channel:_elem <- get *in, chan:offset
+  # block until lock is acquired AND queue has data
+  lock:location <- get-location *chan, lock:offset
+#?   $print [read], 10/newline
+  {
+#?     $print [trying to acquire lock for reading], 10/newline
+    wait-for-reset-then-set lock
+#?     $print [lock acquired for reading], 10/newline
+    empty?:bool <- channel-empty? chan
+    break-unless empty?
+#?     $print [but channel is empty; relinquishing lock], 10/newline
+    # channel is empty; relinquish lock and give a writer the opportunity to
+    # add to it
+    reset lock
+    current-routine-is-blocked
+    <channel-read-empty>
+    switch  # avoid spinlocking
+    loop
+  }
+  current-routine-is-unblocked
+  # pull result off
+  full:num <- get *chan, first-full:offset
+  circular-buffer:&:@:_elem <- get *chan, data:offset
+  result <- index *circular-buffer, full
+  # clear the slot
+  empty:&:_elem <- new _elem:type
+  *circular-buffer <- put-index *circular-buffer, full, *empty
+  # mark its slot as empty
+  full <- add full, 1
+  {
+    # wrap full around to 0 if necessary
+    len:num <- length *circular-buffer
+    at-end?:bool <- greater-or-equal full, len
+    break-unless at-end?
+    full <- copy 0
+  }
+  # write back
+  *chan <- put *chan, first-full:offset, full
+#?   $print [relinquishing lock after reading], 10/newline
+  reset lock
+]
+
+# todo: create a notion of iterator and iterable so we can read/write whole
+# aggregates (arrays, lists, ..) of _elems at once.
+
+scenario channel-initialization [
+  run [
+    local-scope
+    source:&:source:num <- new-channel 3/capacity
+    chan:&:channel:num <- get *source, chan:offset
+    10:num/raw <- get *chan, first-full:offset
+    11:num/raw <- get *chan, first-free:offset
+  ]
+  memory-should-contain [
+    10 <- 0  # first-full
+    11 <- 0  # first-free
+  ]
+]
+
+scenario channel-write-increments-free [
+  local-scope
+  _, sink:&:sink:num <- new-channel 3/capacity
+  run [
+    sink <- write sink, 34
+    chan:&:channel:num <- get *sink, chan:offset
+    10:num/raw <- get *chan, first-full:offset
+    11:num/raw <- get *chan, first-free:offset
+  ]
+  memory-should-contain [
+    10 <- 0  # first-full
+    11 <- 1  # first-free
+  ]
+]
+
+scenario channel-read-increments-full [
+  local-scope
+  source:&:source:num, sink:&:sink:num <- new-channel 3/capacity
+  sink <- write sink, 34
+  run [
+    _, _, source <- read source
+    chan:&:channel:num <- get *source, chan:offset
+    10:num/raw <- get *chan, first-full:offset
+    11:num/raw <- get *chan, first-free:offset
+  ]
+  memory-should-contain [
+    10 <- 1  # first-full
+    11 <- 1  # first-free
+  ]
+]
+
+scenario channel-wrap [
+  local-scope
+  # channel with just 1 slot
+  source:&:source:num, sink:&:sink:num <- new-channel 1/capacity
+  chan:&:channel:num <- get *source, chan:offset
+  # write and read a value
+  sink <- write sink, 34
+  _, _, source <- read source
+  run [
+    # first-free will now be 1
+    10:num/raw <- get *chan, first-free:offset
+    11:num/raw <- get *chan, first-free:offset
+    # write second value, verify that first-free wraps
+    sink <- write sink, 34
+    20:num/raw <- get *chan, first-free:offset
+    # read second value, verify that first-full wraps
+    _, _, source <- read source
+    30:num/raw <- get *chan, first-full:offset
+  ]
+  memory-should-contain [
+    10 <- 1  # first-free after first write
+    11 <- 1  # first-full after first read
+    20 <- 0  # first-free after second write, wrapped
+    30 <- 0  # first-full after second read, wrapped
+  ]
+]
+
+scenario channel-new-empty-not-full [
+  run [
+    local-scope
+    source:&:source:num <- new-channel 3/capacity
+    chan:&:channel:num <- get *source, chan:offset
+    10:bool/raw <- channel-empty? chan
+    11:bool/raw <- channel-full? chan
+  ]
+  memory-should-contain [
+    10 <- 1  # empty?
+    11 <- 0  # full?
+  ]
+]
+
+scenario channel-write-not-empty [
+  local-scope
+  source:&:source:num, sink:&:sink:num <- new-channel 3/capacity
+  chan:&:channel:num <- get *source, chan:offset
+  run [
+    sink <- write sink, 34
+    10:bool/raw <- channel-empty? chan
+    11:bool/raw <- channel-full? chan
+  ]
+  memory-should-contain [
+    10 <- 0  # empty?
+    11 <- 0  # full?
+  ]
+]
+
+scenario channel-write-full [
+  local-scope
+  source:&:source:num, sink:&:sink:num <- new-channel 1/capacity
+  chan:&:channel:num <- get *source, chan:offset
+  run [
+    sink <- write sink, 34
+    10:bool/raw <- channel-empty? chan
+    11:bool/raw <- channel-full? chan
+  ]
+  memory-should-contain [
+    10 <- 0  # empty?
+    11 <- 1  # full?
+  ]
+]
+
+scenario channel-read-not-full [
+  local-scope
+  source:&:source:num, sink:&:sink:num <- new-channel 1/capacity
+  chan:&:channel:num <- get *source, chan:offset
+  sink <- write sink, 34
+  run [
+    _, _, source <- read source
+    10:bool/raw <- channel-empty? chan
+    11:bool/raw <- channel-full? chan
+  ]
+  memory-should-contain [
+    10 <- 1  # empty?
+    11 <- 0  # full?
+  ]
+]
+
+scenario channel-clear [
+  local-scope
+  # create a channel with a few items
+  source:&:source:num, sink:&:sink:num <- new-channel 3/capacity
+  chan:&:channel:num <- get *sink, chan:offset
+  write sink, 30
+  write sink, 31
+  write sink, 32
+  run [
+    clear source
+    10:bool/raw <- channel-empty? chan
+  ]
+  memory-should-contain [
+    10 <- 1  # after the call to 'clear', the channel should be empty
+  ]
+]
+
+def clear in:&:source:_elem -> in:&:source:_elem [
+  local-scope
+  load-inputs
+  chan:&:channel:_elem <- get *in, chan:offset
+  {
+    empty?:bool <- channel-empty? chan
+    break-if empty?
+    _, _, in <- read in
+    loop
+  }
+]
+
+## cancelling channels
+
+# every channel comes with a boolean signifying if it's been closed
+# initially this boolean is false
+container channel:_elem [
+  closed?:bool
+]
+
+# a channel can be closed from either the source or the sink
+# both routines can modify the 'closed?' bit, but they can only ever set it, so this is a benign race
+def close x:&:source:_elem -> x:&:source:_elem [
+  local-scope
+  load-inputs
+  chan:&:channel:_elem <- get *x, chan:offset
+  *chan <- put *chan, closed?:offset, true
+]
+def close x:&:sink:_elem -> x:&:sink:_elem [
+  local-scope
+  load-inputs
+  chan:&:channel:_elem <- get *x, chan:offset
+  *chan <- put *chan, closed?:offset, true
+]
+
+# once a channel is closed from one side, no further operations are expected from that side
+# if a channel is closed for reading,
+#   no further writes will be let through
+# if a channel is closed for writing,
+#   future reads continue until the channel empties,
+#   then the channel is also closed for reading
+after <channel-write-initial> [
+  closed?:bool <- get *chan, closed?:offset
+  return-if closed?
+]
+after <channel-read-empty> [
+  closed?:bool <- get *chan, closed?:offset
+  {
+    break-unless closed?
+    empty-result:&:_elem <- new _elem:type
+    current-routine-is-unblocked
+    return *empty-result, true
+  }
+]
+
+## helpers
+
+# An empty channel has first-free and first-full both at the same value.
+def channel-empty? chan:&:channel:_elem -> result:bool [
+  local-scope
+  load-inputs
+  # return chan.first-full == chan.first-free
+  full:num <- get *chan, first-full:offset
+  free:num <- get *chan, first-free:offset
+  result <- equal full, free
+]
+
+# A full channel has first-free just before first-full, wasting one slot.
+# (Other alternatives: https://www.snellman.net/blog/archive/2016-12-13-ring-buffers)
+def channel-full? chan:&:channel:_elem -> result:bool [
+  local-scope
+  load-inputs
+  # tmp = chan.first-free + 1
+  tmp:num <- get *chan, first-free:offset
+  tmp <- add tmp, 1
+  {
+    # if tmp == chan.capacity, tmp = 0
+    len:num <- capacity chan
+    at-end?:bool <- greater-or-equal tmp, len
+    break-unless at-end?
+    tmp <- copy 0
+  }
+  # return chan.first-full == tmp
+  full:num <- get *chan, first-full:offset
+  result <- equal full, tmp
+]
+
+def capacity chan:&:channel:_elem -> result:num [
+  local-scope
+  load-inputs
+  q:&:@:_elem <- get *chan, data:offset
+  result <- length *q
+]
+
+## helpers for channels of characters in particular
+
+def buffer-lines in:&:source:char, buffered-out:&:sink:char -> buffered-out:&:sink:char, in:&:source:char [
+  local-scope
+  load-inputs
+  # repeat forever
+  eof?:bool <- copy false
+  {
+    line:&:buffer:char <- new-buffer 30
+    # read characters from 'in' until newline, copy into line
+    {
+      +next-character
+      c:char, eof?:bool, in <- read in
+      break-if eof?
+      # drop a character on backspace
+      {
+        # special-case: if it's a backspace
+        backspace?:bool <- equal c, 8
+        break-unless backspace?
+        # drop previous character
+        {
+          buffer-length:num <- get *line, length:offset
+          buffer-empty?:bool <- equal buffer-length, 0
+          break-if buffer-empty?
+          buffer-length <- subtract buffer-length, 1
+          *line <- put *line, length:offset, buffer-length
+        }
+        # and don't append this one
+        loop +next-character
+      }
+      # append anything else
+      line <- append line, c
+      line-done?:bool <- equal c, 10/newline
+      break-if line-done?
+      loop
+    }
+    # copy line into 'buffered-out'
+    i:num <- copy 0
+    line-contents:text <- get *line, data:offset
+    max:num <- get *line, length:offset
+    {
+      done?:bool <- greater-or-equal i, max
+      break-if done?
+      c:char <- index *line-contents, i
+      buffered-out <- write buffered-out, c
+      i <- add i, 1
+      loop
+    }
+    {
+      break-unless eof?
+      buffered-out <- close buffered-out
+      return
+    }
+    loop
+  }
+]
+
+scenario buffer-lines-blocks-until-newline [
+  run [
+    local-scope
+    source:&:source:char, sink:&:sink:char <- new-channel 10/capacity
+    _, buffered-stdin:&:sink:char/buffered-stdin <- new-channel 10/capacity
+    buffered-chan:&:channel:char <- get *buffered-stdin, chan:offset
+    empty?:bool <- channel-empty? buffered-chan
+    assert empty?, [ 
+F buffer-lines-blocks-until-newline: channel should be empty after init]
+    # buffer stdin into buffered-stdin, try to read from buffered-stdin
+    buffer-routine:num <- start-running buffer-lines, source, buffered-stdin
+    wait-for-routine-to-block buffer-routine
+    empty? <- channel-empty? buffered-chan
+    assert empty?:bool, [ 
+F buffer-lines-blocks-until-newline: channel should be empty after buffer-lines bring-up]
+    # write 'a'
+    sink <- write sink, 97/a
+    restart buffer-routine
+    wait-for-routine-to-block buffer-routine
+    empty? <- channel-empty? buffered-chan
+    assert empty?:bool, [ 
+F buffer-lines-blocks-until-newline: channel should be empty after writing 'a']
+    # write 'b'
+    sink <- write sink, 98/b
+    restart buffer-routine
+    wait-for-routine-to-block buffer-routine
+    empty? <- channel-empty? buffered-chan
+    assert empty?:bool, [ 
+F buffer-lines-blocks-until-newline: channel should be empty after writing 'b']
+    # write newline
+    sink <- write sink, 10/newline
+    restart buffer-routine
+    wait-for-routine-to-block buffer-routine
+    empty? <- channel-empty? buffered-chan
+    data-emitted?:bool <- not empty?
+    assert data-emitted?, [ 
+F buffer-lines-blocks-until-newline: channel should contain data after writing newline]
+    trace 1, [test], [reached end]
+  ]
+  trace-should-contain [
+    test: reached end
+  ]
+]
+
+def drain source:&:source:char -> result:text, source:&:source:char [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 30
+  {
+    c:char, done?:bool <- read source
+    break-if done?
+    buf <- append buf, c
+    loop
+  }
+  result <- buffer-to-array buf
+]
diff --git a/archive/2.vm/076continuation.cc b/archive/2.vm/076continuation.cc
new file mode 100644
index 00000000..670fef2f
--- /dev/null
+++ b/archive/2.vm/076continuation.cc
@@ -0,0 +1,406 @@
+//: Continuations are a powerful primitive for constructing advanced kinds of
+//: control *policies* like back-tracking.
+//:
+//: In Mu, continuations are first-class and delimited, and are constructed
+//: out of two primitives:
+//:
+//:  * 'call-with-continuation-mark' marks the top of the call stack and then
+//:    calls the provided recipe.
+//:  * 'return-continuation-until-mark' copies the top of the stack
+//:    until the mark, and returns it as the result of
+//:    'call-with-continuation-mark' (which might be a distant ancestor on the
+//:    call stack; intervening calls don't return)
+//:
+//: The resulting slice of the stack can now be called just like a regular
+//: recipe.
+//:
+//: See the example programs continuation*.mu to get a sense for the
+//: possibilities.
+//:
+//: Refinements:
+//:  * You can call a single continuation multiple times, and it will preserve
+//:    the state of its local variables at each stack frame between calls.
+//:    The stack frames of a continuation are not destroyed until the
+//:    continuation goes out of scope. See continuation2.mu.
+//:  * 'return-continuation-until-mark' doesn't consume the mark, so you can
+//:    return multiple continuations based on a single mark. In combination
+//:    with the fact that 'return-continuation-until-mark' can return from
+//:    regular calls, just as long as there was an earlier call to
+//:    'call-with-continuation-mark', this gives us a way to create resumable
+//:    recipes. See continuation3.mu.
+//:  * 'return-continuation-until-mark' can take ingredients to return just
+//:    like other 'return' instructions. It just implicitly also returns a
+//:    continuation as the first result. See continuation4.mu.
+//:  * Conversely, you can pass ingredients to a continuation when calling it,
+//:    to make it available to products of 'return-continuation-until-mark'.
+//:    See continuation5.mu.
+//:  * There can be multiple continuation marks on the stack at once;
+//:    'call-with-continuation-mark' and 'return-continuation-until-mark' both
+//:    need to pass in a tag to coordinate on the correct mark. This allows us
+//:    to save multiple continuations for different purposes (say if one is
+//:    for exceptions) with overlapping stack frames. See exception.mu.
+//:
+//: Inspired by James and Sabry, "Yield: Mainstream delimited continuations",
+//: Workshop on the Theory and Practice of Delimited Continuations, 2011.
+//: https://www.cs.indiana.edu/~sabry/papers/yield.pdf
+//:
+//: Caveats:
+//:  * At the moment we can't statically type-check continuations. So we raise
+//:    runtime errors for a call that doesn't return a continuation when the
+//:    caller expects, or one that returns a continuation when the caller
+//:    doesn't expect it. This shouldn't cause memory corruption, though.
+//:    There should still be no way to lookup addresses that aren't allocated.
+
+:(before "End Mu Types Initialization")
+type_ordinal continuation = Type_ordinal["continuation"] = Next_type_ordinal++;
+Type[continuation].name = "continuation";
+
+//: A continuation can be called like a recipe.
+:(before "End is_mu_recipe Atom Cases(r)")
+if (r.type->name == "continuation") return true;
+
+//: However, it can't be type-checked like most recipes. Pretend it's like a
+//: header-less recipe.
+:(after "Begin Reagent->Recipe(r, recipe_header)")
+if (r.type->atom && r.type->name == "continuation") {
+  result_header.has_header = false;
+  return result_header;
+}
+
+:(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;
+:(before "End call Constructor")
+continuation_mark_tag = 0;
+
+:(before "End Primitive Recipe Declarations")
+CALL_WITH_CONTINUATION_MARK,
+:(before "End Primitive Recipe Numbers")
+Recipe_ordinal["call-with-continuation-mark"] = CALL_WITH_CONTINUATION_MARK;
+:(before "End Primitive Recipe Checks")
+case CALL_WITH_CONTINUATION_MARK: {
+  if (SIZE(inst.ingredients) < 2) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' requires at least two ingredients: a mark number and a recipe to call\n" << end();
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CALL_WITH_CONTINUATION_MARK: {
+  // like call, but mark the current call as a 'base of continuation' call
+  // before pushing the next one on it
+  trace(Callstack_depth+1, "trace") << "delimited continuation; incrementing callstack depth to " << Callstack_depth << end();
+  ++Callstack_depth;
+  assert(Callstack_depth < Max_depth);
+  instruction/*copy*/ caller_instruction = current_instruction();
+  Current_routine->calls.front().continuation_mark_tag = current_instruction().ingredients.at(0).value;
+  Current_routine->calls.push_front(call(ingredients.at(1).at(0)));
+  // drop the mark
+  caller_instruction.ingredients.erase(caller_instruction.ingredients.begin());
+  ingredients.erase(ingredients.begin());
+  // drop the callee
+  caller_instruction.ingredients.erase(caller_instruction.ingredients.begin());
+  ingredients.erase(ingredients.begin());
+  finish_call_housekeeping(caller_instruction, ingredients);
+  continue;
+}
+
+:(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"
+  );
+}
+
+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.
+//: todo: implement delimited continuations in Mu's memory
+:(before "End Types")
+struct delimited_continuation {
+  call_stack frames;
+  int nrefs;
+  delimited_continuation(call_stack::iterator begin, call_stack::iterator end) :frames(call_stack(begin, end)), nrefs(0) {}
+};
+:(before "End Globals")
+map<long long int, delimited_continuation> Delimited_continuation;
+long long int Next_delimited_continuation_id = 1;  // 0 is null just like an address
+:(before "End Reset")
+Delimited_continuation.clear();
+Next_delimited_continuation_id = 1;
+
+:(before "End Primitive Recipe Declarations")
+RETURN_CONTINUATION_UNTIL_MARK,
+:(before "End Primitive Recipe Numbers")
+Recipe_ordinal["return-continuation-until-mark"] = RETURN_CONTINUATION_UNTIL_MARK;
+:(before "End Primitive Recipe Checks")
+case RETURN_CONTINUATION_UNTIL_MARK: {
+  if (inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'" << to_original_string(inst) << "' requires at least one ingredient: a mark tag (number)\n" << end();
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case RETURN_CONTINUATION_UNTIL_MARK: {
+  // I don't know how to think about next-ingredient in combination with
+  // continuations, so seems cleaner to just kill it. Functions have to read
+  // their inputs before ever returning a continuation.
+  Current_routine->calls.front().ingredient_atoms.clear();
+  Current_routine->calls.front().next_ingredient_to_process = 0;
+  // copy the current call stack until the most recent marked call
+  call_stack::iterator find_base_of_continuation(call_stack&, int);  // manual prototype containing '::'
+  call_stack::iterator base = find_base_of_continuation(Current_routine->calls, /*mark tag*/current_instruction().ingredients.at(0).value);
+  if (base == Current_routine->calls.end()) {
+    raise << maybe(current_recipe_name()) << "couldn't find a 'call-with-continuation-mark' to return to with tag " << current_instruction().ingredients.at(0).original_string << '\n' << end();
+    raise << maybe(current_recipe_name()) << "call stack:\n" << end();
+    for (call_stack::iterator p = Current_routine->calls.begin();  p != Current_routine->calls.end();  ++p)
+      raise << maybe(current_recipe_name()) << "  " << get(Recipe, p->running_recipe).name << '\n' << end();
+    break;
+  }
+  trace(Callstack_depth+1, "run") << "creating continuation " << Next_delimited_continuation_id << end();
+  put(Delimited_continuation, Next_delimited_continuation_id, delimited_continuation(Current_routine->calls.begin(), base));
+  while (Current_routine->calls.begin() != base) {
+    --Callstack_depth;
+    assert(Callstack_depth >= 0);
+    Current_routine->calls.pop_front();
+  }
+  // return it as the result of the marked call
+  products.resize(1);
+  products.at(0).push_back(Next_delimited_continuation_id);
+  // return any other ingredients passed in
+  copy(/*skip mark tag*/++ingredients.begin(), ingredients.end(), inserter(products, products.end()));
+  ++Next_delimited_continuation_id;
+  break;  // continue to process rest of marked call
+}
+
+:(code)
+call_stack::iterator find_base_of_continuation(call_stack& c, int mark_tag) {
+  for (call_stack::iterator p = c.begin(); p != c.end(); ++p)
+    if (p->continuation_mark_tag == mark_tag) return p;
+  return c.end();
+}
+
+//: overload 'call' for continuations
+:(after "Begin Call")
+if (is_mu_continuation(current_instruction().ingredients.at(0))) {
+  // copy multiple calls on to current call stack
+  assert(scalar(ingredients.at(0)));
+  trace(Callstack_depth+1, "run") << "calling continuation " << ingredients.at(0).at(0) << end();
+  if (!contains_key(Delimited_continuation, ingredients.at(0).at(0)))
+    raise << maybe(current_recipe_name()) << "no such delimited continuation " << current_instruction().ingredients.at(0).original_string << '\n' << end();
+  const call_stack& new_frames = get(Delimited_continuation, ingredients.at(0).at(0)).frames;
+  for (call_stack::const_reverse_iterator p = new_frames.rbegin(); p != new_frames.rend(); ++p)
+    Current_routine->calls.push_front(*p);
+  trace(Callstack_depth+1, "trace") << "calling delimited continuation; growing callstack depth to " << Callstack_depth+SIZE(new_frames) << end();
+  Callstack_depth += SIZE(new_frames);
+  assert(Callstack_depth < Max_depth);
+  // no call housekeeping; continuations don't support next-ingredient
+  copy(/*drop continuation*/++ingredients.begin(), ingredients.end(), inserter(products, products.begin()));
+  break;  // record results of resuming 'return-continuation-until-mark' instruction
+}
+
+:(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"
+  );
+}
+
+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.
+
+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") {
+  if (SIZE(inst.ingredients) > 1 && is_recipe_literal(inst.ingredients.at(/*skip mark*/1))) {
+  resolve_indirect_continuation_call(r, index, inst, caller_recipe);
+  return;
+  }
+}
+:(code)
+void resolve_indirect_continuation_call(const recipe_ordinal r, int index, instruction& inst, const recipe& caller_recipe) {
+  instruction inst2;
+  inst2.name = inst.ingredients.at(/*skip mark*/1).name;
+  for (int i = /*skip mark and recipe*/2;  i < SIZE(inst.ingredients);  ++i)
+    inst2.ingredients.push_back(inst.ingredients.at(i));
+  for (int i = /*skip continuation*/1;  i < SIZE(inst.products);  ++i)
+    inst2.products.push_back(inst.products.at(i));
+  resolve_ambiguous_call(r, index, inst2, caller_recipe);
+  inst.ingredients.at(/*skip mark*/1).name = inst2.name;
+  inst.ingredients.at(/*skip mark*/1).set_value(get(Recipe_ordinal, inst2.name));
+}
+
+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);
+}
+
+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) {
+  canonize_type(x);
+  return x.type && x.type->atom && x.type->value == get(Type_ordinal, "continuation");
+}
+
+// helper for debugging
+void dump(const int continuation_id) {
+  if (!contains_key(Delimited_continuation, continuation_id)) {
+    raise << "missing delimited continuation: " << continuation_id << '\n' << end();
+    return;
+  }
+  delimited_continuation& curr = get(Delimited_continuation, continuation_id);
+  dump(curr.frames);
+}
diff --git a/archive/2.vm/080display.cc b/archive/2.vm/080display.cc
new file mode 100644
index 00000000..6bf8e51d
--- /dev/null
+++ b/archive/2.vm/080display.cc
@@ -0,0 +1,462 @@
+//: Take raw control of the text-mode display and console, putting it in
+//: 'console' mode rather than the usual automatically-scrolling 'typewriter'
+//: mode.
+
+//:: Display management
+
+:(before "End Globals")
+int Display_row = 0;
+int Display_column = 0;
+
+:(before "End Includes")
+#define CHECK_SCREEN \
+    if (!tb_is_active()) { \
+      if (Run_tests) \
+        raise << maybe(current_recipe_name()) << "tried to print to real screen in a test!\n" << end(); \
+      else \
+        raise << maybe(current_recipe_name()) << "tried to print to real screen before 'open-console' or after 'close-console'\n" << end(); \
+      break; \
+    }
+#define CHECK_CONSOLE \
+    if (!tb_is_active()) { \
+      if (Run_tests) \
+        raise << maybe(current_recipe_name()) << "tried to read event from real keyboard/mouse in a test!\n" << end(); \
+      else \
+        raise << maybe(current_recipe_name()) << "tried to read event from real keyboard/mouse before 'open-console' or after 'close-console'\n" << end(); \
+      break; \
+    }
+
+:(before "End Primitive Recipe Declarations")
+OPEN_CONSOLE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "open-console", OPEN_CONSOLE);
+:(before "End Primitive Recipe Checks")
+case OPEN_CONSOLE: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case OPEN_CONSOLE: {
+  tb_init();
+  std::setvbuf(stdout, NULL, _IONBF, 0);  // disable buffering in cout
+  Display_row = Display_column = 0;
+  int width = tb_width();
+  int height = tb_height();
+  if (width > 222 || height > 222) {
+    if (width > 222)
+      raise << "sorry, Mu doesn't support windows wider than 222 characters in console mode. Please resize your window.\n" << end();
+    if (height > 222)
+      raise << "sorry, Mu doesn't support windows taller than 222 characters in console mode. Please resize your window.\n" << end();
+    exit(1);
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CLOSE_CONSOLE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "close-console", CLOSE_CONSOLE);
+:(before "End Primitive Recipe Checks")
+case CLOSE_CONSOLE: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CLOSE_CONSOLE: {
+  tb_shutdown();
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CLEAR_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "clear-display", CLEAR_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case CLEAR_DISPLAY: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CLEAR_DISPLAY: {
+  CHECK_SCREEN;
+  tb_clear();
+  Display_row = Display_column = 0;
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+PRINT_CHARACTER_TO_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "print-character-to-display", PRINT_CHARACTER_TO_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case PRINT_CHARACTER_TO_DISPLAY: {
+  if (inst.ingredients.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'print-character-to-display' requires at least one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'print-character-to-display' should be a character, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.ingredients) > 1) {
+    if (!is_mu_number(inst.ingredients.at(1))) {
+      raise << maybe(get(Recipe, r).name) << "second ingredient of 'print-character-to-display' should be a foreground color number, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+      break;
+    }
+  }
+  if (SIZE(inst.ingredients) > 2) {
+    if (!is_mu_number(inst.ingredients.at(2))) {
+      raise << maybe(get(Recipe, r).name) << "third ingredient of 'print-character-to-display' should be a background color number, but got '" << inst.ingredients.at(2).original_string << "'\n" << end();
+      break;
+    }
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case PRINT_CHARACTER_TO_DISPLAY: {
+  CHECK_SCREEN;
+  int h=tb_height(), w=tb_width();
+  int height = (h >= 0) ? h : 0;
+  int width = (w >= 0) ? w : 0;
+  int c = ingredients.at(0).at(0);
+  int color = TB_WHITE;
+  if (SIZE(ingredients) > 1) {
+    color = ingredients.at(1).at(0);
+  }
+  int bg_color = TB_BLACK;
+  if (SIZE(ingredients) > 2) {
+    bg_color = ingredients.at(2).at(0);
+    if (bg_color == 0) bg_color = TB_BLACK;
+  }
+  tb_print(c, color, bg_color);
+  // track row and column, mimicking what happens on screen
+  if (c == '\n') {
+    if (Display_row < height-1) ++Display_row;  // otherwise we scroll and Display_row remains unchanged
+  }
+  else if (c == '\r') {
+    Display_column = 0;
+  }
+  else if (c == '\b') {
+    if (Display_column > 0) --Display_column;
+  }
+  else {
+    ++Display_column;
+    if (Display_column >= width) {
+      Display_column = 0;
+      if (Display_row < height-1) ++Display_row;
+    }
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CURSOR_POSITION_ON_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "cursor-position-on-display", CURSOR_POSITION_ON_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case CURSOR_POSITION_ON_DISPLAY: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CURSOR_POSITION_ON_DISPLAY: {
+  CHECK_SCREEN;
+  products.resize(2);
+  products.at(0).push_back(Display_row);
+  products.at(1).push_back(Display_column);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+MOVE_CURSOR_ON_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "move-cursor-on-display", MOVE_CURSOR_ON_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case MOVE_CURSOR_ON_DISPLAY: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'move-cursor-on-display' requires two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'move-cursor-on-display' should be a row number, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'move-cursor-on-display' should be a column number, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MOVE_CURSOR_ON_DISPLAY: {
+  CHECK_SCREEN;
+  Display_row = ingredients.at(0).at(0);
+  Display_column = ingredients.at(1).at(0);
+  tb_set_cursor(Display_column, Display_row);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+MOVE_CURSOR_DOWN_ON_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "move-cursor-down-on-display", MOVE_CURSOR_DOWN_ON_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case MOVE_CURSOR_DOWN_ON_DISPLAY: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MOVE_CURSOR_DOWN_ON_DISPLAY: {
+  CHECK_SCREEN;
+  int h=tb_height();
+  int height = (h >= 0) ? h : 0;
+  if (Display_row < height-1) {
+    ++Display_row;
+    tb_set_cursor(Display_column, Display_row);
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+MOVE_CURSOR_UP_ON_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "move-cursor-up-on-display", MOVE_CURSOR_UP_ON_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case MOVE_CURSOR_UP_ON_DISPLAY: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MOVE_CURSOR_UP_ON_DISPLAY: {
+  CHECK_SCREEN;
+  if (Display_row > 0) {
+    --Display_row;
+    tb_set_cursor(Display_column, Display_row);
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+MOVE_CURSOR_RIGHT_ON_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "move-cursor-right-on-display", MOVE_CURSOR_RIGHT_ON_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case MOVE_CURSOR_RIGHT_ON_DISPLAY: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MOVE_CURSOR_RIGHT_ON_DISPLAY: {
+  CHECK_SCREEN;
+  int w=tb_width();
+  int width = (w >= 0) ? w : 0;
+  if (Display_column < width-1) {
+    ++Display_column;
+    tb_set_cursor(Display_column, Display_row);
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+MOVE_CURSOR_LEFT_ON_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "move-cursor-left-on-display", MOVE_CURSOR_LEFT_ON_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case MOVE_CURSOR_LEFT_ON_DISPLAY: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case MOVE_CURSOR_LEFT_ON_DISPLAY: {
+  CHECK_SCREEN;
+  if (Display_column > 0) {
+    --Display_column;
+    tb_set_cursor(Display_column, Display_row);
+  }
+  break;
+}
+
+//: as a convenience, make $print mostly work in console mode
+:(before "End $print 10/newline Special-cases")
+else if (tb_is_active()) {
+  move_cursor_to_start_of_next_line_on_display();
+}
+:(code)
+void move_cursor_to_start_of_next_line_on_display() {
+  if (Display_row < tb_height()-1) ++Display_row;
+  else Display_row = 0;
+  Display_column = 0;
+  tb_set_cursor(Display_column, Display_row);
+}
+
+:(before "End Primitive Recipe Declarations")
+DISPLAY_WIDTH,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "display-width", DISPLAY_WIDTH);
+:(before "End Primitive Recipe Checks")
+case DISPLAY_WIDTH: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case DISPLAY_WIDTH: {
+  CHECK_SCREEN;
+  products.resize(1);
+  products.at(0).push_back(tb_width());
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+DISPLAY_HEIGHT,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "display-height", DISPLAY_HEIGHT);
+:(before "End Primitive Recipe Checks")
+case DISPLAY_HEIGHT: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case DISPLAY_HEIGHT: {
+  CHECK_SCREEN;
+  products.resize(1);
+  products.at(0).push_back(tb_height());
+  break;
+}
+
+//:: Keyboard/mouse management
+
+:(before "End Primitive Recipe Declarations")
+WAIT_FOR_SOME_INTERACTION,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "wait-for-some-interaction", WAIT_FOR_SOME_INTERACTION);
+:(before "End Primitive Recipe Checks")
+case WAIT_FOR_SOME_INTERACTION: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case WAIT_FOR_SOME_INTERACTION: {
+  CHECK_SCREEN;
+  tb_event event;
+  tb_poll_event(&event);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CHECK_FOR_INTERACTION,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "check-for-interaction", CHECK_FOR_INTERACTION);
+:(before "End Primitive Recipe Checks")
+case CHECK_FOR_INTERACTION: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CHECK_FOR_INTERACTION: {
+  CHECK_CONSOLE;
+  products.resize(2);  // result and status
+  tb_event event;
+  int event_type = tb_peek_event(&event, 5/*ms*/);
+  if (event_type == TB_EVENT_KEY && event.ch) {
+    products.at(0).push_back(/*text event*/0);
+    products.at(0).push_back(event.ch);
+    products.at(0).push_back(0);
+    products.at(0).push_back(0);
+    products.at(1).push_back(/*found*/true);
+    break;
+  }
+  // treat keys within ascii as unicode characters
+  if (event_type == TB_EVENT_KEY && event.key < 0xff) {
+    products.at(0).push_back(/*text event*/0);
+    if (event.key == TB_KEY_CTRL_C) exit(1);
+    if (event.key == TB_KEY_BACKSPACE2) event.key = TB_KEY_BACKSPACE;
+    if (event.key == TB_KEY_CARRIAGE_RETURN) event.key = TB_KEY_NEWLINE;
+    products.at(0).push_back(event.key);
+    products.at(0).push_back(0);
+    products.at(0).push_back(0);
+    products.at(1).push_back(/*found*/true);
+    break;
+  }
+  // keys outside ascii aren't unicode characters but arbitrary termbox inventions
+  if (event_type == TB_EVENT_KEY) {
+    products.at(0).push_back(/*keycode event*/1);
+    products.at(0).push_back(event.key);
+    products.at(0).push_back(0);
+    products.at(0).push_back(0);
+    products.at(1).push_back(/*found*/true);
+    break;
+  }
+  if (event_type == TB_EVENT_MOUSE) {
+    products.at(0).push_back(/*touch event*/2);
+    products.at(0).push_back(event.key);  // which button, etc.
+    products.at(0).push_back(event.y);  // row
+    products.at(0).push_back(event.x);  // column
+    products.at(1).push_back(/*found*/true);
+    break;
+  }
+  if (event_type == TB_EVENT_RESIZE) {
+    products.at(0).push_back(/*resize event*/3);
+    products.at(0).push_back(event.w);  // width
+    products.at(0).push_back(event.h);  // height
+    products.at(0).push_back(0);
+    products.at(1).push_back(/*found*/true);
+    break;
+  }
+  assert(event_type == 0);
+  products.at(0).push_back(0);
+  products.at(0).push_back(0);
+  products.at(0).push_back(0);
+  products.at(0).push_back(0);
+  products.at(1).push_back(/*found*/false);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+INTERACTIONS_LEFT,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "interactions-left?", INTERACTIONS_LEFT);
+:(before "End Primitive Recipe Checks")
+case INTERACTIONS_LEFT: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case INTERACTIONS_LEFT: {
+  CHECK_CONSOLE;
+  products.resize(1);
+  products.at(0).push_back(tb_event_ready());
+  break;
+}
+
+//: hacks to make text-mode apps more responsive under Unix
+
+:(before "End Primitive Recipe Declarations")
+CLEAR_LINE_ON_DISPLAY,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "clear-line-on-display", CLEAR_LINE_ON_DISPLAY);
+:(before "End Primitive Recipe Checks")
+case CLEAR_LINE_ON_DISPLAY: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CLEAR_LINE_ON_DISPLAY: {
+  CHECK_SCREEN;
+  int width = tb_width();
+  for (int x = Display_column;  x < width;  ++x)
+    tb_print(' ', TB_WHITE, TB_BLACK);
+  tb_set_cursor(Display_column, Display_row);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+CLEAR_DISPLAY_FROM,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "clear-display-from", CLEAR_DISPLAY_FROM);
+:(before "End Primitive Recipe Checks")
+case CLEAR_DISPLAY_FROM: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case CLEAR_DISPLAY_FROM: {
+  CHECK_SCREEN;
+  // todo: error checking
+  int row = ingredients.at(0).at(0);
+  int column = ingredients.at(1).at(0);
+  int left = ingredients.at(2).at(0);
+  int right = ingredients.at(3).at(0);
+  int height=tb_height();
+  for (/*nada*/;  row < height;  ++row, column=left) {  // start column from left in every inner loop except first
+    tb_set_cursor(column, row);
+    for (/*nada*/;  column <= right;  ++column)
+      tb_print(' ', TB_WHITE, TB_BLACK);
+  }
+  tb_set_cursor(Display_column, Display_row);
+  break;
+}
diff --git a/archive/2.vm/081print.mu b/archive/2.vm/081print.mu
new file mode 100644
index 00000000..98ea0f21
--- /dev/null
+++ b/archive/2.vm/081print.mu
@@ -0,0 +1,914 @@
+# Wrappers around print primitives that take a 'screen' object and are thus
+# easier to test.
+#
+# Screen objects are intended to exactly mimic the behavior of traditional
+# terminals. Moving a cursor too far right wraps it to the next line,
+# scrolling if necessary. The details are subtle:
+#
+# a) Rows can take unbounded values. When printing, large values for the row
+# saturate to the bottom row (because scrolling).
+#
+# b) If you print to a square (row, right) on the right margin, the cursor
+# position depends on whether 'row' is in range. If it is, the new cursor
+# position is (row+1, 0). If it isn't, the new cursor position is (row, 0).
+# Because scrolling.
+
+container screen [
+  num-rows:num
+  num-columns:num
+  cursor-row:num
+  cursor-column:num
+  data:&:@:screen-cell  # capacity num-rows*num-columns
+  pending-scroll?:bool
+  top-idx:num  # index inside data that corresponds to top-left of screen
+               # modified on scroll, wrapping around to the top of data
+]
+
+container screen-cell [
+  contents:char
+  color:num
+]
+
+def new-fake-screen w:num, h:num -> result:&:screen [
+  local-scope
+  load-inputs
+  result <- new screen:type
+  non-zero-width?:bool <- greater-than w, 0
+  assert non-zero-width?, [screen can't have zero width]
+  non-zero-height?:bool <- greater-than h, 0
+  assert non-zero-height?, [screen can't have zero height]
+  bufsize:num <- multiply w, h
+  data:&:@:screen-cell <- new screen-cell:type, bufsize
+  *result <- merge h/num-rows, w/num-columns, 0/cursor-row, 0/cursor-column, data, false/pending-scroll?, 0/top-idx
+  result <- clear-screen result
+]
+
+def clear-screen screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [clear-screen]
+  {
+    break-if screen
+    # real screen
+    clear-display
+    return
+  }
+  # fake screen
+  buf:&:@:screen-cell <- get *screen, data:offset
+  max:num <- length *buf
+  i:num <- copy 0
+  {
+    done?:bool <- greater-or-equal i, max
+    break-if done?
+    curr:screen-cell <- merge 0/empty, 7/white
+    *buf <- put-index *buf, i, curr
+    i <- add i, 1
+    loop
+  }
+  # reset cursor
+  *screen <- put *screen, cursor-row:offset, 0
+  *screen <- put *screen, cursor-column:offset, 0
+  *screen <- put *screen, top-idx:offset, 0
+]
+
+def fake-screen-is-empty? screen:&:screen -> result:bool [
+  local-scope
+  load-inputs
+#?   stash [fake-screen-is-empty?]
+  return-unless screen, true  # do nothing for real screens
+  buf:&:@:screen-cell <- get *screen, data:offset
+  i:num <- copy 0
+  len:num <- length *buf
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    curr:screen-cell <- index *buf, i
+    curr-contents:char <- get curr, contents:offset
+    i <- add i, 1
+    loop-unless curr-contents
+    # not 0
+    return false
+  }
+  return true
+]
+
+def print screen:&:screen, c:char -> screen:&:screen [
+  local-scope
+  load-inputs
+  color:num, color-found?:bool <- next-input
+  {
+    # default color to white
+    break-if color-found?
+    color <- copy 7/white
+  }
+  bg-color:num, bg-color-found?:bool <- next-input
+  {
+    # default bg-color to black
+    break-if bg-color-found?
+    bg-color <- copy 0/black
+  }
+  c2:num <- character-to-code c
+  trace 90, [print-character], c2
+  {
+    # real screen
+    break-if screen
+    print-character-to-display c, color, bg-color
+    return
+  }
+  # fake screen
+  # (handle special cases exactly like in the real screen)
+  width:num <- get *screen, num-columns:offset
+  height:num <- get *screen, num-rows:offset
+  capacity:num <- multiply width, height
+  row:num <- get *screen, cursor-row:offset
+  column:num <- get *screen, cursor-column:offset
+  buf:&:@:screen-cell <- get *screen, data:offset
+  # some potentially slow sanity checks for preconditions {
+  # eliminate fractions from column and row
+  row <- round row
+  column <- round column
+  # if cursor is past left margin (error), reset to left margin
+  {
+    too-far-left?:bool <- lesser-than column, 0
+    break-unless too-far-left?
+    column <- copy 0
+    *screen <- put *screen, cursor-column:offset, column
+  }
+  # if cursor is at or past right margin, wrap
+  {
+    at-right?:bool <- greater-or-equal column, width
+    break-unless at-right?
+    column <- copy 0
+    *screen <- put *screen, cursor-column:offset, column
+    row <- add row, 1
+    *screen <- put *screen, cursor-row:offset, row
+  }
+  # }
+  # if there's a pending scroll, perform it
+  {
+    pending-scroll?:bool <- get *screen, pending-scroll?:offset
+    break-unless pending-scroll?
+#?     stash [scroll]
+    scroll-fake-screen screen
+    *screen <- put *screen, pending-scroll?:offset, false
+  }
+#?     $print [print-character (], row, [, ], column, [): ], c, 10/newline
+  # special-case: newline
+  {
+    newline?:bool <- equal c, 10/newline
+    break-unless newline?
+    cursor-down-on-fake-screen screen  # doesn't modify column
+    return
+  }
+  # special-case: linefeed
+  {
+    linefeed?:bool <- equal c, 13/linefeed
+    break-unless linefeed?
+    *screen <- put *screen, cursor-column:offset, 0
+    return
+  }
+  # special-case: backspace
+  # moves cursor left but does not erase
+  {
+    backspace?:bool <- equal c, 8/backspace
+    break-unless backspace?
+    {
+      break-unless column
+      column <- subtract column, 1
+      *screen <- put *screen, cursor-column:offset, column
+    }
+    return
+  }
+  # save character in fake screen
+  top-idx:num <- get *screen, top-idx:offset
+  index:num <- data-index row, column, width, height, top-idx
+  cursor:screen-cell <- merge c, color
+  *buf <- put-index *buf, index, cursor
+  # move cursor to next character, wrapping as necessary
+  # however, don't scroll just yet
+  column <- add column, 1
+  {
+    past-right?:bool <- greater-or-equal column, width
+    break-unless past-right?
+    column <- copy 0
+    row <- add row, 1
+    past-bottom?:bool <- greater-or-equal row, height
+    break-unless past-bottom?
+    # queue up a scroll
+#?     stash [pending scroll]
+    *screen <- put *screen, pending-scroll?:offset, true
+    row <- subtract row, 1  # update cursor as if scroll already happened
+  }
+  *screen <- put *screen, cursor-row:offset, row
+  *screen <- put *screen, cursor-column:offset, column
+]
+
+def cursor-down-on-fake-screen screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [cursor-down]
+  row:num <- get *screen, cursor-row:offset
+  height:num <- get *screen, num-rows:offset
+  bottom:num <- subtract height, 1
+  at-bottom?:bool <- greater-or-equal row, bottom
+  {
+    break-if at-bottom?
+    row <- add row, 1
+    *screen <- put *screen, cursor-row:offset, row
+  }
+  {
+    break-unless at-bottom?
+    scroll-fake-screen screen  # does not modify row
+  }
+]
+
+def scroll-fake-screen screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [scroll-fake-screen]
+  width:num <- get *screen, num-columns:offset
+  height:num <- get *screen, num-rows:offset
+  buf:&:@:screen-cell <- get *screen, data:offset
+  # clear top line and 'rotate' it to the bottom
+  top-idx:num <- get *screen, top-idx:offset  # 0 <= top-idx < len(buf)
+  next-top-idx:num <- add top-idx, width  # 0 <= next-top-idx <= len(buf)
+  empty-cell:screen-cell <- merge 0/empty, 7/white
+  {
+    done?:bool <- greater-or-equal top-idx, next-top-idx
+    break-if done?
+    put-index *buf, top-idx, empty-cell
+    top-idx <- add top-idx, 1
+    # no modulo; top-idx is always a multiple of width,
+    # so it can never wrap around inside this loop
+    loop
+  }
+  # top-idx now same as next-top-idx; wrap around if necessary
+  capacity:num <- multiply width, height
+  _, top-idx <- divide-with-remainder, top-idx, capacity
+  *screen <- put *screen, top-idx:offset, top-idx
+]
+
+# translate from screen (row, column) coordinates to an index into data
+# while accounting for scrolling (sliding top-idx)
+def data-index row:num, column:num, width:num, height:num, top-idx:num -> result:num [
+  local-scope
+  load-inputs
+  {
+    overflow?:bool <- greater-or-equal row, height
+    break-unless overflow?
+    row <- subtract height, 1
+  }
+  result <- multiply width, row
+  result <- add result, column, top-idx
+  capacity:num <- multiply width, height
+  _, result <- divide-with-remainder result, capacity
+]
+
+scenario print-character-at-top-left [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  run [
+    a:char <- copy 97/a
+    fake-screen <- print fake-screen, a:char
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    1:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    1 <- 6  # width*height
+    2 <- 97  # 'a'
+    3 <- 7  # white
+    # rest of screen is empty
+    4 <- 0
+  ]
+]
+
+scenario print-character-at-fractional-coordinate [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  a:char <- copy 97/a
+  run [
+    move-cursor fake-screen, 0.5, 0
+    fake-screen <- print fake-screen, a:char
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    1:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    1 <- 6  # width*height
+    2 <- 97  # 'a'
+    3 <- 7  # white
+    # rest of screen is empty
+    4 <- 0
+  ]
+]
+
+scenario print-character-in-color [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  run [
+    a:char <- copy 97/a
+    fake-screen <- print fake-screen, a:char, 1/red
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    1:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    1 <- 6  # width*height
+    2 <- 97  # 'a'
+    3 <- 1  # red
+    # rest of screen is empty
+    4 <- 0
+  ]
+]
+
+scenario print-backspace-character [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  a:char <- copy 97/a
+  fake-screen <- print fake-screen, a
+  run [
+    backspace:char <- copy 8/backspace
+    fake-screen <- print fake-screen, backspace
+    10:num/raw <- get *fake-screen, cursor-column:offset
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    11:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    10 <- 0  # cursor column
+    11 <- 6  # width*height
+    12 <- 97  # still 'a'
+    13 <- 7  # white
+    # rest of screen is empty
+    14 <- 0
+  ]
+]
+
+scenario print-extra-backspace-character [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  a:char <- copy 97/a
+  fake-screen <- print fake-screen, a
+  run [
+    backspace:char <- copy 8/backspace
+    fake-screen <- print fake-screen, backspace
+    fake-screen <- print fake-screen, backspace  # cursor already at left margin
+    1:num/raw <- get *fake-screen, cursor-column:offset
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    3:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    1 <- 0  # cursor column
+    3 <- 6  # width*height
+    4 <- 97  # still 'a'
+    5 <- 7  # white
+    # rest of screen is empty
+    6 <- 0
+  ]
+]
+
+scenario print-character-at-right-margin [
+  # fill top row of screen with text
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 2/width, 2/height
+  a:char <- copy 97/a
+  fake-screen <- print fake-screen, a
+  b:char <- copy 98/b
+  fake-screen <- print fake-screen, b
+  run [
+    # cursor now at next row
+    c:char <- copy 99/c
+    fake-screen <- print fake-screen, c
+    10:num/raw <- get *fake-screen, cursor-row:offset
+    11:num/raw <- get *fake-screen, cursor-column:offset
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    12:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    10 <- 1  # cursor row
+    11 <- 1  # cursor column
+    12 <- 4  # width*height
+    13 <- 97  # 'a'
+    14 <- 7  # white
+    15 <- 98  # 'b'
+    16 <- 7  # white
+    17 <- 99  # 'c'
+    18 <- 7  # white
+    19 <- 0  # ' '
+    20 <- 7  # white
+  ]
+]
+
+scenario print-newline-character [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  a:char <- copy 97/a
+  fake-screen <- print fake-screen, a
+  run [
+    newline:char <- copy 10/newline
+    fake-screen <- print fake-screen, newline
+    10:num/raw <- get *fake-screen, cursor-row:offset
+    11:num/raw <- get *fake-screen, cursor-column:offset
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    12:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    10 <- 1  # cursor row
+    11 <- 1  # cursor column
+    12 <- 6  # width*height
+    13 <- 97  # 'a'
+    14 <- 7  # white
+    # rest of screen is empty
+    15 <- 0
+  ]
+]
+
+scenario print-newline-at-bottom-line [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  newline:char <- copy 10/newline
+  fake-screen <- print fake-screen, newline
+  fake-screen <- print fake-screen, newline
+  run [
+    # cursor now at bottom of screen
+    fake-screen <- print fake-screen, newline
+    10:num/raw <- get *fake-screen, cursor-row:offset
+    11:num/raw <- get *fake-screen, cursor-column:offset
+  ]
+  # doesn't move further down
+  memory-should-contain [
+    10 <- 1  # cursor row
+    11 <- 0  # cursor column
+  ]
+]
+
+scenario print-character-at-bottom-right [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 2/width, 2/height
+  a:char <- copy 97/a
+  fake-screen <- print fake-screen, a
+  b:char <- copy 98/b
+  fake-screen <- print fake-screen, b
+  c:char <- copy 99/c
+  fake-screen <- print fake-screen, c
+  run [
+    # cursor now at bottom right
+    d:char <- copy 100/d
+    fake-screen <- print fake-screen, d
+    10:num/raw <- get *fake-screen, cursor-row:offset
+    11:num/raw <- get *fake-screen, cursor-column:offset
+    12:num/raw <- get *fake-screen, top-idx:offset
+    13:bool/raw <- get *fake-screen, pending-scroll?:offset
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    20:@:screen-cell/raw <- copy *cell
+  ]
+  # cursor column wraps but the screen doesn't scroll yet
+  memory-should-contain [
+    10 <- 1  # cursor row
+    11 <- 0  # cursor column -- outside screen
+    12 <- 0  # top-idx -- not yet scrolled
+    13 <- 1  # pending-scroll?
+    20 <- 4  # screen size (width*height)
+    21 <- 97  # 'a'
+    22 <- 7  # white
+    23 <- 98  # 'b'
+    24 <- 7  # white
+    25 <- 99 # 'c'
+    26 <- 7  # white
+    27 <- 100  # 'd'
+    28 <- 7  # white
+  ]
+  run [
+    e:char <- copy 101/e
+    print fake-screen, e
+    10:num/raw <- get *fake-screen, cursor-row:offset
+    11:num/raw <- get *fake-screen, cursor-column:offset
+    12:num/raw <- get *fake-screen, top-idx:offset
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    20:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    # text scrolls by 1, we lose the top line
+    10 <- 1  # cursor row
+    11 <- 1  # cursor column -- wrapped
+    12 <- 2  # top-idx -- scrolled
+    20 <- 4  # screen size (width*height)
+    # screen now checked in rotated order
+    25 <- 99 # 'c'
+    26 <- 7  # white
+    27 <- 100  # 'd'
+    28 <- 7  # white
+    # screen wraps; bottom line is cleared of old contents
+    21 <- 101  # 'e'
+    22 <- 7  # white
+    23 <- 0  # unused
+    24 <- 7  # white
+  ]
+]
+
+# even though our screen supports scrolling, some apps may want to avoid
+# scrolling
+# these helpers help check for scrolling at development time
+def save-top-idx screen:&:screen -> result:num [
+  local-scope
+  load-inputs
+  return-unless screen, 0  # check is only for fake screens
+  result <- get *screen, top-idx:offset
+]
+def assert-no-scroll screen:&:screen, old-top-idx:num [
+  local-scope
+  load-inputs
+  return-unless screen
+  new-top-idx:num <- get *screen, top-idx:offset
+  no-scroll?:bool <- equal old-top-idx, new-top-idx
+  assert no-scroll?, [render should never use screen's scrolling capabilities]
+]
+
+def clear-line screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [clear-line]
+  space:char <- copy 0/nul
+  {
+    break-if screen
+    # real screen
+    clear-line-on-display
+    return
+  }
+  # fake screen
+  width:num <- get *screen, num-columns:offset
+  column:num <- get *screen, cursor-column:offset
+  original-column:num <- copy column
+  # space over the entire line
+  {
+    right:num <- subtract width, 1
+    done?:bool <- greater-or-equal column, right
+    break-if done?
+    print screen, space
+    column <- add column, 1
+    loop
+  }
+  # now back to where the cursor was
+  *screen <- put *screen, cursor-column:offset, original-column
+]
+
+# only for non-scrolling apps
+def clear-line-until screen:&:screen, right:num/inclusive -> screen:&:screen [
+  local-scope
+  load-inputs
+  row:num, column:num <- cursor-position screen
+#?   stash [clear-line-until] row column
+  height:num <- screen-height screen
+  past-bottom?:bool <- greater-or-equal row, height
+  return-if past-bottom?
+  space:char <- copy 32/space
+  bg-color:num, bg-color-found?:bool <- next-input
+  {
+    # default bg-color to black
+    break-if bg-color-found?
+    bg-color <- copy 0/black
+  }
+  {
+    done?:bool <- greater-than column, right
+    break-if done?
+    screen <- print screen, space, 7/white, bg-color  # foreground color is mostly unused except if the cursor shows up at this cell
+    column <- add column, 1
+    loop
+  }
+]
+
+def cursor-position screen:&:screen -> row:num, column:num [
+  local-scope
+  load-inputs
+  {
+    break-if screen
+    # real screen
+    row, column <- cursor-position-on-display
+    return
+  }
+  # fake screen
+  row:num <- get *screen, cursor-row:offset
+  column:num <- get *screen, cursor-column:offset
+]
+
+def move-cursor screen:&:screen, new-row:num, new-column:num -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [move-cursor] new-row new-column
+  {
+    break-if screen
+    # real screen
+    move-cursor-on-display new-row, new-column
+    return
+  }
+  # fake screen
+  *screen <- put *screen, cursor-row:offset, new-row
+  *screen <- put *screen, cursor-column:offset, new-column
+  # if cursor column is within bounds, reset 'pending-scroll?'
+  {
+    width:num <- get *screen, num-columns:offset
+    scroll?:bool <- greater-or-equal new-column, width
+    break-if scroll?
+#?     stash [resetting pending-scroll?]
+    *screen <- put *screen, pending-scroll?:offset, false
+  }
+]
+
+scenario clear-line-erases-printed-characters [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  # print a character
+  a:char <- copy 97/a
+  fake-screen <- print fake-screen, a
+  # move cursor to start of line
+  fake-screen <- move-cursor fake-screen, 0/row, 0/column
+  run [
+    fake-screen <- clear-line fake-screen
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    10:@:screen-cell/raw <- copy *cell
+  ]
+  # screen should be blank
+  memory-should-contain [
+    10 <- 6  # width*height
+    11 <- 0
+    12 <- 7
+    13 <- 0
+    14 <- 7
+    15 <- 0
+    16 <- 7
+    17 <- 0
+    18 <- 7
+    19 <- 0
+    20 <- 7
+    21 <- 0
+    22 <- 7
+  ]
+]
+
+def cursor-down screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [cursor-down]
+  {
+    break-if screen
+    # real screen
+    move-cursor-down-on-display
+    return
+  }
+  # fake screen
+  cursor-down-on-fake-screen screen
+]
+
+scenario cursor-down-scrolls [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  # print something to screen and scroll
+  run [
+    print fake-screen, [abc]
+    cursor-to-next-line fake-screen
+    cursor-to-next-line fake-screen
+    data:&:@:screen-cell <- get *fake-screen, data:offset
+    10:@:screen-cell/raw <- copy *data
+  ]
+  # screen is now blank
+  memory-should-contain [
+    10 <- 6  # width*height
+    11 <- 0
+    12 <- 7  # white
+    13 <- 0
+    14 <- 7  # white
+    15 <- 0
+    16 <- 7  # white
+    17 <- 0
+    18 <- 7  # white
+    19 <- 0
+    20 <- 7  # white
+    21 <- 0
+    22 <- 7  # white
+  ]
+]
+
+def cursor-up screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [cursor-up]
+  {
+    break-if screen
+    # real screen
+    move-cursor-up-on-display
+    return
+  }
+  # fake screen
+  row:num <- get *screen, cursor-row:offset
+  at-top?:bool <- lesser-or-equal row, 0
+  return-if at-top?
+  row <- subtract row, 1
+  *screen <- put *screen, cursor-row:offset, row
+]
+
+def cursor-right screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [cursor-right]
+  {
+    break-if screen
+    # real screen
+    move-cursor-right-on-display
+    return
+  }
+  # fake screen
+  width:num <- get *screen, num-columns:offset
+  column:num <- get *screen, cursor-column:offset
+  max:num <- subtract width, 1
+  at-bottom?:bool <- greater-or-equal column, max
+  return-if at-bottom?
+  column <- add column, 1
+  *screen <- put *screen, cursor-column:offset, column
+]
+
+def cursor-left screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [cursor-left]
+  {
+    break-if screen
+    # real screen
+    move-cursor-left-on-display
+    return
+  }
+  # fake screen
+  column:num <- get *screen, cursor-column:offset
+  at-top?:bool <- lesser-or-equal column, 0
+  return-if at-top?
+  column <- subtract column, 1
+  *screen <- put *screen, cursor-column:offset, column
+]
+
+def cursor-to-start-of-line screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [cursor-to-start-of-line]
+  row:num <- cursor-position screen
+  screen <- move-cursor screen, row, 0/column
+]
+
+def cursor-to-next-line screen:&:screen -> screen:&:screen [
+  local-scope
+  load-inputs
+#?   stash [cursor-to-next-line]
+  screen <- cursor-down screen
+  screen <- cursor-to-start-of-line screen
+]
+
+def move-cursor-to-column screen:&:screen, column:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  row:num, _ <- cursor-position screen
+#?   stash [move-cursor-to-column] row
+  move-cursor screen, row, column
+]
+
+def screen-width screen:&:screen -> width:num [
+  local-scope
+  load-inputs
+#?   stash [screen-width]
+  {
+    break-unless screen
+    # fake screen
+    width <- get *screen, num-columns:offset
+    return
+  }
+  # real screen
+  width <- display-width
+]
+
+def screen-height screen:&:screen -> height:num [
+  local-scope
+  load-inputs
+#?   stash [screen-height]
+  {
+    break-unless screen
+    # fake screen
+    height <- get *screen, num-rows:offset
+    return
+  }
+  # real screen
+  height <- display-height
+]
+
+def print screen:&:screen, s:text -> screen:&:screen [
+  local-scope
+  load-inputs
+  color:num, color-found?:bool <- next-input
+  {
+    # default color to white
+    break-if color-found?
+    color <- copy 7/white
+  }
+  bg-color:num, bg-color-found?:bool <- next-input
+  {
+    # default bg-color to black
+    break-if bg-color-found?
+    bg-color <- copy 0/black
+  }
+  len:num <- length *s
+  i:num <- copy 0
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    c:char <- index *s, i
+    print screen, c, color, bg-color
+    i <- add i, 1
+    loop
+  }
+]
+
+scenario print-text-wraps-past-right-margin [
+  local-scope
+  fake-screen:&:screen <- new-fake-screen 3/width, 2/height
+  run [
+    fake-screen <- print fake-screen, [abcd]
+    5:num/raw <- get *fake-screen, cursor-row:offset
+    6:num/raw <- get *fake-screen, cursor-column:offset
+    7:num/raw <- get *fake-screen, top-idx:offset
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    10:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    5 <- 1  # cursor-row
+    6 <- 1  # cursor-column
+    7 <- 0  # top-idx
+    10 <- 6  # width*height
+    11 <- 97  # 'a'
+    12 <- 7  # white
+    13 <- 98  # 'b'
+    14 <- 7  # white
+    15 <- 99  # 'c'
+    16 <- 7  # white
+    17 <- 100  # 'd'
+    18 <- 7  # white
+    # rest of screen is empty
+    19 <- 0
+  ]
+]
+
+def print screen:&:screen, n:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  color:num, color-found?:bool <- next-input
+  {
+    # default color to white
+    break-if color-found?
+    color <- copy 7/white
+  }
+  bg-color:num, bg-color-found?:bool <- next-input
+  {
+    # default bg-color to black
+    break-if bg-color-found?
+    bg-color <- copy 0/black
+  }
+  # todo: other bases besides decimal
+  s:text <- to-text n
+  screen <- print screen, s, color, bg-color
+]
+
+def print screen:&:screen, n:bool -> screen:&:screen [
+  local-scope
+  load-inputs
+  color:num, color-found?:bool <- next-input
+  {
+    # default color to white
+    break-if color-found?
+    color <- copy 7/white
+  }
+  bg-color:num, bg-color-found?:bool <- next-input
+  {
+    # default bg-color to black
+    break-if bg-color-found?
+    bg-color <- copy 0/black
+  }
+  {
+    break-if n
+    screen <- print screen, [false], color, bg-color
+  }
+  {
+    break-unless n
+    screen <- print screen, [true], color, bg-color
+  }
+]
+
+def print screen:&:screen, n:&:_elem -> screen:&:screen [
+  local-scope
+  load-inputs
+  color:num, color-found?:bool <- next-input
+  {
+    # default color to white
+    break-if color-found?
+    color <- copy 7/white
+  }
+  bg-color:num, bg-color-found?:bool <- next-input
+  {
+    # default bg-color to black
+    break-if bg-color-found?
+    bg-color <- copy 0/black
+  }
+  n2:num <- deaddress n
+  screen <- print screen, n2, color, bg-color
+]
diff --git a/archive/2.vm/082scenario_screen.cc b/archive/2.vm/082scenario_screen.cc
new file mode 100644
index 00000000..39ba76e7
--- /dev/null
+++ b/archive/2.vm/082scenario_screen.cc
@@ -0,0 +1,458 @@
+//: Clean syntax to manipulate and check the screen in scenarios.
+//: Instructions 'assume-screen' and 'screen-should-contain' implicitly create
+//: a variable called 'screen' that is accessible to later instructions in the
+//: scenario. 'screen-should-contain' can check unicode characters in the fake
+//: screen
+
+//: first make sure we don't mangle these instructions in other transforms
+:(before "End initialize_transform_rewrite_literal_string_to_text()")
+recipes_taking_literal_strings.insert("screen-should-contain");
+recipes_taking_literal_strings.insert("screen-should-contain-in-color");
+
+:(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
+}
+
+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
+}
+
+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
+}
+
+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"
+  );
+}
+
+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"
+  );
+}
+
+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
+//: single fixed location for the per-scenario screen, keyboard, file system,
+//: etc. Carve space for these fixed locations out of the reserved-for-test
+//: locations.
+
+:(before "End Globals")
+extern const int Max_variables_in_scenarios = Reserved_for_tests-100;
+int Next_predefined_global_for_scenarios = Max_variables_in_scenarios;
+:(before "End Reset")
+assert(Next_predefined_global_for_scenarios < Reserved_for_tests);
+
+:(before "End Globals")
+// Scenario Globals.
+extern const int SCREEN = next_predefined_global_for_scenarios(/*size_of(address:screen)*/2);
+// End Scenario Globals.
+:(code)
+int next_predefined_global_for_scenarios(int size) {
+  int result = Next_predefined_global_for_scenarios;
+  Next_predefined_global_for_scenarios += size;
+  return result;
+}
+
+//: give 'screen' a fixed location in scenarios
+:(before "End Special Scenario Variable Names(r)")
+Name[r]["screen"] = SCREEN;
+//: make 'screen' always a raw location in scenarios
+:(before "End is_special_name Special-cases")
+if (s == "screen") return true;
+
+:(before "End Rewrite Instruction(curr, recipe result)")
+// rewrite 'assume-screen width, height' to
+// 'screen:&:screen <- new-fake-screen width, height'
+if (curr.name == "assume-screen") {
+  curr.name = "new-fake-screen";
+  if (!curr.products.empty()) {
+    raise << result.name << ": 'assume-screen' has no products\n" << end();
+  }
+  else if (!starts_with(result.name, "scenario_")) {
+    raise << result.name << ": 'assume-screen' can't be called here, only in scenarios\n" << end();
+  }
+  else {
+    assert(curr.products.empty());
+    curr.products.push_back(reagent("screen:&:screen/raw"));
+    curr.products.at(0).set_value(SCREEN);
+  }
+}
+
+:(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")
+SCREEN_SHOULD_CONTAIN,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "screen-should-contain", SCREEN_SHOULD_CONTAIN);
+:(before "End Primitive Recipe Checks")
+case SCREEN_SHOULD_CONTAIN: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'screen-should-contain' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_literal_text(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'screen-should-contain' should be a literal string, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SCREEN_SHOULD_CONTAIN: {
+  if (!Passed) break;
+  assert(scalar(ingredients.at(0)));
+  check_screen(current_instruction().ingredients.at(0).name, -1);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+SCREEN_SHOULD_CONTAIN_IN_COLOR,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "screen-should-contain-in-color", SCREEN_SHOULD_CONTAIN_IN_COLOR);
+:(before "End Primitive Recipe Checks")
+case SCREEN_SHOULD_CONTAIN_IN_COLOR: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'screen-should-contain-in-color' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'screen-should-contain-in-color' should be a number (color code), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  if (!is_literal_text(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of 'screen-should-contain-in-color' should be a literal string, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SCREEN_SHOULD_CONTAIN_IN_COLOR: {
+  if (!Passed) break;
+  assert(scalar(ingredients.at(0)));
+  assert(scalar(ingredients.at(1)));
+  check_screen(current_instruction().ingredients.at(1).name, ingredients.at(0).at(0));
+  break;
+}
+
+:(before "End Types")
+// scan an array of characters in a unicode-aware, bounds-checked manner
+struct raw_string_stream {
+  int index;
+  const int max;
+  const char* buf;
+
+  raw_string_stream(const string&);
+  uint32_t get();  // unicode codepoint
+  uint32_t peek();  // unicode codepoint
+  bool at_end() const;
+  void skip_whitespace_and_comments();
+};
+
+:(code)
+void check_screen(const string& expected_contents, const int color) {
+  int screen_location = get_or_insert(Memory, SCREEN+/*skip address alloc id*/1) + /*skip payload alloc id*/1;
+  reagent screen("x:screen");  // just to ensure screen.type is reclaimed
+  int screen_data_location = find_element_location(screen_location, "data", screen.type, "check_screen");  // type: address:array:character
+  assert(screen_data_location >= 0);
+//?   cerr << "screen data is at location " << screen_data_location << '\n';
+  int screen_data_start = get_or_insert(Memory, screen_data_location+/*skip address alloc id*/1) + /*skip payload alloc id*/1;  // type: array:character
+//?   cerr << "screen data start is at " << screen_data_start << '\n';
+  int screen_width_location = find_element_location(screen_location, "num-columns", screen.type, "check_screen");
+//?   cerr << "screen width is at location " << screen_width_location << '\n';
+  int screen_width = get_or_insert(Memory, screen_width_location);
+//?   cerr << "screen width: " << screen_width << '\n';
+  int screen_height_location = find_element_location(screen_location, "num-rows", screen.type, "check_screen");
+//?   cerr << "screen height is at location " << screen_height_location << '\n';
+  int screen_height = get_or_insert(Memory, screen_height_location);
+//?   cerr << "screen height: " << screen_height << '\n';
+  int top_index_location= find_element_location(screen_location, "top-idx", screen.type, "check_screen");
+//?   cerr << "top of screen is at location " << top_index_location << '\n';
+  int top_index = get_or_insert(Memory, top_index_location);
+//?   cerr << "top of screen is index " << top_index << '\n';
+  raw_string_stream cursor(expected_contents);
+  // todo: too-long expected_contents should fail
+  for (int i=0, row=top_index/screen_width;  i < screen_height;  ++i, row=(row+1)%screen_height) {
+    cursor.skip_whitespace_and_comments();
+    if (cursor.at_end()) break;
+    if (cursor.get() != '.') {
+      raise << maybe(current_recipe_name()) << "each row of the expected screen should start with a '.'\n" << end();
+      if (!Scenario_testing_scenario) Passed = false;
+      return;
+    }
+    int addr = screen_data_start+/*length*/1+row*screen_width* /*size of screen-cell*/2;
+    for (int column = 0;  column < screen_width;  ++column, addr+= /*size of screen-cell*/2) {
+      const int cell_color_offset = 1;
+      uint32_t curr = cursor.get();
+      if (get_or_insert(Memory, addr) == 0 && isspace(curr)) continue;
+      if (curr == ' ' && color != -1 && color != get_or_insert(Memory, addr+cell_color_offset)) {
+        // filter out other colors
+        continue;
+      }
+      if (get_or_insert(Memory, addr) != 0 && get_or_insert(Memory, addr) == curr) {
+        if (color == -1 || color == get_or_insert(Memory, addr+cell_color_offset)) continue;
+        // contents match but color is off
+        if (!Hide_errors) cerr << '\n';
+        raise << "F - " << maybe(current_recipe_name()) << "expected screen location (" << row << ", " << column << ") to contain '" << unicode_character_at(addr) << "' in color " << color << " instead of " << no_scientific(get_or_insert(Memory, addr+cell_color_offset)) << "\n" << end();
+        if (!Hide_errors) dump_screen();
+        if (!Scenario_testing_scenario) Passed = false;
+        return;
+      }
+
+      // really a mismatch
+      // can't print multi-byte unicode characters in errors just yet. not very useful for debugging anyway.
+      char expected_pretty[10] = {0};
+      if (curr < 256 && !iscntrl(curr)) {
+        // " ('<curr>')"
+        expected_pretty[0] = ' ', expected_pretty[1] = '(', expected_pretty[2] = '\'', expected_pretty[3] = static_cast<unsigned char>(curr), expected_pretty[4] = '\'', expected_pretty[5] = ')', expected_pretty[6] = '\0';
+      }
+      char actual_pretty[10] = {0};
+      if (get_or_insert(Memory, addr) < 256 && !iscntrl(get_or_insert(Memory, addr))) {
+        // " ('<curr>')"
+        actual_pretty[0] = ' ', actual_pretty[1] = '(', actual_pretty[2] = '\'', actual_pretty[3] = static_cast<unsigned char>(get_or_insert(Memory, addr)), actual_pretty[4] = '\'', actual_pretty[5] = ')', actual_pretty[6] = '\0';
+      }
+
+      ostringstream color_phrase;
+      if (color != -1) color_phrase << " in color " << color;
+      if (!Hide_errors) cerr << '\n';
+      raise << "F - " << maybe(current_recipe_name()) << "expected screen location (" << row << ", " << column << ") to contain " << curr << expected_pretty << color_phrase.str() << " instead of " << no_scientific(get_or_insert(Memory, addr)) << actual_pretty << '\n' << end();
+      if (!Hide_errors) dump_screen();
+      if (!Scenario_testing_scenario) Passed = false;
+      return;
+    }
+    if (cursor.get() != '.') {
+      raise << maybe(current_recipe_name()) << "row " << row << " of the expected screen is too long\n" << end();
+      if (!Scenario_testing_scenario) Passed = false;
+      return;
+    }
+  }
+  cursor.skip_whitespace_and_comments();
+  if (!cursor.at_end()) {
+    raise << maybe(current_recipe_name()) << "expected screen has too many rows\n" << end();
+    Passed = false;
+  }
+}
+
+const char* unicode_character_at(int addr) {
+  int unicode_code_point = static_cast<int>(get_or_insert(Memory, addr));
+  return to_unicode(unicode_code_point);
+}
+
+raw_string_stream::raw_string_stream(const string& backing) :index(0), max(SIZE(backing)), buf(backing.c_str()) {}
+
+bool raw_string_stream::at_end() const {
+  if (index >= max) return true;
+  if (tb_utf8_char_length(buf[index]) > max-index) {
+    raise << "unicode string seems corrupted at index "<< index << " character " << static_cast<int>(buf[index]) << '\n' << end();
+    return true;
+  }
+  return false;
+}
+
+uint32_t raw_string_stream::get() {
+  assert(index < max);  // caller must check bounds before calling 'get'
+  uint32_t result = 0;
+  int length = tb_utf8_char_to_unicode(&result, &buf[index]);
+  assert(length != TB_EOF);
+  index += length;
+  return result;
+}
+
+uint32_t raw_string_stream::peek() {
+  assert(index < max);  // caller must check bounds before calling 'get'
+  uint32_t result = 0;
+  int length = tb_utf8_char_to_unicode(&result, &buf[index]);
+  assert(length != TB_EOF);
+  return result;
+}
+
+void raw_string_stream::skip_whitespace_and_comments() {
+  while (!at_end()) {
+    if (isspace(peek())) get();
+    else if (peek() == '#') {
+      // skip comment
+      get();
+      while (peek() != '\n') get();  // implicitly also handles CRLF
+    }
+    else break;
+  }
+}
+
+:(before "End Primitive Recipe Declarations")
+_DUMP_SCREEN,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$dump-screen", _DUMP_SCREEN);
+:(before "End Primitive Recipe Checks")
+case _DUMP_SCREEN: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _DUMP_SCREEN: {
+  dump_screen();
+  break;
+}
+
+:(code)
+void dump_screen() {
+  int screen_location = get_or_insert(Memory, SCREEN+/*skip address alloc id*/1) + /*skip payload alloc id*/1;
+  reagent screen("x:screen");  // just to ensure screen.type is reclaimed
+  int screen_data_location = find_element_location(screen_location, "data", screen.type, "check_screen");  // type: address:array:character
+  assert(screen_data_location >= 0);
+//?   cerr << "screen data is at location " << screen_data_location << '\n';
+  int screen_data_start = get_or_insert(Memory, screen_data_location+/*skip address alloc id*/1) + /*skip payload alloc id*/1;  // type: array:character
+//?   cerr << "screen data start is at " << screen_data_start << '\n';
+  int screen_width_location = find_element_location(screen_location, "num-columns", screen.type, "check_screen");
+//?   cerr << "screen width is at location " << screen_width_location << '\n';
+  int screen_width = get_or_insert(Memory, screen_width_location);
+//?   cerr << "screen width: " << screen_width << '\n';
+  int screen_height_location = find_element_location(screen_location, "num-rows", screen.type, "check_screen");
+//?   cerr << "screen height is at location " << screen_height_location << '\n';
+  int screen_height = get_or_insert(Memory, screen_height_location);
+//?   cerr << "screen height: " << screen_height << '\n';
+  int top_index_location= find_element_location(screen_location, "top-idx", screen.type, "check_screen");
+//?   cerr << "top of screen is at location " << top_index_location << '\n';
+  int top_index = get_or_insert(Memory, top_index_location);
+//?   cerr << "top of screen is index " << top_index << '\n';
+  for (int i=0, row=top_index/screen_width;  i < screen_height;  ++i, row=(row+1)%screen_height) {
+    cerr << '.';
+    int curr = screen_data_start+/*length*/1+row*screen_width* /*size of screen-cell*/2;
+    for (int col = 0;  col < screen_width;  ++col) {
+      if (get_or_insert(Memory, curr))
+        cerr << to_unicode(static_cast<uint32_t>(get_or_insert(Memory, curr)));
+      else
+        cerr << ' ';
+      curr += /*size of screen-cell*/2;
+    }
+    cerr << ".\n";
+  }
+}
diff --git a/archive/2.vm/083scenario_screen_test.mu b/archive/2.vm/083scenario_screen_test.mu
new file mode 100644
index 00000000..b4ac6e5e
--- /dev/null
+++ b/archive/2.vm/083scenario_screen_test.mu
@@ -0,0 +1,47 @@
+# To check our support for screens in scenarios, rewrite tests from print.mu
+
+scenario print-character-at-top-left-2 [
+  local-scope
+  assume-screen 3/width, 2/height
+  run [
+    a:char <- copy 97/a
+    screen <- print screen, a
+  ]
+  screen-should-contain [
+    .a  .
+    .   .
+  ]
+]
+
+scenario clear-line-erases-printed-characters-2 [
+  local-scope
+  assume-screen 5/width, 3/height
+  # print a character
+  a:char <- copy 97/a
+  screen <- print screen, a
+  # move cursor to start of line
+  screen <- move-cursor screen, 0/row, 0/column
+  run [
+    screen <- clear-line screen
+  ]
+  screen-should-contain [
+    .     .
+    .     .
+    .     .
+  ]
+]
+
+scenario scroll-screen [
+  local-scope
+  assume-screen 3/width, 2/height
+  run [
+    a:char <- copy 97/a
+    move-cursor screen, 1/row, 2/column
+    screen <- print screen, a
+    screen <- print screen, a
+  ]
+  screen-should-contain [
+    .  a.
+    .a  .
+  ]
+]
diff --git a/archive/2.vm/084console.mu b/archive/2.vm/084console.mu
new file mode 100644
index 00000000..bd18226a
--- /dev/null
+++ b/archive/2.vm/084console.mu
@@ -0,0 +1,104 @@
+# Wrappers around interaction primitives that take a potentially fake object
+# and are thus easier to test.
+
+exclusive-container event [
+  text:char
+  keycode:num  # keys on keyboard without a unicode representation
+  touch:touch-event  # mouse, track ball, etc.
+  resize:resize-event
+  # update the assume-console handler if you add more variants
+]
+
+container touch-event [
+  type:num
+  row:num
+  column:num
+]
+
+container resize-event [
+  width:num
+  height:num
+]
+
+container console [
+  current-event-index:num
+  events:&:@:event
+]
+
+def new-fake-console events:&:@:event -> result:&:console [
+  local-scope
+  load-inputs
+  result:&:console <- new console:type
+  *result <- put *result, events:offset, events
+]
+
+def read-event console:&:console -> result:event, found?:bool, quit?:bool, console:&:console [
+  local-scope
+  load-inputs
+  {
+    break-unless console
+    current-event-index:num <- get *console, current-event-index:offset
+    buf:&:@:event <- get *console, events:offset
+    {
+      max:num <- length *buf
+      done?:bool <- greater-or-equal current-event-index, max
+      break-unless done?
+      dummy:&:event <- new event:type
+      return *dummy, true/found, true/quit
+    }
+    result <- index *buf, current-event-index
+    current-event-index <- add current-event-index, 1
+    *console <- put *console, current-event-index:offset, current-event-index
+    return result, true/found, false/quit
+  }
+  switch  # real event source is infrequent; avoid polling it too much
+  result:event, found?:bool <- check-for-interaction
+  return result, found?, false/quit
+]
+
+# variant of read-event for just keyboard events. Discards everything that
+# isn't unicode, so no arrow keys, page-up/page-down, etc. But you still get
+# newlines, tabs, ctrl-d..
+def read-key console:&:console -> result:char, found?:bool, quit?:bool, console:&:console [
+  local-scope
+  load-inputs
+  x:event, found?:bool, quit?:bool, console <- read-event console
+  return-if quit?, 0, found?, quit?
+  return-unless found?, 0, found?, quit?
+  c:char, converted?:bool <- maybe-convert x, text:variant
+  return-unless converted?, 0, false/found, false/quit
+  return c, true/found, false/quit
+]
+
+def send-keys-to-channel console:&:console, chan:&:sink:char, screen:&:screen -> console:&:console, chan:&:sink:char, screen:&:screen [
+  local-scope
+  load-inputs
+  {
+    c:char, found?:bool, quit?:bool, console <- read-key console
+    loop-unless found?
+    break-if quit?
+    assert c, [invalid event, expected text]
+    screen <- print screen, c
+    chan <- write chan, c
+    loop
+  }
+  chan <- close chan
+]
+
+def wait-for-event console:&:console -> console:&:console [
+  local-scope
+  load-inputs
+  {
+    _, found?:bool <- read-event console
+    break-if found?
+    switch
+    loop
+  }
+]
+
+def has-more-events? console:&:console -> result:bool [
+  local-scope
+  load-inputs
+  return-if console, false  # fake events are processed as soon as they arrive
+  result <- interactions-left?
+]
diff --git a/archive/2.vm/085scenario_console.cc b/archive/2.vm/085scenario_console.cc
new file mode 100644
index 00000000..75c2a289
--- /dev/null
+++ b/archive/2.vm/085scenario_console.cc
@@ -0,0 +1,317 @@
+//: Clean syntax to manipulate and check the console in scenarios.
+//: Instruction 'assume-console' implicitly creates a variable called
+//: 'console' that is accessible inside other 'run' instructions in the
+//: scenario. Like with the fake screen, 'assume-console' transparently
+//: supports unicode.
+
+//: first make sure we don't mangle this instruction in other transforms
+:(before "End initialize_transform_rewrite_literal_string_to_text()")
+recipes_taking_literal_strings.insert("assume-console");
+
+:(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);
+//: give 'console' a fixed location in scenarios
+:(before "End Special Scenario Variable Names(r)")
+Name[r]["console"] = CONSOLE;
+//: make 'console' always a raw location in scenarios
+:(before "End is_special_name Special-cases")
+if (s == "console") return true;
+
+:(before "End Primitive Recipe Declarations")
+ASSUME_CONSOLE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "assume-console", ASSUME_CONSOLE);
+:(before "End Primitive Recipe Checks")
+case ASSUME_CONSOLE: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case ASSUME_CONSOLE: {
+  // create a temporary recipe just for parsing; it won't contain valid instructions
+  istringstream in("[" + current_instruction().ingredients.at(0).name + "]");
+  recipe r;
+  slurp_body(in, r);
+  int num_events = count_events(r);
+  // initialize the events like in new-fake-console
+  int size = /*length*/1 + num_events*size_of_event();
+  int event_data_address = allocate(size);
+  // store length
+  put(Memory, event_data_address+/*skip alloc id*/1, num_events);
+  int curr_address = event_data_address + /*skip alloc id*/1 + /*skip length*/1;
+  for (int i = 0;  i < SIZE(r.steps);  ++i) {
+    const instruction& inst = r.steps.at(i);
+    if (inst.name == "left-click") {
+      trace(Callstack_depth+1, "mem") << "storing 'left-click' event starting at " << Current_routine->alloc << end();
+      put(Memory, curr_address, /*tag for 'touch-event' variant of 'event' exclusive-container*/2);
+      put(Memory, curr_address+/*skip tag*/1+/*offset of 'type' in 'mouse-event'*/0, TB_KEY_MOUSE_LEFT);
+      put(Memory, curr_address+/*skip tag*/1+/*offset of 'row' in 'mouse-event'*/1, to_integer(inst.ingredients.at(0).name));
+      put(Memory, curr_address+/*skip tag*/1+/*offset of 'column' in 'mouse-event'*/2, to_integer(inst.ingredients.at(1).name));
+      curr_address += size_of_event();
+    }
+    else if (inst.name == "press") {
+      trace(Callstack_depth+1, "mem") << "storing 'press' event starting at " << curr_address << end();
+      string key = inst.ingredients.at(0).name;
+      if (is_integer(key))
+        put(Memory, curr_address+1, to_integer(key));
+      else if (contains_key(Key, key))
+        put(Memory, curr_address+1, Key[key]);
+      else
+        raise << "assume-console: can't press '" << key << "'\n" << end();
+      if (get_or_insert(Memory, curr_address+1) < 256)
+        // these keys are in ascii
+        put(Memory, curr_address, /*tag for 'text' variant of 'event' exclusive-container*/0);
+      else {
+        // distinguish from unicode
+        put(Memory, curr_address, /*tag for 'keycode' variant of 'event' exclusive-container*/1);
+      }
+      curr_address += size_of_event();
+    }
+    // End Event Handlers
+    else {
+      // keyboard input
+      assert(inst.name == "type");
+      trace(Callstack_depth+1, "mem") << "storing 'type' event starting at " << curr_address << end();
+      const string& contents = inst.ingredients.at(0).name;
+      const char* raw_contents = contents.c_str();
+      int num_keyboard_events = unicode_length(contents);
+      int curr = 0;
+      for (int i = 0;  i < num_keyboard_events;  ++i) {
+        trace(Callstack_depth+1, "mem") << "storing 'text' tag at " << curr_address << end();
+        put(Memory, curr_address, /*tag for 'text' variant of 'event' exclusive-container*/0);
+        uint32_t curr_character;
+        assert(curr < SIZE(contents));
+        tb_utf8_char_to_unicode(&curr_character, &raw_contents[curr]);
+        trace(Callstack_depth+1, "mem") << "storing character " << curr_character << " at " << curr_address+/*skip exclusive container tag*/1 << end();
+        put(Memory, curr_address+/*skip exclusive container tag*/1, curr_character);
+        curr += tb_utf8_char_length(raw_contents[curr]);
+        curr_address += size_of_event();
+      }
+    }
+  }
+  assert(curr_address == event_data_address+/*skip alloc id*/1+size);
+  // wrap the array of events in a console object
+  int console_address = allocate(size_of_console());
+  trace(Callstack_depth+1, "mem") << "storing console in " << console_address << end();
+  put(Memory, CONSOLE+/*skip alloc id*/1, console_address);
+  trace(Callstack_depth+1, "mem") << "storing console data in " << console_address+/*offset of 'data' in container 'events'*/1 << end();
+  put(Memory, console_address+/*skip alloc id*/1+/*offset of 'data' in container 'events'*/1+/*skip alloc id of 'data'*/1, event_data_address);
+  break;
+}
+
+:(before "End Globals")
+map<string, int> Key;
+:(before "End One-time Setup")
+initialize_key_names();
+:(code)
+void initialize_key_names() {
+  Key["F1"] = TB_KEY_F1;
+  Key["F2"] = TB_KEY_F2;
+  Key["F3"] = TB_KEY_F3;
+  Key["F4"] = TB_KEY_F4;
+  Key["F5"] = TB_KEY_F5;
+  Key["F6"] = TB_KEY_F6;
+  Key["F7"] = TB_KEY_F7;
+  Key["F8"] = TB_KEY_F8;
+  Key["F9"] = TB_KEY_F9;
+  Key["F10"] = TB_KEY_F10;
+  Key["F11"] = TB_KEY_F11;
+  Key["F12"] = TB_KEY_F12;
+  Key["insert"] = TB_KEY_INSERT;
+  Key["delete"] = TB_KEY_DELETE;
+  Key["home"] = TB_KEY_HOME;
+  Key["end"] = TB_KEY_END;
+  Key["page-up"] = TB_KEY_PGUP;
+  Key["page-down"] = TB_KEY_PGDN;
+  Key["up-arrow"] = TB_KEY_ARROW_UP;
+  Key["down-arrow"] = TB_KEY_ARROW_DOWN;
+  Key["left-arrow"] = TB_KEY_ARROW_LEFT;
+  Key["right-arrow"] = TB_KEY_ARROW_RIGHT;
+  Key["ctrl-a"] = TB_KEY_CTRL_A;
+  Key["ctrl-b"] = TB_KEY_CTRL_B;
+  Key["ctrl-c"] = TB_KEY_CTRL_C;
+  Key["ctrl-d"] = TB_KEY_CTRL_D;
+  Key["ctrl-e"] = TB_KEY_CTRL_E;
+  Key["ctrl-f"] = TB_KEY_CTRL_F;
+  Key["ctrl-g"] = TB_KEY_CTRL_G;
+  Key["backspace"] = TB_KEY_BACKSPACE;
+  Key["ctrl-h"] = TB_KEY_CTRL_H;
+  Key["tab"] = TB_KEY_TAB;
+  Key["ctrl-i"] = TB_KEY_CTRL_I;
+  Key["ctrl-j"] = TB_KEY_CTRL_J;
+  Key["enter"] = TB_KEY_NEWLINE;  // ignore CR/LF distinction; there is only 'enter'
+  Key["ctrl-k"] = TB_KEY_CTRL_K;
+  Key["ctrl-l"] = TB_KEY_CTRL_L;
+  Key["ctrl-m"] = TB_KEY_CTRL_M;
+  Key["ctrl-n"] = TB_KEY_CTRL_N;
+  Key["ctrl-o"] = TB_KEY_CTRL_O;
+  Key["ctrl-p"] = TB_KEY_CTRL_P;
+  Key["ctrl-q"] = TB_KEY_CTRL_Q;
+  Key["ctrl-r"] = TB_KEY_CTRL_R;
+  Key["ctrl-s"] = TB_KEY_CTRL_S;
+  Key["ctrl-t"] = TB_KEY_CTRL_T;
+  Key["ctrl-u"] = TB_KEY_CTRL_U;
+  Key["ctrl-v"] = TB_KEY_CTRL_V;
+  Key["ctrl-w"] = TB_KEY_CTRL_W;
+  Key["ctrl-x"] = TB_KEY_CTRL_X;
+  Key["ctrl-y"] = TB_KEY_CTRL_Y;
+  Key["ctrl-z"] = TB_KEY_CTRL_Z;
+  Key["escape"] = TB_KEY_ESC;
+  Key["ctrl-slash"] = TB_KEY_CTRL_SLASH;
+}
+
+:(after "Begin check_or_set_invalid_types(r)")
+if (is_scenario(caller))
+  initialize_special_name(r);
+:(code)
+bool is_scenario(const recipe& caller) {
+  return starts_with(caller.name, "scenario_");
+}
+void initialize_special_name(reagent& r) {
+  if (r.type) return;
+  // no need for screen
+  if (r.name == "console") r.type = new_type_tree("address:console");
+  // End Initialize Type Of Special Name In Scenario(r)
+}
+
+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.
+:(before "End Primitive Recipe Declarations")
+REPLACE_IN_CONSOLE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "replace-in-console", REPLACE_IN_CONSOLE);
+:(before "End Primitive Recipe Checks")
+case REPLACE_IN_CONSOLE: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case REPLACE_IN_CONSOLE: {
+  assert(scalar(ingredients.at(0)));
+  if (!get_or_insert(Memory, CONSOLE)) {
+    raise << "console not initialized\n" << end();
+    break;
+  }
+  int console_address = get_or_insert(Memory, CONSOLE);
+  int console_data = get_or_insert(Memory, console_address+1);
+  int length = get_or_insert(Memory, console_data);  // array length
+  for (int i = 0, curr = console_data+1;  i < length;  ++i, curr+=size_of_event()) {
+    if (get_or_insert(Memory, curr) != /*text*/0) continue;
+    if (get_or_insert(Memory, curr+1) != ingredients.at(0).at(0)) continue;
+    for (int n = 0;  n < size_of_event();  ++n)
+      put(Memory, curr+n, ingredients.at(1).at(n));
+  }
+  break;
+}
+
+:(code)
+int count_events(const recipe& r) {
+  int result = 0;
+  for (int i = 0;  i < SIZE(r.steps);  ++i) {
+    const instruction& curr = r.steps.at(i);
+    if (curr.name == "type")
+      result += unicode_length(curr.ingredients.at(0).name);
+    else
+      ++result;
+  }
+  return result;
+}
+
+int size_of_event() {
+  // memoize result if already computed
+  static int result = 0;
+  if (result) return result;
+  type_tree* type = new type_tree("event");
+  result = size_of(type);
+  delete type;
+  return result;
+}
+
+int size_of_console() {
+  // memoize result if already computed
+  static int result = 0;
+  if (result) return result;
+  assert(get(Type_ordinal, "console"));
+  type_tree* type = new type_tree("console");
+  result = size_of(type);
+  delete type;
+  return result;
+}
diff --git a/archive/2.vm/086scenario_console_test.mu b/archive/2.vm/086scenario_console_test.mu
new file mode 100644
index 00000000..f5aa1438
--- /dev/null
+++ b/archive/2.vm/086scenario_console_test.mu
@@ -0,0 +1,25 @@
+# To check our support for consoles in scenarios, rewrite tests from
+# scenario_console.mu
+# Tests for console interface.
+
+scenario read-key-in-mu [
+  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 <- read-key console
+  ]
+  memory-should-contain [
+    1 <- 97  # 'a'
+    2 <- 1
+    3 <- 98  # 'b'
+    4 <- 1
+    5 <- 99  # 'c'
+    6 <- 1
+    7 <- 0  # eof
+    8 <- 1
+  ]
+]
diff --git a/archive/2.vm/087file.cc b/archive/2.vm/087file.cc
new file mode 100644
index 00000000..9fd056db
--- /dev/null
+++ b/archive/2.vm/087file.cc
@@ -0,0 +1,225 @@
+//: Interacting with the file system.
+//:   '$open-file-for-reading' returns a FILE* as a number (ugh)
+//:   '$read-from-file' accepts a number, interprets it as a FILE* (double ugh) and reads a character from it
+//: Similarly for writing files.
+//: These interfaces are ugly and tied to the current (Linux) host Mu happens
+//: to be implemented atop. Later layers will wrap them with better, more
+//: testable interfaces.
+//:
+//: Clearly we don't care about performance or any of that so far.
+//: todo: reading/writing binary files
+
+:(before "End Primitive Recipe Declarations")
+_OPEN_FILE_FOR_READING,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$open-file-for-reading", _OPEN_FILE_FOR_READING);
+:(before "End Primitive Recipe Checks")
+case _OPEN_FILE_FOR_READING: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$open-file-for-reading' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_text(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$open-file-for-reading' should be a string, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$open-file-for-reading' requires exactly one product, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first product of '$open-file-for-reading' should be a number (file handle), but got '" << to_string(inst.products.at(0)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _OPEN_FILE_FOR_READING: {
+  string filename = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
+  assert(sizeof(long long int) >= sizeof(FILE*));
+  FILE* f = fopen(filename.c_str(), "r");
+  long long int result = reinterpret_cast<long long int>(f);
+  products.resize(1);
+  products.at(0).push_back(static_cast<double>(result));
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_OPEN_FILE_FOR_WRITING,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$open-file-for-writing", _OPEN_FILE_FOR_WRITING);
+:(before "End Primitive Recipe Checks")
+case _OPEN_FILE_FOR_WRITING: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$open-file-for-writing' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_text(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$open-file-for-writing' should be a string, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$open-file-for-writing' requires exactly one product, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first product of '$open-file-for-writing' should be a number (file handle), but got '" << to_string(inst.products.at(0)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _OPEN_FILE_FOR_WRITING: {
+  string filename = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
+  assert(sizeof(long long int) >= sizeof(FILE*));
+  long long int result = reinterpret_cast<long long int>(fopen(filename.c_str(), "w"));
+  products.resize(1);
+  products.at(0).push_back(static_cast<double>(result));
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_READ_FROM_FILE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$read-from-file", _READ_FROM_FILE);
+:(before "End Primitive Recipe Checks")
+case _READ_FROM_FILE: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$read-from-file' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$read-from-file' should be a number, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'$read-from-file' requires exactly two products, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_character(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first product of '$read-from-file' should be a character, but got '" << to_string(inst.products.at(0)) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_boolean(inst.products.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second product of '$read-from-file' should be a boolean, but got '" << to_string(inst.products.at(1)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _READ_FROM_FILE: {
+  long long int x = static_cast<long long int>(ingredients.at(0).at(0));
+  FILE* f = reinterpret_cast<FILE*>(x);
+  if (f == NULL) {
+    raise << maybe(current_recipe_name()) << "can't read from null file in '" << to_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  products.resize(2);
+  if (feof(f)) {
+    products.at(0).push_back(0);
+    products.at(1).push_back(1);  // eof
+    break;
+  }
+  if (ferror(f)) {
+    raise << maybe(current_recipe_name()) << "file in invalid state in '" << to_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  char c = getc(f);  // todo: unicode
+  if (c == EOF) {
+    products.at(0).push_back(0);
+    products.at(1).push_back(1);  // eof
+    break;
+  }
+  if (ferror(f)) {
+    raise << maybe(current_recipe_name()) << "couldn't read from file in '" << to_string(current_instruction()) << "'\n" << end();
+    raise << "  errno: " << errno << '\n' << end();
+    break;
+  }
+  products.at(0).push_back(c);
+  products.at(1).push_back(0);  // not eof
+  break;
+}
+:(before "End Includes")
+#include <errno.h>
+
+:(before "End Primitive Recipe Declarations")
+_WRITE_TO_FILE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$write-to-file", _WRITE_TO_FILE);
+:(before "End Primitive Recipe Checks")
+case _WRITE_TO_FILE: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'$write-to-file' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$write-to-file' should be a number, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_character(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of '$write-to-file' should be a character, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (!inst.products.empty()) {
+    raise << maybe(get(Recipe, r).name) << "'$write-to-file' writes to no products, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _WRITE_TO_FILE: {
+  long long int x = static_cast<long long int>(ingredients.at(0).at(0));
+  FILE* f = reinterpret_cast<FILE*>(x);
+  if (f == NULL) {
+    raise << maybe(current_recipe_name()) << "can't write to null file in '" << to_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  if (feof(f)) break;
+  if (ferror(f)) {
+    raise << maybe(current_recipe_name()) << "file in invalid state in '" << to_string(current_instruction()) << "'\n" << end();
+    break;
+  }
+  long long int y = static_cast<long long int>(ingredients.at(1).at(0));
+  char c = static_cast<char>(y);
+  putc(c, f);  // todo: unicode
+  if (ferror(f)) {
+    raise << maybe(current_recipe_name()) << "couldn't write to file in '" << to_string(current_instruction()) << "'\n" << end();
+    raise << "  errno: " << errno << '\n' << end();
+    break;
+  }
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_CLOSE_FILE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$close-file", _CLOSE_FILE);
+:(before "End Primitive Recipe Checks")
+case _CLOSE_FILE: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$close-file' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$close-file' should be a number, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$close-file' requires exactly one product, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (inst.products.at(0).name != inst.ingredients.at(0).name) {
+    raise << maybe(get(Recipe, r).name) << "'$close-file' requires its product to be the same as its ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _CLOSE_FILE: {
+  long long int x = static_cast<long long int>(ingredients.at(0).at(0));
+  FILE* f = reinterpret_cast<FILE*>(x);
+  fclose(f);
+  products.resize(1);
+  products.at(0).push_back(0);  // todo: ensure that caller always resets the ingredient
+  break;
+}
diff --git a/archive/2.vm/088file.mu b/archive/2.vm/088file.mu
new file mode 100644
index 00000000..da3e35d3
--- /dev/null
+++ b/archive/2.vm/088file.mu
@@ -0,0 +1,213 @@
+# Wrappers around file system primitives that take a 'resources' object and
+# are thus easier to test.
+#
+# - start-reading - asynchronously open a file, returning a channel source for
+#   receiving the results
+# - start-writing - asynchronously open a file, returning a channel sink for
+#   the data to write
+# - slurp - synchronously read from a file
+# - dump - synchronously write to a file
+
+container resources [
+  lock:bool
+  data:&:@:resource
+]
+
+container resource [
+  name:text
+  contents:text
+]
+
+def start-reading resources:&:resources, filename:text -> contents:&:source:char, error?:bool [
+  local-scope
+  load-inputs
+  error? <- copy false
+  {
+    break-unless resources
+    # fake file system
+    contents, error? <- start-reading-from-fake-resource resources, filename
+    return
+  }
+  # real file system
+  file:num <- $open-file-for-reading filename
+  return-unless file, null/no-contents, true/error
+  contents:&:source:char, sink:&:sink:char <- new-channel 30
+  start-running receive-from-file file, sink
+]
+
+def slurp resources:&:resources, filename:text -> contents:text, error?:bool [
+  local-scope
+  load-inputs
+  source:&:source:char, error?:bool <- start-reading resources, filename
+  return-if error?, null/no-contents
+  buf:&:buffer:char <- new-buffer 30/capacity
+  {
+    c:char, done?:bool, source <- read source
+    break-if done?
+    buf <- append buf, c
+    loop
+  }
+  contents <- buffer-to-array buf
+]
+
+def start-reading-from-fake-resource resources:&:resources, resource:text -> contents:&:source:char, error?:bool [
+  local-scope
+  load-inputs
+  error? <- copy false
+  i:num <- copy 0
+  data:&:@:resource <- get *resources, data:offset
+  len:num <- length *data
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    tmp:resource <- index *data, i
+    i <- add i, 1
+    curr-resource:text <- get tmp, name:offset
+    found?:bool <- equal resource, curr-resource
+    loop-unless found?
+    contents:&:source:char, sink:&:sink:char <- new-channel 30
+    curr-contents:text <- get tmp, contents:offset
+    start-running receive-from-text curr-contents, sink
+    return
+  }
+  return null/no-such-resource, true/error-found
+]
+
+def receive-from-file file:num, sink:&:sink:char -> sink:&:sink:char [
+  local-scope
+  load-inputs
+  {
+    c:char, eof?:bool <- $read-from-file file
+    break-if eof?
+    sink <- write sink, c
+    loop
+  }
+  sink <- close sink
+  file <- $close-file file
+]
+
+def receive-from-text contents:text, sink:&:sink:char -> sink:&:sink:char [
+  local-scope
+  load-inputs
+  i:num <- copy 0
+  len:num <- length *contents
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    c:char <- index *contents, i
+    sink <- write sink, c
+    i <- add i, 1
+    loop
+  }
+  sink <- close sink
+]
+
+def start-writing resources:&:resources, filename:text -> sink:&:sink:char, routine-id:num, error?:bool [
+  local-scope
+  load-inputs
+  error? <- copy false
+  source:&:source:char, sink:&:sink:char <- new-channel 30
+  {
+    break-unless resources
+    # fake file system
+    routine-id <- start-running transmit-to-fake-resource resources, filename, source
+    return
+  }
+  # real file system
+  file:num <- $open-file-for-writing filename
+  return-unless file, null/sink, 0/routine-id, true/error
+  {
+    break-if file
+    msg:text <- append [no such file: ] filename
+    assert file, msg
+  }
+  routine-id <- start-running transmit-to-file file, source
+]
+
+def dump resources:&:resources, filename:text, contents:text -> resources:&:resources, error?:bool [
+  local-scope
+  load-inputs
+  # todo: really create an empty file
+  return-unless contents, resources, false/no-error
+  sink-file:&:sink:char, write-routine:num, error?:bool <- start-writing resources, filename
+  return-if error?
+  i:num <- copy 0
+  len:num <- length *contents
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    c:char <- index *contents, i
+    sink-file <- write sink-file, c
+    i <- add i, 1
+    loop
+  }
+  close sink-file
+  # make sure to wait for the file to be actually written to disk
+  # (Mu practices structured concurrency: http://250bpm.com/blog:71)
+  wait-for-routine write-routine
+]
+
+def transmit-to-file file:num, source:&:source:char -> source:&:source:char [
+  local-scope
+  load-inputs
+  {
+    c:char, done?:bool, source <- read source
+    break-if done?
+    $write-to-file file, c
+    loop
+  }
+  file <- $close-file file
+]
+
+def transmit-to-fake-resource resources:&:resources, filename:text, source:&:source:char -> resources:&:resources, source:&:source:char [
+  local-scope
+  load-inputs
+  lock:location <- get-location *resources, lock:offset
+  wait-for-reset-then-set lock
+  # compute new file contents
+  buf:&:buffer:char <- new-buffer 30
+  {
+    c:char, done?:bool, source <- read source
+    break-if done?
+    buf <- append buf, c
+    loop
+  }
+  contents:text <- buffer-to-array buf
+  new-resource:resource <- merge filename, contents
+  # write to resources
+  curr-filename:text <- copy null
+  data:&:@:resource <- get *resources, data:offset
+  # replace file contents if it already exists
+  i:num <- copy 0
+  len:num <- length *data
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    tmp:resource <- index *data, i
+    curr-filename <- get tmp, name:offset
+    found?:bool <- equal filename, curr-filename
+    {
+      break-unless found?
+      put-index *data, i, new-resource
+      jump +unlock-and-exit
+    }
+    i <- add i, 1
+    loop
+  }
+  # if file didn't already exist, make room for it
+  new-len:num <- add len, 1
+  new-data:&:@:resource <- new resource:type, new-len
+  put *resources, data:offset, new-data
+  # copy over old files
+  i:num <- copy 0
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    tmp:resource <- index *data, i
+    put-index *new-data, i, tmp
+  }
+  # write new file
+  put-index *new-data, len, new-resource
+  +unlock-and-exit
+  reset lock
+]
diff --git a/archive/2.vm/089scenario_filesystem.cc b/archive/2.vm/089scenario_filesystem.cc
new file mode 100644
index 00000000..c49c20f8
--- /dev/null
+++ b/archive/2.vm/089scenario_filesystem.cc
@@ -0,0 +1,245 @@
+//: Clean syntax to manipulate and check the file system in scenarios.
+//: Instruction 'assume-resources' implicitly creates a variable called
+//: 'resources' that is accessible to later instructions in the scenario.
+
+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"
+  );
+}
+
+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);
+//: give 'resources' a fixed location in scenarios
+:(before "End Special Scenario Variable Names(r)")
+Name[r]["resources"] = RESOURCES;
+//: make 'resources' always a raw location in scenarios
+:(before "End is_special_name Special-cases")
+if (s == "resources") return true;
+:(before "End Initialize Type Of Special Name In Scenario(r)")
+if (r.name == "resources") r.type = new_type_tree("address:resources");
+
+:(before "End initialize_transform_rewrite_literal_string_to_text()")
+recipes_taking_literal_strings.insert("assume-resources");
+
+//: screen-should-contain is a regular instruction
+:(before "End Primitive Recipe Declarations")
+ASSUME_RESOURCES,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "assume-resources", ASSUME_RESOURCES);
+:(before "End Primitive Recipe Checks")
+case ASSUME_RESOURCES: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case ASSUME_RESOURCES: {
+  assert(scalar(ingredients.at(0)));
+  assume_resources(current_instruction().ingredients.at(0).name, current_recipe_name());
+  break;
+}
+
+:(code)
+void assume_resources(const string& data, const string& caller) {
+  map<string, string> contents;
+  parse_resources(data, contents, caller);
+  construct_resources_object(contents);
+}
+
+void parse_resources(const string& data, map<string, string>& out, const string& caller) {
+  istringstream in(data);
+  in >> std::noskipws;
+  while (true) {
+    if (!has_data(in)) break;
+    skip_whitespace_and_comments(in);
+    if (!has_data(in)) break;
+    string filename = next_word(in);
+    if (filename.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete 'resources' block at end of file (0)\n" << end();
+      return;
+    }
+    if (*filename.begin() != '[') {
+      raise << caller << ": assume-resources: filename '" << filename << "' must begin with a '['\n" << end();
+      break;
+    }
+    if (*filename.rbegin() != ']') {
+      raise << caller << ": assume-resources: filename '" << filename << "' must end with a ']'\n" << end();
+      break;
+    }
+    filename.erase(0, 1);
+    filename.erase(SIZE(filename)-1);
+    if (!has_data(in)) {
+      raise << caller << ": assume-resources: no data for filename '" << filename << "'\n" << end();
+      break;
+    }
+    string arrow = next_word(in);
+    if (arrow.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete 'resources' block at end of file (1)\n" << end();
+      return;
+    }
+    if (arrow != "<-") {
+      raise << caller << ": assume-resources: expected '<-' after filename '" << filename << "' but got '" << arrow << "'\n" << end();
+      break;
+    }
+    if (!has_data(in)) {
+      raise << caller << ": assume-resources: no data for filename '" << filename << "' after '<-'\n" << end();
+      break;
+    }
+    string contents = next_word(in);
+    if (contents.empty()) {
+      assert(!has_data(in));
+      raise << "incomplete 'resources' block at end of file (2)\n" << end();
+      return;
+    }
+    if (*contents.begin() != '[') {
+      raise << caller << ": assume-resources: file contents '" << contents << "' for filename '" << filename << "' must begin with a '['\n" << end();
+      break;
+    }
+    if (*contents.rbegin() != ']') {
+      raise << caller << ": assume-resources: file contents '" << contents << "' for filename '" << filename << "' must end with a ']'\n" << end();
+      break;
+    }
+    contents.erase(0, 1);
+    contents.erase(SIZE(contents)-1);
+    put(out, filename, munge_resources_contents(contents, filename, caller));
+  }
+}
+
+string munge_resources_contents(const string& data, const string& filename, const string& caller) {
+  if (data.empty()) return "";
+  istringstream in(data);
+  in >> std::noskipws;
+  skip_whitespace_and_comments(in);
+  ostringstream out;
+  while (true) {
+    if (!has_data(in)) break;
+    skip_whitespace(in);
+    if (!has_data(in)) break;
+    if (in.peek() != '|') {
+      raise << caller << ": assume-resources: file contents for filename '" << filename << "' must be delimited in '|'s\n" << end();
+      break;
+    }
+    in.get();  // skip leading '|'
+    string line;
+    getline(in, line);
+    for (int i = 0;  i < SIZE(line);  ++i) {
+      if (line.at(i) == '|') break;
+      if (line.at(i) == '\\') {
+        ++i;  // skip
+        if (i == SIZE(line)) {
+          raise << caller << ": assume-resources: file contents can't end a line with '\\'\n" << end();
+          break;
+        }
+      }
+      out << line.at(i);
+    }
+    // todo: some way to represent a file without a final newline
+    out << '\n';
+  }
+  return out.str();
+}
+
+void construct_resources_object(const map<string, string>& contents) {
+  int resources_data_address = allocate(SIZE(contents) * /*size of resource*/4 + /*array length*/1);
+  int curr = resources_data_address + /*skip alloc id*/1 + /*skip array length*/1;
+  for (map<string, string>::const_iterator p = contents.begin();  p != contents.end();  ++p) {
+    ++curr;  // skip alloc id of resource.name
+    put(Memory, curr, new_mu_text(p->first));
+    trace(Callstack_depth+1, "mem") << "storing file name " << get(Memory, curr) << " in location " << curr << end();
+    ++curr;
+    ++curr;  // skip alloc id of resource.contents
+    put(Memory, curr, new_mu_text(p->second));
+    trace(Callstack_depth+1, "mem") << "storing file contents " << get(Memory, curr) << " in location " << curr << end();
+    ++curr;
+  }
+  curr = resources_data_address + /*skip alloc id of resources.data*/1;
+  put(Memory, curr, SIZE(contents));  // array length
+  trace(Callstack_depth+1, "mem") << "storing resources size " << get(Memory, curr) << " in location " << curr << end();
+  // wrap the resources data in a 'resources' object
+  int resources_address = allocate(size_of_resources());
+  curr = resources_address+/*alloc id*/1+/*offset of 'data' element*/1+/*skip alloc id of 'data' element*/1;
+  put(Memory, curr, resources_data_address);
+  trace(Callstack_depth+1, "mem") << "storing resources data address " << resources_data_address << " in location " << curr << end();
+  // save in product
+  put(Memory, RESOURCES+/*skip alloc id*/1, resources_address);
+  trace(Callstack_depth+1, "mem") << "storing resources address " << resources_address << " in location " << RESOURCES << end();
+}
+
+int size_of_resources() {
+  // memoize result if already computed
+  static int result = 0;
+  if (result) return result;
+  assert(get(Type_ordinal, "resources"));
+  type_tree* type = new type_tree("resources");
+  result = size_of(type);
+  delete type;
+  return result;
+}
diff --git a/archive/2.vm/090scenario_filesystem_test.mu b/archive/2.vm/090scenario_filesystem_test.mu
new file mode 100644
index 00000000..b487bfe0
--- /dev/null
+++ b/archive/2.vm/090scenario_filesystem_test.mu
@@ -0,0 +1,99 @@
+# Check our support for fake file systems in scenarios.
+
+scenario read-from-fake-file [
+  local-scope
+  assume-resources [
+    [a] <- [
+      |xyz|
+    ]
+  ]
+  contents:&:source:char <- start-reading resources, [a]
+  1:char/raw <- read contents
+  2:char/raw <- read contents
+  3:char/raw <- read contents
+  4:char/raw <- read contents
+  _, 5:bool/raw <- read contents
+  memory-should-contain [
+    1 <- 120  # x
+    2 <- 121  # y
+    3 <- 122  # z
+    4 <- 10  # newline
+    5 <- 1  # eof
+  ]
+]
+
+scenario write-to-new-fake-file [
+  local-scope
+  assume-resources [
+  ]
+  sink:&:sink:char, writer:num/routine <- start-writing resources, [a]
+  sink <- write sink, 120/x
+  sink <- write sink, 121/y
+  close sink
+  wait-for-routine writer
+  contents-read-back:text <- slurp resources, [a]
+  10:bool/raw <- equal contents-read-back, [xy]
+  memory-should-contain [
+    10 <- 1  # file contents read back exactly match what was written
+  ]
+]
+
+scenario write-to-new-fake-file-2 [
+  local-scope
+  assume-resources [
+    [a] <- [
+      |abc|
+    ]
+  ]
+  sink:&:sink:char, writer:num/routine <- start-writing resources, [b]
+  sink <- write sink, 120/x
+  sink <- write sink, 121/y
+  close sink
+  wait-for-routine writer
+  contents-read-back:text <- slurp resources, [b]
+  10:bool/raw <- equal contents-read-back, [xy]
+  memory-should-contain [
+    10 <- 1  # file contents read back exactly match what was written
+  ]
+]
+
+scenario write-to-fake-file-that-exists [
+  local-scope
+  assume-resources [
+    [a] <- []
+  ]
+  sink:&:sink:char, writer:num/routine <- start-writing resources, [a]
+  sink <- write sink, 120/x
+  sink <- write sink, 121/y
+  close sink
+  wait-for-routine writer
+  contents-read-back:text <- slurp resources, [a]
+  10:bool/raw <- equal contents-read-back, [xy]
+  memory-should-contain [
+    10 <- 1  # file contents read back exactly match what was written
+  ]
+]
+
+scenario write-to-existing-file-preserves-other-files [
+  local-scope
+  assume-resources [
+    [a] <- []
+    [b] <- [
+      |bcd|
+    ]
+  ]
+  sink:&:sink:char, writer:num/routine <- start-writing resources, [a]
+  sink <- write sink, 120/x
+  sink <- write sink, 121/y
+  close sink
+  wait-for-routine writer
+  contents-read-back:text <- slurp resources, [a]
+  10:bool/raw <- equal contents-read-back, [xy]
+  other-file-contents:text <- slurp resources, [b]
+  11:bool/raw <- equal other-file-contents, [bcd
+]
+  memory-should-contain [
+    10 <- 1  # file contents read back exactly match what was written
+    11 <- 1  # other files also continue to persist unchanged
+  ]
+]
diff --git a/archive/2.vm/091socket.cc b/archive/2.vm/091socket.cc
new file mode 100644
index 00000000..a0f3b948
--- /dev/null
+++ b/archive/2.vm/091socket.cc
@@ -0,0 +1,348 @@
+:(before "End Types")
+struct socket_t {
+  int fd;
+  sockaddr_in addr;
+  bool polled;
+  socket_t() {
+    fd = 0;
+    polled = false;
+    bzero(&addr, sizeof(addr));
+  }
+};
+
+:(before "End Primitive Recipe Declarations")
+_OPEN_CLIENT_SOCKET,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$open-client-socket", _OPEN_CLIENT_SOCKET);
+:(before "End Primitive Recipe Checks")
+case _OPEN_CLIENT_SOCKET: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'$open-client-socket' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_text(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$open-client-socket' should be text (the hostname), but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second ingredient of '$open-client-socket' should be a number (the port of the hostname to connect to), but got '" << to_string(inst.ingredients.at(1)) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$open-client-socket' requires exactly one product, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first product of '$open-client-socket' should be a number (socket handle), but got '" << to_string(inst.products.at(0)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _OPEN_CLIENT_SOCKET: {
+  string host = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
+  int port = ingredients.at(1).at(0);
+  socket_t* client = client_socket(host, port);
+  products.resize(1);
+  if (client->fd < 0) {  // error
+    delete client;
+    products.at(0).push_back(0);
+    break;
+  }
+  long long int result = reinterpret_cast<long long int>(client);
+  products.at(0).push_back(static_cast<double>(result));
+  break;
+}
+:(code)
+socket_t* client_socket(const string& host, int port) {
+  socket_t* result = new socket_t;
+  result->fd = socket(AF_INET, SOCK_STREAM, 0);
+  if (result->fd < 0) {
+    raise << "Failed to create socket.\n" << end();
+    return result;
+  }
+  result->addr.sin_family = AF_INET;
+  hostent* tmp = gethostbyname(host.c_str());
+  bcopy(tmp->h_addr, reinterpret_cast<char*>(&result->addr.sin_addr.s_addr), tmp->h_length);
+  result->addr.sin_port = htons(port);
+  if (connect(result->fd, reinterpret_cast<sockaddr*>(&result->addr), sizeof(result->addr)) < 0) {
+    close(result->fd);
+    result->fd = -1;
+    raise << "Failed to connect to " << host << ':' << port << '\n' << end();
+  }
+  return result;
+}
+
+:(before "End Primitive Recipe Declarations")
+_OPEN_SERVER_SOCKET,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$open-server-socket", _OPEN_SERVER_SOCKET);
+:(before "End Primitive Recipe Checks")
+case _OPEN_SERVER_SOCKET: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$open-server-socket' requires exactly one ingredient (the port to listen for requests on), but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$open-server-socket' should be a number, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$open-server-socket' requires exactly one product, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first product of '$open-server-socket' should be a number (file handle), but got '" << to_string(inst.products.at(0)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _OPEN_SERVER_SOCKET: {
+  int port = ingredients.at(0).at(0);
+  socket_t* server = server_socket(port);
+  products.resize(1);
+  if (server->fd < 0) {
+    delete server;
+    products.at(0).push_back(0);
+    break;
+  }
+  long long int result = reinterpret_cast<long long int>(server);
+  products.at(0).push_back(static_cast<double>(result));
+  break;
+}
+:(code)
+socket_t* server_socket(int port) {
+  socket_t* result = new socket_t;
+  result->fd = socket(AF_INET, SOCK_STREAM, 0);
+  if (result->fd < 0) {
+    raise << "Failed to create server socket.\n" << end();
+    return result;
+  }
+  int dummy = 0;
+  setsockopt(result->fd, SOL_SOCKET, SO_REUSEADDR, &dummy, sizeof(dummy));
+  result->addr.sin_family = AF_INET;
+  result->addr.sin_addr.s_addr = Current_scenario ? htonl(INADDR_LOOPBACK) : INADDR_ANY;  // run tests without running afoul of any firewall
+  result->addr.sin_port = htons(port);
+  if (bind(result->fd, reinterpret_cast<sockaddr*>(&result->addr), sizeof(result->addr)) >= 0) {
+    listen(result->fd, /*queue length*/5);
+  }
+  else {
+    close(result->fd);
+    result->fd = -1;
+    raise << "Failed to bind result socket to port " << port << ". Something's already using that port.\n" << end();
+  }
+  return result;
+}
+
+:(before "End Primitive Recipe Declarations")
+_ACCEPT,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$accept", _ACCEPT);
+:(before "End Primitive Recipe Checks")
+case _ACCEPT: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$accept' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$accept' should be a number, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$accept' requires exactly one product, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first product of '$accept' should be a number (file handle), but got '" << to_string(inst.products.at(0)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _ACCEPT: {
+  products.resize(2);
+  products.at(1).push_back(ingredients.at(0).at(0));  // indicate it modifies its ingredient
+  long long int x = static_cast<long long int>(ingredients.at(0).at(0));
+  socket_t* server = reinterpret_cast<socket_t*>(x);
+  if (server) {
+    socket_t* session = accept_session(server);
+    long long int result = reinterpret_cast<long long int>(session);
+    products.at(0).push_back(static_cast<double>(result));
+  }
+  else {
+    products.at(0).push_back(0);
+  }
+  break;
+}
+:(code)
+socket_t* accept_session(socket_t* server) {
+  if (server->fd == 0) return NULL;
+  socket_t* result = new socket_t;
+  socklen_t dummy = sizeof(result->addr);
+  result->fd = accept(server->fd, reinterpret_cast<sockaddr*>(&result->addr), &dummy);
+  return result;
+}
+
+:(before "End Primitive Recipe Declarations")
+_READ_FROM_SOCKET,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$read-from-socket", _READ_FROM_SOCKET);
+:(before "End Primitive Recipe Checks")
+case _READ_FROM_SOCKET: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$read-from-socket' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$read-from-socket' should be a number (socket), but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  int nprod = SIZE(inst.products);
+  if (nprod == 0 || nprod > 4) {
+    raise << maybe(get(Recipe, r).name) << "'$read-from-socket' requires 1-4 products, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_character(inst.products.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first product of '$read-from-socket' should be a character, but got '" << to_string(inst.products.at(0)) << "'\n" << end();
+    break;
+  }
+  if (nprod > 1 && !is_mu_boolean(inst.products.at(1))) {
+    raise << maybe(get(Recipe, r).name) << "second product of '$read-from-socket' should be a boolean (data received?), but got '" << to_string(inst.products.at(1)) << "'\n" << end();
+    break;
+  }
+  if (nprod > 2 && !is_mu_boolean(inst.products.at(2))) {
+    raise << maybe(get(Recipe, r).name) << "third product of '$read-from-socket' should be a boolean (eof?), but got '" << to_string(inst.products.at(2)) << "'\n" << end();
+    break;
+  }
+  if (nprod > 3 && !is_mu_number(inst.products.at(3))) {
+    raise << maybe(get(Recipe, r).name) << "fourth product of '$read-from-socket' should be a number (error code), but got '" << to_string(inst.products.at(3)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _READ_FROM_SOCKET: {
+  products.resize(4);
+  long long int x = static_cast<long long int>(ingredients.at(0).at(0));
+  socket_t* socket = reinterpret_cast<socket_t*>(x);
+  // 1. we'd like to simply read() from the socket
+  // however read() on a socket never returns EOF, so we wouldn't know when to stop
+  // 2. recv() can signal EOF, but it also signals "no data yet" in the beginning
+  // so use poll() in the beginning to wait for data before calling recv()
+  // 3. but poll() will block on EOF, so only use poll() on the very first
+  // $read-from-socket on a socket
+  //
+  // Also, there was an unresolved issue where attempts to read() a small
+  // number of bytes (less than 447 on Linux and Mac) would cause browsers to
+  // prematurely close the connection. See commit 3403. That seems to be gone
+  // after moving to recv()+poll(). It was never observed on OpenBSD.
+  if (!socket->polled) {
+    pollfd p;
+    bzero(&p, sizeof(p));
+    p.fd = socket->fd;
+    p.events = POLLIN | POLLHUP;
+    int poll_result = poll(&p, /*num pollfds*/1, /*timeout*/100/*ms*/);
+    if (poll_result == 0) {
+      products.at(0).push_back(/*no data*/0);
+      products.at(1).push_back(/*found*/false);
+      products.at(2).push_back(/*eof*/false);
+      products.at(3).push_back(/*error*/0);
+      break;
+    }
+    else if (poll_result < 0) {
+      int error_code = errno;
+      raise << maybe(current_recipe_name()) << "error in $read-from-socket\n" << end();
+      products.at(0).push_back(/*no data*/0);
+      products.at(1).push_back(/*found*/false);
+      products.at(2).push_back(/*eof*/false);
+      products.at(3).push_back(error_code);
+      break;
+    }
+    socket->polled = true;
+  }
+  char c = '\0';
+  int error_code = 0;
+  int bytes_read = recv(socket->fd, &c, /*single byte*/1, MSG_DONTWAIT);
+  if (bytes_read < 0) error_code = errno;
+//?   if (error_code) {
+//?     ostringstream out;
+//?     out << "error in $read-from-socket " << socket->fd;
+//?     perror(out.str().c_str());
+//?   }
+  products.at(0).push_back(c);
+  products.at(1).push_back(/*found*/true);
+  products.at(2).push_back(/*eof*/bytes_read <= 0);
+  products.at(3).push_back(error_code);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_WRITE_TO_SOCKET,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$write-to-socket", _WRITE_TO_SOCKET);
+:(before "End Primitive Recipe Checks")
+case _WRITE_TO_SOCKET: {
+  if (SIZE(inst.ingredients) != 2) {
+    raise << maybe(get(Recipe, r).name) << "'$write-to-socket' requires exactly two ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _WRITE_TO_SOCKET: {
+  long long int x = static_cast<long long int>(ingredients.at(0).at(0));
+  socket_t* socket = reinterpret_cast<socket_t*>(x);
+  // write just one character at a time to the socket
+  long long int y = static_cast<long long int>(ingredients.at(1).at(0));
+  char c = static_cast<char>(y);
+  if (write(socket->fd, &c, 1) != 1) {
+    raise << maybe(current_recipe_name()) << "failed to write to socket\n" << end();
+    exit(0);
+  }
+  products.resize(1);
+  products.at(0).push_back(ingredients.at(0).at(0));
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_CLOSE_SOCKET,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$close-socket", _CLOSE_SOCKET);
+:(before "End Primitive Recipe Checks")
+case _CLOSE_SOCKET: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$close-socket' requires exactly two ingredient, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (!is_mu_number(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of '$close-socket' should be a number, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  if (SIZE(inst.products) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'$close-socket' requires exactly one product, but got '" << to_original_string(inst) << "'\n" << end();
+    break;
+  }
+  if (inst.products.at(0).name != inst.ingredients.at(0).name) {
+    raise << maybe(get(Recipe, r).name) << "product of '$close-socket' must be first ingredient '" << inst.ingredients.at(0).original_string << "', but got '" << inst.products.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _CLOSE_SOCKET: {
+  long long int x = static_cast<long long int>(ingredients.at(0).at(0));
+  socket_t* socket = reinterpret_cast<socket_t*>(x);
+  close(socket->fd);
+  delete socket;
+  products.resize(1);
+  products.at(0).push_back(0);  // make sure we can't reuse the socket
+  break;
+}
+
+:(before "End Includes")
+#include <netinet/in.h>
+#include <netdb.h>
+#include <poll.h>
+#include <sys/socket.h>
+#include <unistd.h>
diff --git a/archive/2.vm/092socket.mu b/archive/2.vm/092socket.mu
new file mode 100644
index 00000000..b0dca4b7
--- /dev/null
+++ b/archive/2.vm/092socket.mu
@@ -0,0 +1,177 @@
+# Wrappers around socket primitives that are easier to test.
+
+# To test server operations, just run a real client against localhost.
+scenario example-server-test [
+  local-scope
+  # test server without a fake on a random (real) port
+  # that way repeatedly running the test will give ports time to timeout and
+  # close before reusing them
+  make-random-nondeterministic
+  port:num <- random-in-range null/real-random-numbers, 8000, 8100
+  run [
+    socket:num <- $open-server-socket port
+    assert socket, [ 
+F - example-server-test: $open-server-socket failed]
+    handler-routine:number <- start-running serve-one-request socket, example-handler
+  ]
+  source:&:source:char <- start-reading-from-network null/real-resources, [localhost/], port
+  response:text <- drain source
+  10:@:char/raw <- copy *response
+  memory-should-contain [
+    10:array:character <- [abc]
+  ]
+  socket <- $close-socket socket
+]
+# helper just for this scenario
+def example-handler query:text -> response:text [
+  local-scope
+  load-inputs
+  return [abc]
+]
+
+# To test client operations, use 'assume-resources' with a filename that
+# begins with a hostname. (Filenames starting with '/' are assumed to be
+# local.)
+scenario example-client-test [
+  local-scope
+  assume-resources [
+    [example.com/] <- [
+      |abc|
+    ]
+  ]
+  run [
+    source:&:source:char <- start-reading-from-network resources, [example.com/]
+  ]
+  contents:text <- drain source
+  10:@:char/raw <- copy *contents
+  memory-should-contain [
+    10:array:character <- [abc
+]
+  ]
+]
+
+type request-handler = (recipe text -> text)
+
+def serve-one-request socket:num, request-handler:request-handler -> socket:num [
+  local-scope
+  load-inputs
+  session:num <- $accept socket
+  assert session, [ 
+F - example-server-test: $accept failed]
+  contents:&:source:char, sink:&:sink:char <- new-channel 30
+  start-running receive-from-socket session, sink
+  query:text <- drain contents
+  response:text <- call request-handler, query
+  write-to-socket session, response
+  session <- $close-socket session
+]
+
+def start-reading-from-network resources:&:resources, uri:text -> contents:&:source:char [
+  local-scope
+  load-inputs
+  {
+    port:num, port-found?:boolean <- next-input
+    break-if port-found?
+    port <- copy 80/http-port
+  }
+  {
+    break-unless resources
+    # fake network
+    contents <- start-reading-from-fake-resource resources, uri
+    return
+  }
+  # real network
+  host:text, path:text <- split-at uri, 47/slash
+  socket:num <- $open-client-socket host, port
+  assert socket, [contents]
+  req:text <- interpolate [GET _ HTTP/1.1], path
+  request-socket socket, req
+  contents:&:source:char, sink:&:sink:char <- new-channel 10000
+  start-running receive-from-client-socket-and-close socket, sink
+]
+
+def request-socket socket:num, s:text -> socket:num [
+  local-scope
+  load-inputs
+  write-to-socket socket, s
+  $write-to-socket socket, 13/cr
+  $write-to-socket socket, 10/lf
+  # empty line to delimit request
+  $write-to-socket socket, 13/cr
+  $write-to-socket socket, 10/lf
+]
+
+def receive-from-socket socket:num, sink:&:sink:char -> sink:&:sink:char, socket:num [
+  local-scope
+  load-inputs
+  {
+    +next-attempt
+    c:char, found?:bool, eof?:bool, error:num <- $read-from-socket socket
+    break-if eof?
+    break-if error
+    {
+      break-unless found?
+      sink <- write sink, c
+    }
+    {
+      break-if found?
+      switch
+    }
+    loop
+  }
+  sink <- close sink
+]
+
+def receive-from-client-socket-and-close socket:num, sink:&:sink:char -> sink:&:sink:char, socket:num [
+  local-scope
+  load-inputs
+  sink <- receive-from-socket socket, sink
+  socket <- $close-socket socket
+]
+
+def write-to-socket socket:num, s:text [
+  local-scope
+  load-inputs
+  len:num <- length *s
+  i:num <- copy 0
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    c:char <- index *s, i
+    $write-to-socket socket, c
+    i <- add i, 1
+    loop
+  }
+]
+
+# like split-first, but don't eat the delimiter
+def split-at text:text, delim:char -> x:text, y:text [
+  local-scope
+  load-inputs
+  # empty text? return empty texts
+  len:num <- length *text
+  {
+    empty?:bool <- equal len, 0
+    break-unless empty?
+    x:text <- new []
+    y:text <- new []
+    return
+  }
+  idx:num <- find-next text, delim, 0
+  x:text <- copy-range text, 0, idx
+  y:text <- copy-range text, idx, len
+]
+
+scenario text-split-at [
+  local-scope
+  x:text <- new [a/b]
+  run [
+    y:text, z:text <- split-at x, 47/slash
+    10:@:char/raw <- copy *y
+    20:@:char/raw <- copy *z
+  ]
+  memory-should-contain [
+    10:array:character <- [a]
+    20:array:character <- [/b]
+  ]
+]
diff --git a/archive/2.vm/099hardware_checks.cc b/archive/2.vm/099hardware_checks.cc
new file mode 100644
index 00000000..c1039c1f
--- /dev/null
+++ b/archive/2.vm/099hardware_checks.cc
@@ -0,0 +1,67 @@
+//: Let's raise errors when students use real hardware in any recipes besides
+//: 'main'. Part of the goal is to teach them testing hygiene and dependency
+//: injection.
+//:
+//: This is easy to sidestep, it's for feedback rather than safety.
+
+:(before "End Globals")
+vector<type_tree*> Real_hardware_types;
+:(before "Begin transform_all")
+setup_real_hardware_types();
+:(before "End transform_all")
+teardown_real_hardware_types();
+:(code)
+void setup_real_hardware_types() {
+  Real_hardware_types.push_back(parse_type("address:screen"));
+  Real_hardware_types.push_back(parse_type("address:console"));
+  Real_hardware_types.push_back(parse_type("address:resources"));
+}
+type_tree* parse_type(string s) {
+  reagent x("x:"+s);
+  type_tree* result = x.type;
+  x.type = NULL;  // don't deallocate on return
+  return result;
+}
+void teardown_real_hardware_types() {
+  for (int i = 0;  i < SIZE(Real_hardware_types);  ++i)
+    delete Real_hardware_types.at(i);
+  Real_hardware_types.clear();
+}
+
+:(before "End Checks")
+Transform.push_back(check_for_misuse_of_real_hardware);
+:(code)
+void check_for_misuse_of_real_hardware(const recipe_ordinal r) {
+  const recipe& caller = get(Recipe, r);
+  if (caller.name == "main") return;
+  if (starts_with(caller.name, "scenario_")) return;
+  trace(101, "transform") << "--- check if recipe " << caller.name << " has any dependency-injection mistakes" << end();
+  for (int index = 0;  index < SIZE(caller.steps);  ++index) {
+    const instruction& inst = caller.steps.at(index);
+    if (is_primitive(inst.operation)) continue;
+    for (int i = 0;  i < SIZE(inst.ingredients);  ++i) {
+      const reagent& ing = inst.ingredients.at(i);
+      if (!is_literal(ing) || ing.name != "0") continue;
+      const recipe& callee = get(Recipe, inst.operation);
+      if (!callee.has_header) continue;
+      if (i >= SIZE(callee.ingredients)) continue;
+      const reagent& expected_ing = callee.ingredients.at(i);
+      for (int j = 0;  j < SIZE(Real_hardware_types);  ++j) {
+        if (*Real_hardware_types.at(j) == *expected_ing.type)
+          raise << maybe(caller.name) << "'" << to_original_string(inst) << "': only 'main' can pass 0 into a " << to_string(expected_ing.type) << '\n' << end();
+      }
+    }
+  }
+}
+
+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/archive/2.vm/101run_sandboxed.cc b/archive/2.vm/101run_sandboxed.cc
new file mode 100644
index 00000000..e464bbe3
--- /dev/null
+++ b/archive/2.vm/101run_sandboxed.cc
@@ -0,0 +1,711 @@
+//: Helper for various programming environments: run arbitrary Mu code and
+//: return some result in text form.
+
+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.
+:(before "End Globals")
+bool Sandbox_mode = false;
+//: for starters, users can't override 'main' when the environment is running
+:(before "End Load Recipe Name")
+if (Sandbox_mode && result.name == "main") {
+  slurp_balanced_bracket(in);
+  return -1;
+}
+
+//: run code in 'interactive mode', i.e. with errors off and return:
+//:   stringified output in case we want to print it to screen
+//:   any errors encountered
+//:   simulated screen any prints went to
+//:   any 'app' layer traces generated
+:(before "End Primitive Recipe Declarations")
+RUN_SANDBOXED,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "run-sandboxed", RUN_SANDBOXED);
+:(before "End Primitive Recipe Checks")
+case RUN_SANDBOXED: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'run-sandboxed' requires exactly one ingredient, but got '" << inst.original_string << "'\n" << end();
+    break;
+  }
+  if (!is_mu_text(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'run-sandboxed' should be a string, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case RUN_SANDBOXED: {
+  bool new_code_pushed_to_stack = run_interactive(ingredients.at(0).at(/*skip alloc id*/1));
+  if (!new_code_pushed_to_stack) {
+    products.resize(5);
+    products.at(0).push_back(/*alloc id*/0);
+    products.at(0).push_back(0);
+    products.at(1).push_back(/*alloc id*/0);
+    products.at(1).push_back(trace_error_contents());
+    products.at(2).push_back(/*alloc id*/0);
+    products.at(2).push_back(0);
+    products.at(3).push_back(/*alloc id*/0);
+    products.at(3).push_back(trace_app_contents());
+    products.at(4).push_back(1);  // completed
+    run_code_end();
+    break;  // done with this instruction
+  }
+  else {
+    continue;  // not done with caller; don't increment current_step_index()
+  }
+}
+
+//: To show results in the sandbox Mu uses a hack: it saves the products
+//: returned by each instruction while Track_most_recent_products is true, and
+//: keeps the most recent such result around so that it can be returned as the
+//: result of a sandbox.
+
+:(before "End Globals")
+bool Track_most_recent_products = false;
+int Call_depth_to_track_most_recent_products_at = 0;
+string Most_recent_products;
+:(before "End Reset")
+Track_most_recent_products = false;
+Call_depth_to_track_most_recent_products_at = 0;
+Most_recent_products = "";
+
+:(before "End Globals")
+trace_stream* Save_trace_stream = NULL;
+string Save_trace_file;
+int Save_callstack_depth = 0;
+:(code)
+// reads a string, tries to call it as code (treating it as a test), saving
+// all errors.
+// returns true if successfully called (no errors found during load and transform)
+bool run_interactive(int address) {
+//?   cerr << "run_interactive: " << address << '\n';
+  assert(contains_key(Recipe_ordinal, "interactive") && get(Recipe_ordinal, "interactive") != 0);
+  // try to sandbox the run as best you can
+  // todo: test this
+  if (!Current_scenario) {
+    for (int i = 1; i < Reserved_for_tests; ++i)
+      Memory.erase(i);
+  }
+  string command = trim(strip_comments(read_mu_text(address)));
+//?   cerr << "command: " << command << '\n';
+  Name[get(Recipe_ordinal, "interactive")].clear();
+  run_code_begin(/*should_stash_snapshots*/true);
+  if (command.empty()) return false;
+  // don't kill the current routine on parse errors
+  routine* save_current_routine = Current_routine;
+  Current_routine = NULL;
+  // call run(string) but without the scheduling
+  load(string("recipe! interactive [\n") +
+          "local-scope\n" +
+          "screen:&:screen <- next-ingredient\n" +
+          "$start-tracking-products\n" +
+          command + "\n" +
+          "$stop-tracking-products\n" +
+          "return screen\n" +
+       "]\n");
+  transform_all();
+  Current_routine = save_current_routine;
+  if (trace_count("error") > 0) return false;
+  // now call 'sandbox' which will run 'interactive' in a separate routine,
+  // and wait for it
+  if (Save_trace_stream) {
+    ++Save_callstack_depth;
+    trace(Save_callstack_depth+1, "trace") << "run-sandboxed: incrementing callstack depth to " << Save_callstack_depth << end();
+    assert(Save_callstack_depth < Max_depth);
+  }
+  Current_routine->calls.push_front(call(get(Recipe_ordinal, "sandbox")));
+  return true;
+}
+
+//: Carefully update all state to exactly how it was -- including snapshots.
+
+:(before "End Globals")
+bool Run_profiler_stash = false;
+map<string, recipe_ordinal> Recipe_ordinal_snapshot_stash;
+map<recipe_ordinal, recipe> Recipe_snapshot_stash;
+map<string, type_ordinal> Type_ordinal_snapshot_stash;
+map<type_ordinal, type_info> Type_snapshot_stash;
+map<recipe_ordinal, map<string, int> > Name_snapshot_stash;
+map<string, vector<recipe_ordinal> > Recipe_variants_snapshot_stash;
+map<string, type_tree*> Type_abbreviations_snapshot_stash;
+vector<scenario> Scenarios_snapshot_stash;
+set<string> Scenario_names_snapshot_stash;
+
+:(code)
+void run_code_begin(bool should_stash_snapshots) {
+  // stuff to undo later, in run_code_end()
+  Hide_errors = true;
+  Disable_redefine_checks = true;
+  Run_profiler_stash = Run_profiler;
+  Run_profiler = false;
+  if (should_stash_snapshots)
+    stash_snapshots();
+  Save_trace_stream = Trace_stream;
+  Save_callstack_depth = Callstack_depth;
+  Callstack_depth = Initial_callstack_depth;
+  Trace_stream = new trace_stream;
+  if (Save_trace_stream)
+    Trace_stream->collect_depth = Save_trace_stream->collect_depth;
+}
+
+void run_code_end() {
+  Hide_errors = false;
+  Disable_redefine_checks = false;
+  Run_profiler = Run_profiler_stash;
+  Run_profiler_stash = false;
+//?   ofstream fout("sandbox.log");
+//?   fout << Trace_stream->readable_contents("");
+//?   fout.close();
+  delete Trace_stream;
+  Trace_stream = Save_trace_stream;
+  Callstack_depth = Save_callstack_depth;
+  Save_trace_stream = NULL;
+  Save_trace_file.clear();
+  Save_callstack_depth = 0;
+  Recipe.erase(get(Recipe_ordinal, "interactive"));  // keep past sandboxes from inserting errors
+  if (!Recipe_snapshot_stash.empty())
+    unstash_snapshots();
+}
+
+// keep sync'd with save_snapshots and restore_snapshots
+void stash_snapshots() {
+  assert(Recipe_ordinal_snapshot_stash.empty());
+  Recipe_ordinal_snapshot_stash = Recipe_ordinal_snapshot;
+  assert(Recipe_snapshot_stash.empty());
+  Recipe_snapshot_stash = Recipe_snapshot;
+  assert(Type_ordinal_snapshot_stash.empty());
+  Type_ordinal_snapshot_stash = Type_ordinal_snapshot;
+  assert(Type_snapshot_stash.empty());
+  Type_snapshot_stash = Type_snapshot;
+  assert(Name_snapshot_stash.empty());
+  Name_snapshot_stash = Name_snapshot;
+  assert(Recipe_variants_snapshot_stash.empty());
+  Recipe_variants_snapshot_stash = Recipe_variants_snapshot;
+  assert(Type_abbreviations_snapshot_stash.empty());
+  Type_abbreviations_snapshot_stash = Type_abbreviations_snapshot;
+  assert(Scenarios_snapshot_stash.empty());
+  Scenarios_snapshot_stash = Scenarios_snapshot;
+  assert(Scenario_names_snapshot_stash.empty());
+  Scenario_names_snapshot_stash = Scenario_names_snapshot;
+  save_snapshots();
+}
+void unstash_snapshots() {
+  restore_snapshots();
+  Recipe_ordinal_snapshot = Recipe_ordinal_snapshot_stash;  Recipe_ordinal_snapshot_stash.clear();
+  Recipe_snapshot = Recipe_snapshot_stash;  Recipe_snapshot_stash.clear();
+  Type_ordinal_snapshot = Type_ordinal_snapshot_stash;  Type_ordinal_snapshot_stash.clear();
+  Type_snapshot = Type_snapshot_stash;  Type_snapshot_stash.clear();
+  Name_snapshot = Name_snapshot_stash;  Name_snapshot_stash.clear();
+  Recipe_variants_snapshot = Recipe_variants_snapshot_stash;  Recipe_variants_snapshot_stash.clear();
+  Type_abbreviations_snapshot = Type_abbreviations_snapshot_stash;  Type_abbreviations_snapshot_stash.clear();
+  Scenarios_snapshot = Scenarios_snapshot_stash;  Scenarios_snapshot_stash.clear();
+  Scenario_names_snapshot = Scenario_names_snapshot_stash;  Scenario_names_snapshot_stash.clear();
+}
+
+:(before "End Mu Prelude")
+load(string(
+"recipe interactive [\n") +  // just a dummy version to initialize the Recipe_ordinal and so on
+"]\n" +
+"recipe sandbox [\n" +
+  "local-scope\n" +
+//?   "$print [aaa] 10/newline\n" +
+  "screen:&:screen <- new-fake-screen 30, 5\n" +
+  "routine-id:num <- start-running interactive, screen\n" +
+  "limit-time routine-id, 100000/instructions\n" +
+  "wait-for-routine routine-id\n" +
+//?   "$print [bbb] 10/newline\n" +
+  "instructions-run:num <- number-of-instructions routine-id\n" +
+  "stash instructions-run [instructions run]\n" +
+  "sandbox-state:num <- routine-state routine-id\n" +
+  "completed?:bool <- equal sandbox-state, 1/completed\n" +
+//?   "$print [completed: ] completed? 10/newline\n" +
+  "output:text <- $most-recent-products\n" +
+//?   "$print [zzz] 10/newline\n" +
+//?   "$print output\n" +
+  "errors:text <- save-errors\n" +
+  "stashes:text <- save-app-trace\n" +
+  "$cleanup-run-sandboxed\n" +
+  "return output, errors, screen, stashes, completed?\n" +
+"]\n");
+
+//: adjust errors in the sandbox
+:(before "End maybe(recipe_name) Special-cases")
+if (recipe_name == "interactive") return "";
+
+:(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,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$start-tracking-products", _START_TRACKING_PRODUCTS);
+:(before "End Primitive Recipe Checks")
+case _START_TRACKING_PRODUCTS: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _START_TRACKING_PRODUCTS: {
+  Track_most_recent_products = true;
+  Call_depth_to_track_most_recent_products_at = SIZE(Current_routine->calls);
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_STOP_TRACKING_PRODUCTS,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$stop-tracking-products", _STOP_TRACKING_PRODUCTS);
+:(before "End Primitive Recipe Checks")
+case _STOP_TRACKING_PRODUCTS: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _STOP_TRACKING_PRODUCTS: {
+  Track_most_recent_products = false;
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_MOST_RECENT_PRODUCTS,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$most-recent-products", _MOST_RECENT_PRODUCTS);
+:(before "End Primitive Recipe Checks")
+case _MOST_RECENT_PRODUCTS: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _MOST_RECENT_PRODUCTS: {
+  products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
+  products.at(0).push_back(new_mu_text(Most_recent_products));
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+SAVE_ERRORS,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "save-errors", SAVE_ERRORS);
+:(before "End Primitive Recipe Checks")
+case SAVE_ERRORS: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SAVE_ERRORS: {
+  products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
+  products.at(0).push_back(trace_error_contents());
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+SAVE_APP_TRACE,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "save-app-trace", SAVE_APP_TRACE);
+:(before "End Primitive Recipe Checks")
+case SAVE_APP_TRACE: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case SAVE_APP_TRACE: {
+  products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
+  products.at(0).push_back(trace_app_contents());
+  break;
+}
+
+:(before "End Primitive Recipe Declarations")
+_CLEANUP_RUN_SANDBOXED,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "$cleanup-run-sandboxed", _CLEANUP_RUN_SANDBOXED);
+:(before "End Primitive Recipe Checks")
+case _CLEANUP_RUN_SANDBOXED: {
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case _CLEANUP_RUN_SANDBOXED: {
+  run_code_end();
+  break;
+}
+
+:(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
+    && !current_instruction().is_label
+    && current_instruction().name != "$stop-tracking-products") {
+  Most_recent_products = "";
+}
+:(before "End Running One Instruction")
+if (Track_most_recent_products && SIZE(Current_routine->calls) == Call_depth_to_track_most_recent_products_at) {
+  Most_recent_products = track_most_recent_products(current_instruction(), products);
+//?   cerr << "most recent products: " << Most_recent_products << '\n';
+}
+:(code)
+string track_most_recent_products(const instruction& instruction, const vector<vector<double> >& products) {
+  ostringstream out;
+  for (int i = 0; i < SIZE(products); ++i) {
+    // A sandbox can print a string result, but only if it is actually saved
+    // to a variable in the sandbox, because otherwise the results are
+    // reclaimed before the sandbox sees them. So you get these interactions
+    // in the sandbox:
+    //
+    //    new [abc]
+    //    => <address>
+    //
+    //    x:text <- new [abc]
+    //    => abc
+    if (i < SIZE(instruction.products)) {
+      if (is_mu_text(instruction.products.at(i))) {
+        if (SIZE(products.at(i)) != 2) continue;  // weak silent check for address
+        out << read_mu_text(products.at(i).at(/*skip alloc id*/1)) << '\n';
+        continue;
+      }
+    }
+    for (int j = 0; j < SIZE(products.at(i)); ++j)
+      out << no_scientific(products.at(i).at(j)) << ' ';
+    out << '\n';
+  }
+  return out.str();
+}
+
+:(code)
+string strip_comments(string in) {
+  ostringstream result;
+  for (int i = 0; i < SIZE(in); ++i) {
+    if (in.at(i) != '#') {
+      result << in.at(i);
+    }
+    else {
+      while (i+1 < SIZE(in) && in.at(i+1) != '\n')
+        ++i;
+    }
+  }
+  return result.str();
+}
+
+int stringified_value_of_location(int address) {
+  // convert to string
+  ostringstream out;
+  out << no_scientific(get_or_insert(Memory, address));
+  return new_mu_text(out.str());
+}
+
+int trace_error_contents() {
+  if (!Trace_stream) return 0;
+  ostringstream out;
+  for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
+    if (p->label != "error") continue;
+    out << p->contents;
+    if (*--p->contents.end() != '\n') out << '\n';
+  }
+  string result = out.str();
+  truncate(result);
+  if (result.empty()) return 0;
+  return new_mu_text(result);
+}
+
+int trace_app_contents() {
+  if (!Trace_stream) return 0;
+  ostringstream out;
+  for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
+    if (p->depth != App_depth) continue;
+    out << p->contents;
+    if (*--p->contents.end() != '\n') out << '\n';
+  }
+  string result = out.str();
+  if (result.empty()) return 0;
+  truncate(result);
+  return new_mu_text(result);
+}
+
+void truncate(string& x) {
+  if (SIZE(x) > 1024) {
+    x.erase(1024);
+    *x.rbegin() = '\n';
+    *++x.rbegin() = '.';
+    *++++x.rbegin() = '.';
+  }
+}
+
+//: simpler version of run-sandboxed: doesn't do any running, just loads
+//: recipes and reports errors.
+
+:(before "End Primitive Recipe Declarations")
+RELOAD,
+:(before "End Primitive Recipe Numbers")
+put(Recipe_ordinal, "reload", RELOAD);
+:(before "End Primitive Recipe Checks")
+case RELOAD: {
+  if (SIZE(inst.ingredients) != 1) {
+    raise << maybe(get(Recipe, r).name) << "'reload' requires exactly one ingredient, but got '" << inst.original_string << "'\n" << end();
+    break;
+  }
+  if (!is_mu_text(inst.ingredients.at(0))) {
+    raise << maybe(get(Recipe, r).name) << "first ingredient of 'reload' should be a string, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
+    break;
+  }
+  break;
+}
+:(before "End Primitive Recipe Implementations")
+case RELOAD: {
+  restore_non_recipe_snapshots();
+  string code = read_mu_text(ingredients.at(0).at(/*skip alloc id*/1));
+  run_code_begin(/*should_stash_snapshots*/false);
+  routine* save_current_routine = Current_routine;
+  Current_routine = NULL;
+  Sandbox_mode = true;
+  vector<recipe_ordinal> recipes_reloaded = load(code);
+  transform_all();
+  Trace_stream->newline();  // flush trace
+  Sandbox_mode = false;
+  Current_routine = save_current_routine;
+  products.resize(1);
+  products.at(0).push_back(/*alloc id*/0);
+  products.at(0).push_back(trace_error_contents());
+  run_code_end();  // wait until we're done with the trace contents
+  break;
+}
+
+:(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/archive/2.vm/998check_type_pointers.cc b/archive/2.vm/998check_type_pointers.cc
new file mode 100644
index 00000000..da19cf3e
--- /dev/null
+++ b/archive/2.vm/998check_type_pointers.cc
@@ -0,0 +1,36 @@
+//: enable this when tracking down null types
+//: (but it interferes with edit/; since recipes created in the environment
+//: can raise warnings here which will stop running the entire environment)
+//? :(before "End Transform All")
+//? check_type_pointers();
+//? 
+//? :(code)
+//? void check_type_pointers() {
+//?   for (map<recipe_ordinal, recipe>::iterator p = Recipe.begin(); p != Recipe.end(); ++p) {
+//?     if (any_type_ingredient_in_header(p->first)) continue;
+//?     const recipe& r = p->second;
+//?     for (long long int i = 0; i < SIZE(r.steps); ++i) {
+//?       const instruction& inst = r.steps.at(i);
+//?       for (long long int j = 0; j < SIZE(inst.ingredients); ++j) {
+//?         if (!inst.ingredients.at(j).type) {
+//?           raise_error << maybe(r.name) << " '" << inst.to_string() << "' -- " << inst.ingredients.at(j).to_string() << " has no type\n" << end();
+//?           return;
+//?         }
+//?         if (!inst.ingredients.at(j).properties.at(0).second) {
+//?           raise_error << maybe(r.name) << " '" << inst.to_string() << "' -- " << inst.ingredients.at(j).to_string() << " has no type name\n" << end();
+//?           return;
+//?         }
+//?       }
+//?       for (long long int j = 0; j < SIZE(inst.products); ++j) {
+//?         if (!inst.products.at(j).type) {
+//?           raise_error << maybe(r.name) << " '" << inst.to_string() << "' -- " << inst.products.at(j).to_string() << " has no type\n" << end();
+//?           return;
+//?         }
+//?         if (!inst.products.at(j).properties.at(0).second) {
+//?           raise_error << maybe(r.name) << " '" << inst.to_string() << "' -- " << inst.products.at(j).to_string() << " has no type name\n" << end();
+//?           return;
+//?         }
+//?       }
+//?     }
+//?   }
+//? }
diff --git a/archive/2.vm/999spaces.cc b/archive/2.vm/999spaces.cc
new file mode 100644
index 00000000..adbbf8a9
--- /dev/null
+++ b/archive/2.vm/999spaces.cc
@@ -0,0 +1,86 @@
+//: Since different layers all carve out different parts of various namespaces
+//: (recipes, memory, etc.) for their own use, there's no previous place where
+//: we can lay out the big picture of what uses what. So we'll do that here
+//: and just have to manually remember to update it when we move boundaries
+//: around.
+//:
+//:: Memory
+//:
+//: Location 0 - unused (since it can help uncover bugs)
+//: Locations 1-899 - reserved for tests
+//: Locations 900-999 - reserved for predefined globals in Mu scenarios, like keyboard, screen, etc.
+:(before "End Reset")
+assert(Max_variables_in_scenarios == 900);
+//: Locations 1000 ('Reserved_for_tests') onward - available to the allocator in chunks of size Initial_memory_per_routine.
+assert(Reserved_for_tests == 1000);
+
+//:: Recipes
+//:
+//: 0 - unused (IDLE; do nothing)
+//: 1-199 - primitives
+assert(MAX_PRIMITIVE_RECIPES < 200);
+//: 200-999 - defined in .mu files as sequences of primitives
+assert(Next_recipe_ordinal == 1000);
+//: 1000 onwards - reserved for tests, cleared between tests
+
+//:: Depths for tracing
+//:
+//: 0 - errors
+//: 1-99 - app-level trace statements in Mu
+//: 100-9999 - call-stack statements (mostly label run)
+assert(Initial_callstack_depth == 100);
+
+//:: Summary of transforms and their dependencies
+//: begin transforms
+//:   begin instruction inserting transforms
+//:     52 insert fragments
+//:      ↳ 52.2 check fragments
+//:   ---
+//:     53 rewrite 'stash' instructions
+//:   end instruction inserting transforms
+//:
+//:   begin instruction modifying transforms
+//:     56.2 check header ingredients
+//:      ↳ 56.4 fill in return ingredients
+//:     48 check or set types by name
+//:
+//:     begin type modifying transforms
+//:       56.3 deduce types from header
+//:     ---
+//:       30 check or set invalid containers
+//:     end type modifying transforms
+//:         ↱ 46 collect surrounding spaces
+//:      ↳ 42 transform names
+//:         ↳ 57 static dispatch
+//:   ---
+//:     13 update instruction operation
+//:     40 transform braces
+//:     41 transform labels
+//:   end instruction modifying transforms
+//:    ↳ 60 check immutable ingredients
+//:
+//:   begin checks
+//:   ---
+//:     21 check instruction
+//:     ↳ 61 check indirect calls against header
+//:     ↳ 56 check calls against header
+//:     ↳ 43 transform 'new' to 'allocate'
+//:     30 check merge calls
+//:     36 check types of return instructions
+//:     43 check default space
+//:     56 check return instructions against header
+//:   end checks
+//: end transforms
+
+//:: Summary of type-checking in different phases
+//: when dispatching instructions we accept first recipe that:
+//:   strictly matches all types
+//:   maps literal 0 or literal 1 to boolean for some ingredients
+//:   performs some other acceptable type conversion
+//:     literal 0 -> address
+//:     literal -> character
+//: when checking instructions we ensure that types match, and that literals map to some scalar
+//:   (address can only map to literal 0)
+//:   (boolean can only map to literal 0 or literal 1)
+//:     (but conditionals can take any scalar)
+//: at runtime we perform no checks
diff --git a/archive/2.vm/Readme.md b/archive/2.vm/Readme.md
new file mode 100644
index 00000000..a7394530
--- /dev/null
+++ b/archive/2.vm/Readme.md
@@ -0,0 +1,449 @@
+Mu explores ways to turn arbitrary manual tests into reproducible automated
+tests. Hoped-for benefits:
+
+1. Projects release with confidence without requiring manual QA or causing
+   regressions for their users.
+
+1. Open source projects become easier for outsiders to comprehend, since they
+   can more confidently try out changes with the knowledge that they'll get
+   rapid feedback if they break something. Projects also become more
+   *rewrite-friendly* for insiders: it's easier to leave your project's
+   historical accidents and other baggage behind if you can be confident of
+   not causing regressions.
+
+1. It becomes easier to teach programming by emphasizing tests far earlier
+   than we do today.
+
+The hypothesis is that designing the entire system to be testable from day 1
+and from the ground up would radically impact the culture of an eco-system in
+a way that no bolted-on tool or service at higher levels can replicate. It
+would make it easier to write programs that can be [easily understood by newcomers](http://akkartik.name/about).
+It would reassure authors that an app is free from regression if all automated
+tests pass. It would make the stack easy to rewrite and simplify by dropping
+features, without fear that a subset of targeted apps might break. As a result
+people might fork projects more easily, and also exchange code between
+disparate forks more easily (copy the tests over, then try copying code over
+and making tests pass, rewriting and polishing where necessary). The community
+would have in effect a diversified portfolio of forks, a “wavefront” of
+possible combinations of features and alternative implementations of features
+instead of the single trunk with monotonically growing complexity that we get
+today. Application writers who wrote thorough tests for their apps (something
+they just can’t do today) would be able to bounce around between forks more
+easily without getting locked in to a single one as currently happens.
+
+In this quest, Mu is currently experimenting with the following mechanisms:
+
+1. New, testable interfaces for the operating system. Currently manual tests
+   are hard to automate because a file you rely on might be deleted, the
+   network might go down, etc. To make manual tests reproducible it suffices
+   to improve the 15 or so OS syscalls through which a computer talks to the
+   outside world. We have to allow programs to transparently write to a fake
+   screen, read from a fake disk/network, etc. In Mu, printing to screen
+   explicitly takes a screen object, so it can be called on the real screen,
+   or on a fake screen inside tests, so that we can then check the expected
+   state of the screen at the end of a test. Here's a test for a little
+   text-mode chessboard program in Mu (delimiting the edge of the 'screen'
+   with dots):
+
+   &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img alt='a screen test' src='https://github.com/akkartik/mu/html/archive/2.vm/chessboard-test.png'>
+
+   We've built up similarly *dependency-injected* interfaces to the keyboard,
+   mouse, disk and network.
+
+1. Support for testing side-effects like performance, deadlock-freedom,
+   race-freeness, memory usage, etc. Mu's *white-box tests* can check not just
+   the results of a function call, but also the presence or absence of
+   specific events in the log of its progress. For example, here's a test that
+   our string-comparison function doesn't scan individual characters unless it
+   has to:
+
+   &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img alt='white-box test' src='https://github.com/akkartik/mu/html/archive/2.vm/tracing-test.png'>
+
+   Another example: if a sort function logs each swap, a performance test can
+   check that the number of swaps doesn't quadruple when the size of the input
+   doubles.
+
+   Besides expanding the scope of tests, this ability also allows more
+   radical refactoring without needing to modify tests. All Mu's tests call a
+   top-level function rather than individual sub-systems directly. As a result
+   the way the subsystems are invoked can be radically changed (interface
+   changes, making synchronous functions asynchronous, etc.). As long as the
+   new versions emit the same implementation-independent events in the logs,
+   the tests will continue to pass. ([More information.](http://akkartik.name/post/tracing-tests))
+
+1. Organizing code and tests in layers of functionality, so that outsiders can
+   build simple and successively more complex versions of a project, gradually
+   enabling more peripheral features. Think of it as a cleaned-up `git log`
+   for the project. ([More information.](http://akkartik.name/post/wart-layers))
+
+These mechanisms exist in the context of a low-level statement-oriented
+language (like Basic, or Assembly). The language is as powerful as C for
+low-level pointer operations and manual memory management, but much safer,
+paying some run-time overhead to validate pointers. It also provides a number
+of features usually associated with higher-level languages: strong
+type-safety, function overloading, lexical scope, generic functions,
+higher-order functions, and [delimited continuations](http://akkartik.name/coroutines-in-mu).
+
+*Taking Mu for a spin*
+
+Mu is currently implemented in C++ and requires a Unix-like environment. It's
+been tested on Ubuntu, Mac OS X and OpenBSD; on x86, x86\_64 and ARMv7; and on
+recent versions of GCC and Clang. Since it uses no bleeding-edge language
+features and has no exotic dependencies, it should work with most reasonable
+versions, compilers or processors.
+
+Running Mu will always (re)compile it if necessary:
+
+  ```shell
+  $ cd mu/archives/2.vm
+  $ ./mu
+  ```
+
+As a simple example, here's a program with some arithmetic:
+
+<img alt='code example' src='https://github.com/akkartik/mu/html/archive/2.vm/example1.png'>
+
+Mu functions are lists of instructions, one to a line. Each instruction
+operates on some *ingredients* and returns some *products*.
+
+  ```
+  [products] <- instruction [ingredients]
+  ```
+
+Product and ingredient *reagents* cannot contain instructions or infix
+expressions. On the other hand, you can have any number of them. In
+particular, you can have any number of products. For example, you can perform
+integer division as follows:
+
+  ```
+  quotient:number, remainder:number <- divide-with-remainder 11, 3
+  ```
+
+Each reagent consists of a name and its type, separated by a colon. You only
+have to specify the type the first time you mention a name, but you can be
+more explicit if you choose. Types can be multiple words and even arbitrary
+trees, like:
+
+  ```nim
+  x:array:number:3  # x is an array of 3 numbers
+  y:list:number  # y is a list of numbers
+  # ':' is just syntactic sugar
+  {z: (map (address array character) (list number))}   # map from string to list of numbers
+  ```
+
+Try out the program now:
+
+  ```shell
+  $ ./mu example1.mu
+  $
+  ```
+
+Not much to see yet, since it doesn't print anything. To print the result, try
+adding the instruction `$print a` to the function.
+
+---
+
+Here's a second example, of a function that can take ingredients:
+
+<img alt='fahrenheit to celsius' src='https://github.com/akkartik/mu/html/archive/2.vm/f2c-1.png'>
+
+Functions can specify headers showing their expected ingredients and products,
+separated by `->` (unlike the `<-` in calls).
+
+Once defined, functions can be called just like primitives. No need to mess
+with a `CALL` instruction or push/pop arguments to the stack.
+
+Since Mu is a low-level VM language, it provides extra control at the cost of
+verbosity. Using `local-scope`, you have explicit control over stack frames to
+isolate your functions in a type-safe manner. You can also create more
+sophisticated setups like closures. One consequence of this extra control: you
+have to explicitly `load-ingredients` after you set up the stack.
+
+An alternative syntax is what the above example is converted to internally:
+
+<img alt='fahrenheit to celsius desugared' src='https://github.com/akkartik/mu/html/archive/2.vm/f2c-2.png'>
+
+The header gets dropped after checking types at call-sites, and after
+replacing `load-ingredients` with explicit instructions to load each
+ingredient separately, and to explicitly return products to the caller. After
+this translation functions are once again just lists of instructions.
+
+This alternative syntax isn't just an implementation detail. It turns out to
+be easier to teach functions to non-programmers by starting with this syntax,
+so that they can visualize a pipe from caller to callee, and see the names of
+variables get translated one by one through the pipe.
+
+---
+
+A third example, this time illustrating conditionals:
+
+<img alt='factorial example' src='https://github.com/akkartik/mu/html/archive/2.vm/factorial.png'>
+
+In spite of how it looks, this is still just a list of instructions and
+labels. Internally, the instructions `break` and `loop` get converted to
+`jump` instructions to after the enclosing `}` or `{` labels, respectively.
+
+Try out the factorial program now:
+
+  ```shell
+  $ ./mu factorial.mu
+  result: 120  # factorial of 5
+  ```
+
+You can also run its unit tests:
+
+  ```shell
+  $ ./mu test factorial.mu
+  ```
+
+Here's what one of the tests inside `factorial.mu` looks like:
+
+<img alt='test example' src='https://github.com/akkartik/mu/html/archive/2.vm/factorial-test.png'>
+
+Every test conceptually spins up a really lightweight virtual machine, so you
+can do things like check the value of specific locations in memory. You can
+also print to screen and check that the screen contains what you expect at the
+end of a test. For example, you've seen earlier how `chessboard.mu` checks the
+initial position of a game of chess (delimiting the edges of the screen with
+dots):
+
+<img alt='screen test' src='https://github.com/akkartik/mu/html/archive/2.vm/chessboard-test.png'>
+
+Similarly you can fake the keyboard to pretend someone typed something:
+
+<img alt='fake keyboard' src='https://github.com/akkartik/mu/html/archive/2.vm/fake-keyboard.png'>
+
+..or clicked somewhere:
+
+<img alt='fake console (keyboard, mouse, ..)' src='https://github.com/akkartik/mu/html/archive/2.vm/fake-console.png'>
+
+Within tests you can map arbitrary paths (local files or URLs) to contents:
+
+<img alt='fake file-system and network' src='https://github.com/akkartik/mu/html/archive/2.vm/resources.png'>
+
+As we add graphics, audio, and so on, we'll augment scenarios with
+corresponding abilities.
+
+---
+
+Mu assumes that all ingredients passed in to functions are immutable by
+default -- *unless* they are also products. So this program will throw an
+error:
+
+<img alt='immutable ingredient triggering an error' src='https://github.com/akkartik/mu/html/archive/2.vm/immutable-error.png'>
+
+To modify `foo`'s ingredient, you have to add it to the list of products
+returned:
+
+<img alt='mutable ingredient' src='https://github.com/akkartik/mu/html/archive/2.vm/mutable.png'>
+
+The names of the variables are important here: a function that takes an
+(immutable) address and returns a different one is different from a function
+that takes a mutable address (and also returns it).
+
+These immutability checks can be annoying, but the benefit they provide is
+that you can always tell what a function modifies just by looking at its
+header. In combination with dependency-injected hardware, they provide all the
+benefits of [referential transparency](https://en.wikipedia.org/wiki/Referential_transparency)
+that we typically associate with purely functional languages -- along with the
+option of imperatively modifying variables willy-nilly.
+
+---
+
+You can append arbitrary properties to reagents besides types. Just separate
+them with slashes.
+
+  ```nim
+  x:array:number:3/uninitialized
+  y:string/tainted:yes
+  z:number/assign-once:true/assigned:false
+  ```
+
+Most properties are meaningless to Mu, and it'll silently skip them when
+running, but they are fodder for *meta-programs* to check or modify your
+programs, a task other languages typically hide from their programmers. For
+example, where other programmers are restricted to the checks their type
+system permits and forces them to use, you'll learn to create new checks that
+make sense for your specific program. If it makes sense to perform different
+checks in different parts of your program, you'll be able to do that.
+
+You can imagine each reagent as a table, rows separated by slashes, columns
+within a row separated by colons. So the last example above would become
+something like this:
+
+  ```
+  z           : number   /
+  assign-once : true     /
+  assigned    : false
+  ```
+
+---
+
+An alternative way to define factorial is by inserting labels and later
+inserting code at them.
+
+<img alt='literate programming' src='https://github.com/akkartik/mu/html/archive/2.vm/tangle.png'>
+
+(You'll find this version in `tangle.mu`.)
+
+By convention we use the prefix '+' to indicate function-local label names you
+can jump to, and surround in '<>' global label names for inserting code at.
+
+---
+
+Another example, this time with concurrency:
+
+<img alt='forking concurrent routines' src='https://github.com/akkartik/mu/html/archive/2.vm/fork.png'>
+
+  ```shell
+  $ ./mu fork.mu
+  ```
+
+Notice that it repeatedly prints either '34' or '35' at random. Hit ctrl-c to
+stop.
+
+[Yet another example](https://github.com/akkartik/mu/blob/master/archive/2.vm/channel.mu) forks
+two 'routines' that communicate over a channel:
+
+  ```shell
+  $ ./mu channel.mu
+  produce: 0
+  produce: 1
+  produce: 2
+  produce: 3
+  consume: 0
+  consume: 1
+  consume: 2
+  produce: 4
+  consume: 3
+  consume: 4
+
+  # The exact order above might shift over time, but you'll never see a number
+  # consumed before it's produced.
+  ```
+
+Channels are the unit of synchronization in Mu. Blocking on a channel is the
+only way for the OS to put a task to sleep. The plan is to do all I/O over
+channels.
+
+Routines are expected to communicate purely by message passing, though nothing
+stops them from sharing memory since all routines share a common address
+space. However, idiomatic Mu will make it hard to accidentally read or
+clobber random memory locations. Bounds checking is baked deeply into
+the semantics, and using pointers after freeing them immediately fails.
+
+---
+
+Mu has a programming environment:
+
+  ```shell
+  $ ./mu edit
+  ```
+
+Screenshot:
+
+<img alt='programming environment' src='https://github.com/akkartik/mu/html/archive/2.vm/edit.png'>
+
+You write functions on the left and try them out in *sandboxes* on the right.
+Hit F4 to rerun all sandboxes with the latest version of the code. More
+details: http://akkartik.name/post/mu. Beware, it won't save your edits by
+default. But if you create a sub-directory called `lesson/` under `mu/` it
+will. If you turn that directory into a git repo with `git init`, it will also
+back up your changes each time you hit F4. Use the provided `new_lesson`
+script to take care of these details.
+
+Once you have a sandbox you can click on its result to mark it as expected:
+
+<img alt='expected result' src='https://github.com/akkartik/mu/html/archive/2.vm/expected-result.png'>
+
+Later if the result changes it'll be flagged in red to draw your attention to
+it. Thus, manually tested sandboxes become reproducible automated tests.
+
+<img alt='unexpected result' src='https://github.com/akkartik/mu/html/archive/2.vm/unexpected-result.png'>
+
+Another feature: Clicking on the code in a sandbox expands its trace for you
+to browse. To add to the trace, use `stash`. For example:
+
+  ```nim
+  stash [first ingredient is], x
+  ```
+
+Invaluable at times for understanding program behavior, but it won't clutter
+up your screen by default.
+
+---
+
+If you're still reading, here are some more things to check out:
+
+a) Look at the [chessboard program](https://github.com/akkartik/mu/blob/master/archive/2.vm/chessboard.mu)
+for a more complex example with tests of blocking reads from the keyboard and
+what gets printed to the screen -- things we don't typically associate with
+automated tests.
+
+b) Try running the tests:
+
+  ```shell
+  $ ./mu test
+  ```
+
+c) Check out [the programming environment](https://github.com/akkartik/mu/tree/master/edit#readme),
+the largest app built so far in Mu.
+
+d) Check out the tracing infrastructure which gives you a maps-like zoomable
+UI for browsing Mu's traces:
+
+  ```shell
+  $ ./mu --trace nqueens.mu  # just an example
+  saving trace to 'last_run'
+  $ ./browse_trace/browse_trace last_run
+  # hit 'q' to exit
+  ```
+
+For more details see the [Readme](browse_trace/Readme.md).
+
+e) Look at the `build` scripts. Mu's compilation process is itself designed to
+support staged learning. Each of the scripts (`build0`, `build1`, `build2`,
+etc.) is self-contained and can compile the project by itself. Successive
+versions add new features and configurability -- and complexity -- to the
+compilation process.
+
+f) Try skimming the source code. You should be able to get a pretty good sense
+for how things work just by skimming the files in order, skimming the top of
+each file and ignoring details lower down.
+[Some details on my unconventional approach to organizing projects.](http://akkartik.name/post/four-repos)
+
+**Credits**
+
+Mu builds on many ideas that have come before, especially:
+
+- [Peter Naur](http://alistair.cockburn.us/ASD+book+extract%3A+%22Naur,+Ehn,+Musashi%22)
+  for articulating the paramount problem of programming: communicating a
+  codebase to others;
+- [Christopher Alexander](http://www.amazon.com/Notes-Synthesis-Form-Harvard-Paperbacks/dp/0674627512)
+  and [Richard Gabriel](http://dreamsongs.net/Files/PatternsOfSoftware.pdf) for
+  the intellectual tools for reasoning about the higher order design of a
+  codebase;
+- Unix and C for showing us how to co-evolve language and OS, and for teaching
+  the (much maligned, misunderstood and underestimated) value of concise
+  *implementation* in addition to a clean interface;
+- Donald Knuth's [literate programming](http://www.literateprogramming.com/knuthweb.pdf)
+  for liberating "code for humans to read" from the tyranny of compiler order;
+- [David Parnas](http://www.cs.umd.edu/class/spring2003/cmsc838p/Design/criteria.pdf)
+  and others for highlighting the value of separating concerns and stepwise
+  refinement;
+- [Lisp](http://www.paulgraham.com/rootsoflisp.html) for showing the power of
+  dynamic languages, late binding and providing the right primitives *a la
+  carte*, especially lisp macros;
+- The folklore of debugging by print and the trace facility in many lisp
+  systems;
+- Automated tests for showing the value of developing programs inside an
+  elaborate harness;
+- [Python doctest](http://docs.python.org/2/library/doctest.html) for
+  exemplifying interactive documentation that doubles as tests;
+- [ReStructuredText](https://en.wikipedia.org/wiki/ReStructuredText)
+  and [its antecedents](https://en.wikipedia.org/wiki/Setext) for showing that
+  markup can be clean;
+- BDD for challenging us all to write tests at a higher level;
+- JavaScript and CSS for demonstrating the power of a DOM for complex
+  structured documents.
+- Rust for demonstrating that a system-programming language can be safe.
diff --git a/archive/2.vm/args.mu b/archive/2.vm/args.mu
new file mode 100644
index 00000000..3726f097
--- /dev/null
+++ b/archive/2.vm/args.mu
@@ -0,0 +1,8 @@
+# To provide commandline args to a Mu program, use '--'. In this case:
+#   $ ./mu args.mu -- abc
+#   abc
+def main text:text [
+  local-scope
+  load-inputs
+  $print text 10/newline
+]
diff --git a/archive/2.vm/build0 b/archive/2.vm/build0
new file mode 100755
index 00000000..f45a72d2
--- /dev/null
+++ b/archive/2.vm/build0
@@ -0,0 +1,46 @@
+#!/bin/sh
+# Compile mu from scratch.
+
+set -v
+set -e  # stop immediately on error
+
+cd ../../tangle
+  # auto-generate various lists (ending in '_list' by convention) {
+  # list of types
+  {
+    grep -h "^struct .* {" [0-9]*.cc  |sed 's/\(struct *[^ ]*\).*/\1;/'
+    grep -h "^typedef " [0-9]*.cc
+  }  > type_list
+  # list of function declarations, so I can define them in any order
+  grep -h "^[^ #].*) {" [0-9]*.cc  |sed 's/ {.*/;/'  > function_list
+  # list of code files to compile
+  ls [0-9]*.cc  |grep -v "\.test\.cc$"  |sed 's/.*/#include "&"/'  > file_list
+  # list of test files to compile
+  ls [0-9]*.test.cc  |sed 's/.*/#include "&"/'  > test_file_list
+  # list of tests to run
+  grep -h "^[[:space:]]*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {$/\1,/'  > test_list
+  grep -h "^\s*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  > test_name_list
+  # }
+  # Now that we have all the _lists, compile 'tangle'
+  g++ -std=c++98 -g -O2 boot.cc -o tangle
+  ./tangle test
+cd ../archive/2.vm
+
+cd termbox
+  gcc -g -O2 -c termbox.c
+  gcc -g -O2 -c utf8.c
+  ar rcs libtermbox.a *.o
+cd ..
+
+../../tangle/tangle [0-9]*.cc > mu.cc
+# auto-generate function declarations, so I can define them in any order
+# functions start out unindented, have all args on the same line, and end in ') {'
+#
+#                                      \/ ignore struct/class methods
+grep -h "^[^[:space:]#].*) {$" mu.cc  |grep -v ":.*("  |sed 's/ {.*/;/'  > function_list
+# auto-generate list of tests to run
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/\1,/'  > test_list
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  > test_name_list
+g++ -std=c++98 -g -O2 mu.cc termbox/libtermbox.a -o mu_bin
+
+cat [0-9]*.mu > core.mu
diff --git a/archive/2.vm/build1 b/archive/2.vm/build1
new file mode 100755
index 00000000..66bdb003
--- /dev/null
+++ b/archive/2.vm/build1
@@ -0,0 +1,69 @@
+#!/bin/sh
+# Alternative to build0 that supports a --until flag to include only a subset
+# of layers.
+#   $ ./build1 --until 050
+UNTIL_LAYER=${2:-zzz}
+
+set -v
+set -e  # stop immediately on error
+
+# Some environment variables that can be passed in. For example, to turn off
+# optimization:
+#   $ CFLAGS=-g ./build1
+test "$CXX" || export CXX=c++
+test "$CC" || export CC=cc
+test "$CFLAGS" || export CFLAGS="-g -O2"
+export CFLAGS="$CFLAGS -Wall -Wextra -ftrapv -fno-strict-aliasing"
+export CXXFLAGS="-std=c++98 $CFLAGS"  # CI has an ancient version; don't expect recent dialects
+
+# Outline:
+# [0-9]*.cc -> mu.cc -> mu_bin
+# (layers)   |        |
+#          tangle   $CXX
+
+$CXX $CFLAGS ../../enumerate/enumerate.cc -o ../../enumerate/enumerate
+
+cd ../../tangle
+  # auto-generate various lists (ending in '_list' by convention) {
+  # list of types
+  {
+    grep -h "^struct .* {" [0-9]*.cc  |sed 's/\(struct *[^ ]*\).*/\1;/'
+    grep -h "^typedef " [0-9]*.cc
+  }  > type_list
+  # list of function declarations, so I can define them in any order
+  grep -h "^[^ #].*) {" [0-9]*.cc  |sed 's/ {.*/;/'  > function_list
+  # list of code files to compile
+  ls [0-9]*.cc  |grep -v "\.test\.cc$"  |sed 's/.*/#include "&"/'  > file_list
+  # list of test files to compile
+  ls [0-9]*.test.cc  |sed 's/.*/#include "&"/'  > test_file_list
+  # list of tests to run
+  grep -h "^[[:space:]]*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {$/\1,/'  > test_list
+  grep -h "^\s*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  > test_name_list
+  # }
+  # Now that we have all the _lists, compile 'tangle'
+  $CXX $CXXFLAGS boot.cc -o tangle
+  ./tangle test
+cd ../archive/2.vm
+
+cd termbox
+  $CC $CFLAGS -c termbox.c
+  $CC $CFLAGS -c utf8.c
+  ar rcs libtermbox.a *.o
+cd ..
+
+LAYERS=$(../../enumerate/enumerate --until $UNTIL_LAYER  |grep '\.cc$')
+../../tangle/tangle $LAYERS  > mu.cc
+# auto-generate function declarations, so I can define them in any order
+# functions start out unindented, have all args on the same line, and end in ') {'
+#
+#                                      \/ ignore struct/class methods
+grep -h "^[^[:space:]#].*) {$" mu.cc  |grep -v ":.*("  |sed 's/ {.*/;/'  > function_list
+# auto-generate list of tests to run
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/\1,/'  > test_list
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  > test_name_list
+$CXX $CXXFLAGS mu.cc termbox/libtermbox.a -o mu_bin
+
+## [0-9]*.mu -> core.mu
+
+MU_LAYERS=$(../../enumerate/enumerate --until $UNTIL_LAYER  |grep '\.mu$') || exit 0  # ok if no .mu files
+cat $MU_LAYERS  > core.mu
diff --git a/archive/2.vm/build2 b/archive/2.vm/build2
new file mode 100755
index 00000000..b07e8a63
--- /dev/null
+++ b/archive/2.vm/build2
@@ -0,0 +1,175 @@
+#!/bin/sh
+# Alternative to build1 that tries to avoid redoing redundant work.
+# Also splits compilation into multiple .cc files (see 'cleave' below).
+# Faster than build1 for recompiling after small changes.
+#
+# For details on the basic form of this script, see https://notabug.org/akkartik/basic-build.
+
+set -e  # stop immediately on error
+
+# Some environment variables that can be passed in. For example, to turn off
+# optimization:
+#   $ CFLAGS=-g ./build2
+test "$CXX" || export CXX=c++
+test "$CC" || export CC=cc
+test "$CFLAGS" || export CFLAGS="-g -O2"
+export CFLAGS="$CFLAGS -Wall -Wextra -ftrapv -fno-strict-aliasing"
+export CXXFLAGS="-std=c++98 $CFLAGS"  # CI has an ancient version; don't expect recent dialects
+
+# Outline:
+# [0-9]*.cc -> mu.cc -> .build/*.cc -> .build/*.o -> .build/mu_bin
+# (layers)   |        |              |             |
+#          tangle  cleave          $CXX          $CXX
+
+# can also be called with a layer to only build until
+#   $ ./build2 --until 050
+UNTIL_LAYER=${2:-zzz}
+
+# there's two mechanisms for fast builds here:
+# - if a command is quick to run, always run it but update the result only on any change
+# - otherwise run it only if the output is 'older_than' the inputs
+#
+# avoid combining both mechanisms for a single file
+# otherwise you'll see spurious messages about files being updated
+# risk: a file may unnecessarily update without changes, causing unnecessary work downstream
+
+# return 1 if $1 is older than _any_ of the remaining args
+older_than() {
+  local target=$1
+  shift
+  if [ ! -e $target ]
+  then
+#?     echo "$target doesn't exist"
+    echo "updating $target" >&2
+    return 0  # success
+  fi
+  local f
+  for f in $*
+  do
+    if [ $f -nt $target ]
+    then
+      echo "updating $target" >&2
+      return 0  # success
+    fi
+  done
+  return 1  # failure
+}
+
+# redirect to $1, unless it's already identical
+update() {
+  if [ ! -e $1 ]
+  then
+    cat > $1
+  else
+    cat > $1.tmp
+    diff -q $1 $1.tmp >/dev/null  &&  rm $1.tmp  ||  mv $1.tmp $1
+  fi
+}
+
+update_cp() {
+  if [ ! -e $2/$1 ]
+  then
+    cp $1 $2
+  elif [ $1 -nt $2/$1 ]
+  then
+    cp $1 $2
+  fi
+}
+
+noisy_cd() {
+  cd $1
+  echo "-- `pwd`" >&2
+}
+
+older_than ../../enumerate/enumerate ../../enumerate/enumerate.cc && {
+  $CXX $CXXFLAGS ../../enumerate/enumerate.cc -o ../../enumerate/enumerate
+}
+
+older_than ../../tangle/tangle tangle/*.cc && {
+  noisy_cd ../../tangle
+    # auto-generate various lists (ending in '_list' by convention) {
+    # list of types
+    {
+      grep -h "^struct .* {" [0-9]*.cc  |sed 's/\(struct *[^ ]*\).*/\1;/'
+      grep -h "^typedef " [0-9]*.cc
+    }  |update type_list
+    # list of function declarations, so I can define them in any order
+    grep -h "^[^ #].*) {" [0-9]*.cc  |sed 's/ {.*/;/'  |update function_list
+    # list of code files to compile
+    ls [0-9]*.cc  |grep -v "\.test\.cc$"  |sed 's/.*/#include "&"/'  |update file_list
+    # list of test files to compile
+    ls [0-9]*.test.cc  |sed 's/.*/#include "&"/'  |update test_file_list
+    # list of tests to run
+    grep -h "^[[:space:]]*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {$/\1,/'  |update test_list
+    grep -h "^\s*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update test_name_list
+    # }
+    # Now that we have all the _lists, compile 'tangle'
+    $CXX $CXXFLAGS boot.cc -o tangle
+    ./tangle test
+  noisy_cd ../archive/2.vm  # no effect; just to show us returning to the parent directory
+}
+
+LAYERS=$(../../enumerate/enumerate --until $UNTIL_LAYER  |grep '\.cc$')
+older_than mu.cc $LAYERS ../../enumerate/enumerate ../../tangle/tangle && {
+  # no update here; rely on 'update' calls downstream
+  ../../tangle/tangle $LAYERS  > mu.cc
+}
+
+older_than ../../cleave/cleave ../../cleave/cleave.cc && {
+  $CXX $CXXFLAGS ../../cleave/cleave.cc -o ../../cleave/cleave
+  rm -rf .build
+}
+
+mkdir -p .build
+# auto-generate function declarations, so I can define them in any order
+# functions start out unindented, have all args on the same line, and end in ') {'
+#
+#                                      \/ ignore struct/class methods
+grep -h "^[^[:space:]#].*) {$" mu.cc  |grep -v ":.*("  |sed 's/ {.*/;/'  |update .build/function_list
+# auto-generate list of tests to run
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/\1,/'  |update .build/test_list
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update .build/test_name_list
+mkdir -p .build/termbox
+update_cp termbox/termbox.h .build/termbox
+
+older_than mu_bin mu.cc *_list ../../cleave/cleave termbox/* && {
+  ../../cleave/cleave mu.cc .build
+  noisy_cd .build
+    # create the list of global variable declarations from the corresponding definitions
+    grep ';' global_definitions_list  |sed 's/[=(].*/;/'  |sed 's/^[^\/# ]/extern &/'  |sed 's/^extern extern /extern /'  |update global_declarations_list
+    for f in mu_*.cc
+    do
+      older_than `echo $f  |sed 's/\.cc$/.o/'` $f header global_declarations_list function_list test_list && {
+        $CXX $CXXFLAGS -c $f
+      }
+    done
+  noisy_cd ../termbox
+    older_than utf8.o utf8.c && {
+      $CC $CFLAGS -c utf8.c
+    }
+    older_than termbox.o termbox.c termbox.h input.inl output.inl bytebuffer.inl && {
+      $CC $CFLAGS -c termbox.c
+    }
+    older_than libtermbox.a *.o && {
+      ar rcs libtermbox.a *.o
+    }
+  noisy_cd ..
+  $CXX $CXXFLAGS .build/*.o termbox/libtermbox.a -o .build/mu_bin
+  cp .build/mu_bin .
+}
+
+## [0-9]*.mu -> core.mu
+
+MU_LAYERS=$(../../enumerate/enumerate --until $UNTIL_LAYER  |grep '\.mu$') || exit 0  # ok if no .mu files
+cat $MU_LAYERS  |update core.mu
+
+exit 0
+
+# scenarios considered:
+#   0 status when nothing needs updating
+#   no output when nothing needs updating
+#     no output for mu.cc when .mu files modified
+#     touch mu.cc but don't modify it; no output on second build
+#     touch a .cc layer but don't modify it; no output on second build
+#   only a single layer is recompiled when changing a C++ function
+#   stop immediately after failure in tangle
diff --git a/archive/2.vm/build3 b/archive/2.vm/build3
new file mode 100755
index 00000000..42e8ffd9
--- /dev/null
+++ b/archive/2.vm/build3
@@ -0,0 +1,201 @@
+#!/bin/sh
+# Alternative to build2 that can stop after any step. For example:
+#   $ ./build3 mu.cc
+
+set -e  # stop immediately on error
+
+# Some environment variables that can be passed in. For example, to turn off
+# optimization:
+#   $ CFLAGS=-g ./build3
+test "$CXX" || export CXX=c++
+test "$CC" || export CC=cc
+test "$CFLAGS" || export CFLAGS="-g -O2"
+export CFLAGS="$CFLAGS -Wall -Wextra -ftrapv -fno-strict-aliasing"
+export CXXFLAGS="-std=c++98 $CFLAGS"  # CI has an ancient version; don't expect recent dialects
+
+# Outline:
+# [0-9]*.cc -> mu.cc -> .build/*.cc -> .build/*.o -> .build/mu_bin
+# (layers)   |        |              |             |
+#          tangle  cleave          $CXX          $CXX
+
+## arg parsing
+
+# can be called with a target to stop after a partial build
+#   $ ./build3 mu.cc
+# can also be called with a layer to only build until
+#   $ ./build3 --until 050
+# scenarios:
+#   ./build3              => TARGET=  UNTIL_LAYER=zzz
+#   ./build3 x            => TARGET=x UNTIL_LAYER=zzz
+#   ./build3 --until      => TARGET=  UNTIL_LAYER=zzz
+#   ./build3 --until 050  => TARGET=  UNTIL_LAYER=050
+TARGET=
+UNTIL_LAYER=zzz
+if [ $# -ge 1 ] && [ $1 != "--until" ]
+then
+  TARGET=$1
+fi
+if [ $# -ge 2 ] && [ $1 = "--until" ]
+then
+  UNTIL_LAYER=$2
+fi
+
+##
+
+# there's two mechanisms for fast builds here:
+# - if a command is quick to run, always run it but update the result only on any change
+# - otherwise run it only if the output is 'older_than' the inputs
+#
+# avoid combining both mechanisms for a single file
+# otherwise you'll see spurious messages about files being updated
+# risk: a file may unnecessarily update without changes, causing unnecessary work downstream
+
+# return 1 if $1 is older than _any_ of the remaining args
+# also exit the entire script if previous invocation was to update $TARGET
+older_than() {
+  test $TARGET  &&  test "$last_target" = "$TARGET"  &&  exit 0
+  local target=$1
+  shift
+  last_target=$target
+  if [ ! -e $target ]
+  then
+#?     echo "$target doesn't exist"
+    echo "updating $target" >&2
+    return 0  # success
+  fi
+  local f
+  for f in $*
+  do
+    if [ $f -nt $target ]
+    then
+      echo "updating $target" >&2
+      return 0  # success
+    fi
+  done
+  return 1  # failure
+}
+
+# redirect to $1, unless it's already identical
+# no point checking for an early exit, because this usually runs in a pipeline/subshell
+update() {
+  if [ ! -e $1 ]
+  then
+    cat > $1
+  else
+    cat > $1.tmp
+    diff -q $1 $1.tmp >/dev/null  &&  rm $1.tmp  ||  mv $1.tmp $1
+  fi
+}
+
+# cp file $1 to directory $2, unless it's already identical
+# also exit the entire script if previous invocation was to update $TARGET
+update_cp() {
+  test $TARGET  &&  test "$last_target" = "$TARGET"  &&  exit 0
+  last_target=$2/$1
+  if [ ! -e $2/$1 ]
+  then
+    cp $1 $2
+  elif [ $1 -nt $2/$1 ]
+  then
+    cp $1 $2
+  fi
+}
+
+noisy_cd() {
+  cd $1
+  echo "-- `pwd`" >&2
+}
+
+older_than ../../enumerate/enumerate ../../enumerate/enumerate.cc && {
+  $CXX $CXXFLAGS ../../enumerate/enumerate.cc -o ../../enumerate/enumerate
+}
+
+older_than ../../tangle/tangle ../../tangle/*.cc && {
+  noisy_cd ../../tangle
+    # auto-generate various lists (ending in '_list' by convention) {
+    # list of types
+    {
+      grep -h "^struct .* {" [0-9]*.cc  |sed 's/\(struct *[^ ]*\).*/\1;/'
+      grep -h "^typedef " [0-9]*.cc
+    }  |update type_list
+    # list of function declarations, so I can define them in any order
+    grep -h "^[^ #].*) {" [0-9]*.cc  |sed 's/ {.*/;/'  |update function_list
+    # list of code files to compile
+    ls [0-9]*.cc  |grep -v "\.test\.cc$"  |sed 's/.*/#include "&"/'  |update file_list
+    # list of test files to compile
+    ls [0-9]*.test.cc  |sed 's/.*/#include "&"/'  |update test_file_list
+    # list of tests to run
+    grep -h "^[[:space:]]*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {$/\1,/'  |update test_list
+    grep -h "^\s*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update test_name_list
+    # }
+    # Now that we have all the _lists, compile 'tangle'
+    $CXX $CXXFLAGS boot.cc -o tangle
+    ./tangle test
+  noisy_cd ../archive/2.vm  # no effect; just to show us returning to the parent directory
+}
+
+LAYERS=$(../../enumerate/enumerate --until $UNTIL_LAYER  |grep '\.cc$')
+older_than mu.cc $LAYERS ../../enumerate/enumerate ../../tangle/tangle && {
+  # no update here; rely on 'update' calls downstream
+  ../../tangle/tangle $LAYERS  > mu.cc
+}
+
+older_than ../../cleave/cleave ../../cleave/cleave.cc && {
+  $CXX $CXXFLAGS ../../cleave/cleave.cc -o ../../cleave/cleave
+  rm -rf .build
+}
+
+mkdir -p .build
+# auto-generate function declarations, so I can define them in any order
+# functions start out unindented, have all args on the same line, and end in ') {'
+#
+#                                      \/ ignore struct/class methods
+grep -h "^[^[:space:]#].*) {$" mu.cc  |grep -v ":.*("  |sed 's/ {.*/;/'  |update .build/function_list
+# auto-generate list of tests to run
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/\1,/'  |update .build/test_list
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update .build/test_name_list
+mkdir -p .build/termbox
+update_cp termbox/termbox.h .build/termbox
+
+older_than mu_bin mu.cc *_list ../../cleave/cleave termbox/* && {
+  ../../cleave/cleave mu.cc .build
+  noisy_cd .build
+    # create the list of global variable declarations from the corresponding definitions
+    grep ';' global_definitions_list  |sed 's/[=(].*/;/'  |sed 's/^[^\/# ]/extern &/'  |sed 's/^extern extern /extern /'  |update global_declarations_list
+    for f in mu_*.cc
+    do
+      older_than `echo $f  |sed 's/\.cc$/.o/'` $f header global_declarations_list function_list test_list && {
+        $CXX $CXXFLAGS -c $f
+      }
+    done
+  noisy_cd ../termbox
+    older_than utf8.o utf8.c && {
+      $CC $CFLAGS -c utf8.c
+    }
+    older_than termbox.o termbox.c termbox.h input.inl output.inl bytebuffer.inl && {
+      $CC $CFLAGS -c termbox.c
+    }
+    older_than libtermbox.a *.o && {
+      ar rcs libtermbox.a *.o
+    }
+  noisy_cd ..
+  $CXX $CXXFLAGS .build/*.o termbox/libtermbox.a -o .build/mu_bin
+  cp .build/mu_bin .
+}
+
+## [0-9]*.mu -> core.mu
+
+MU_LAYERS=$(../../enumerate/enumerate --until $UNTIL_LAYER  |grep '\.mu$') || exit 0  # ok if no .mu files
+cat $MU_LAYERS  |update core.mu
+
+exit 0
+
+# scenarios considered:
+#   0 status when nothing needs updating
+#   no output when nothing needs updating
+#     no output for mu.cc when .mu files modified
+#     touch mu.cc but don't modify it; no output on second build
+#     touch a .cc layer but don't modify it; no output on second build
+#   only a single layer is recompiled when changing a C++ function
+#   stop immediately after failure in tangle
+#   stop immediately after target provided at commandline
diff --git a/archive/2.vm/build4 b/archive/2.vm/build4
new file mode 100755
index 00000000..a1483089
--- /dev/null
+++ b/archive/2.vm/build4
@@ -0,0 +1,297 @@
+#!/bin/sh
+# Experimental alternative to build2 that can run steps in parallel if their
+# dependencies are met. Caveats:
+#
+#   1. We rely on the OS to schedule steps, so thousands of independent tasks
+#      will likely be counter-productive.
+#   2. Can run out of virtual memory if you spawn too many say $CC processes.
+#   3. Compilation errors can cause the script to hang. We tag the most common
+#      suspects with '|| quit', but can't eliminate the possibility entirely.
+#   4. Ugly as heck! This version really benefits from comparisons with its
+#      'upstream', build2. And even then, diff gets confused.
+#   5. There's a mechanical difficulty: we use mktemp to reliably create
+#      temporary filenames, which has the side effect of also creating the
+#      files. So zero-size files are considered equivalent to non-existent
+#      files. When some commands have trouble with this (e.g. ar) we need to
+#      delete the empty file, which can expose us to a race condition wrt
+#      mktemp.
+
+set -e  # stop immediately on error
+
+# Some environment variables that can be passed in. For example, to turn off
+# optimization:
+#   $ CFLAGS=-g ./build4
+test "$CXX" || export CXX=c++
+test "$CC" || export CC=cc
+test "$CFLAGS" || export CFLAGS="-g -O2"
+export CFLAGS="$CFLAGS -Wall -Wextra -ftrapv -fno-strict-aliasing"
+export CXXFLAGS="-std=c++98 $CFLAGS"  # CI has an ancient version; don't expect recent dialects
+
+# Outline:
+# [0-9]*.cc -> mu.cc -> .build/*.cc -> .build/*.o -> .build/mu_bin
+# (layers)   |        |              |             |
+#          tangle  cleave          $CXX          $CXX
+
+# can also be called with a layer to only build until
+#   $ ./build4 --until 050
+UNTIL_LAYER=${2:-zzz}
+
+# there's two mechanisms for fast builds here:
+# - if a command is quick to run, always run it but update the result only on any change
+# - otherwise run it only if the output is 'older_than' the inputs
+#
+# avoid combining both mechanisms for a single file
+# otherwise you'll see spurious messages about files being updated
+# risk: a file may unnecessarily update without changes, causing unnecessary work downstream
+
+# return 1 if $1 is older than _any_ of the remaining args
+older_than() {
+  local target=$1
+  shift
+  if [ ! -s $target ]
+  then
+#?     echo "$target has size zero"
+#?     echo "updating $target" >&2
+    return 0  # success
+  fi
+  local f
+  for f in $*
+  do
+    if [ $f -nt $target ]
+    then
+#?       echo "updating $target" >&2
+      return 0  # success
+    fi
+  done
+  return 1  # failure
+}
+
+# redirect to $1, unless it's already identical
+update() {
+  if [ ! -e $1 ]
+  then
+    cat > $1
+  else
+    cat > $1.tmp
+    diff -q $1 $1.tmp >/dev/null  &&  rm $1.tmp  ||  mv $1.tmp $1
+  fi
+}
+
+update_cp() {
+  if [ ! -e $2/$1 ]
+  then
+    cp $1 $2
+  elif [ $1 -nt $2/$1 ]
+  then
+    cp $1 $2
+  fi
+}
+
+noisy_cd() {
+  cd $1
+  echo "-- `pwd`" >&2
+}
+
+mv_if_exists() {
+  test -e $1  &&  mv $1 $2
+  return 0
+}
+
+# wait for all the given filenames to exist
+# exit immediately if a special file called '.quit' exists
+QUITFILE=`pwd`/.quit
+rm -f $QUITFILE
+wait_for_all() {
+  # could use inotify on Linux
+  while ! all_exist "$@"
+  do
+#?     echo waiting: $*
+    test -e $QUITFILE  &&  return 1  # some step had an error; stop all waiting steps using errexit
+    sleep 1
+  done
+  return 0
+}
+quit() {
+  touch $QUITFILE
+  exit 1
+}
+
+all_exist() {
+  for f in "$@"
+  do
+    test -e $f  ||  return 1
+  done
+  return 0
+}
+
+TMP=`mktemp`
+mv_if_exists ../../enumerate/enumerate $TMP
+(
+  wait_for_all ../../enumerate/enumerate.cc
+  older_than $TMP ../../enumerate/enumerate.cc && {
+    echo "building enumerate"
+    $CXX $CXXFLAGS ../../enumerate/enumerate.cc -o $TMP  ||  quit
+    echo "done building enumerate"
+  }
+  mv $TMP ../../enumerate/enumerate
+) &
+
+TMP=`mktemp`
+mv_if_exists ../../tangle/tangle $TMP
+(
+  wait_for_all ../../tangle/*.cc
+  older_than $TMP ../../tangle/*.cc && {
+    echo "building tangle"
+    cd ../../tangle
+      # auto-generate various lists (ending in '_list' by convention) {
+      # list of types
+      {
+        grep -h "^struct .* {" [0-9]*.cc  |sed 's/\(struct *[^ ]*\).*/\1;/'
+        grep -h "^typedef " [0-9]*.cc
+      }  |update type_list
+      # list of function declarations, so I can define them in any order
+      grep -h "^[^ #].*) {" [0-9]*.cc  |sed 's/ {.*/;/'  |update function_list
+      # list of code files to compile
+      ls [0-9]*.cc  |grep -v "\.test\.cc$"  |sed 's/.*/#include "&"/'  |update file_list
+      # list of test files to compile
+      ls [0-9]*.test.cc  |sed 's/.*/#include "&"/'  |update test_file_list
+      # list of tests to run
+      grep -h "^[[:space:]]*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {$/\1,/'  |update test_list
+      grep -h "^\s*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update test_name_list
+      # }
+      # Now that we have all the _lists, compile 'tangle'
+      $CXX $CXXFLAGS boot.cc -o $TMP  ||  quit
+      $TMP test
+    cd ../archive/2.vm
+    echo "done building tangle"
+  }
+  mv $TMP ../../tangle/tangle
+) &
+
+wait_for_all ../../enumerate/enumerate
+echo "enumerating layers"
+LAYERS=$(../../enumerate/enumerate --until $UNTIL_LAYER  |grep '\.cc$')
+echo "done enumerating layers"
+
+TMP=`mktemp`
+mv_if_exists mu.cc $TMP
+(
+  wait_for_all $LAYERS ../../enumerate/enumerate ../../tangle/tangle
+  older_than $TMP $LAYERS ../../enumerate/enumerate ../../tangle/tangle && {
+    echo "running tangle"
+    # no update here; rely on 'update' calls downstream
+    ../../tangle/tangle $LAYERS >$TMP  ||  quit
+    echo "done running tangle"
+  }
+  mv $TMP mu.cc
+) &
+
+TMP=`mktemp`
+mv_if_exists ../../cleave/cleave $TMP
+(
+  wait_for_all ../../cleave/cleave.cc
+  older_than $TMP ../../cleave/cleave.cc && {
+    echo "building cleave"
+    $CXX $CXXFLAGS ../../cleave/cleave.cc -o $TMP  ||  quit
+    rm -rf .build
+    echo "done building cleave"
+  }
+  mv $TMP ../../cleave/cleave
+) &
+
+wait_for_all mu.cc ../../cleave/cleave  # cleave/cleave just for the .build cleanup
+mkdir -p .build
+# auto-generate function declarations, so I can define them in any order
+# functions start out unindented, have all args on the same line, and end in ') {'
+#
+#                                      \/ ignore struct/class methods
+grep -h "^[^[:space:]#].*) {$" mu.cc  |grep -v ":.*("  |sed 's/ {.*/;/'  |update .build/function_list
+# auto-generate list of tests to run
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/\1,/'  |update .build/test_list
+grep -h "^\s*void test_" mu.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update .build/test_name_list
+mkdir -p .build/termbox
+update_cp termbox/termbox.h .build/termbox
+
+TMP=`mktemp`
+mv_if_exists mu_bin $TMP
+(
+  wait_for_all mu.cc ../../cleave/cleave termbox/*.c termbox/*.h termbox/*.inl
+  older_than $TMP mu.cc *_list ../../cleave/cleave termbox/* && {
+    echo "building mu_bin"
+    ../../cleave/cleave mu.cc .build  ||  quit
+    cd .build
+      # create the list of global variable declarations from the corresponding definitions
+      grep ';' global_definitions_list  |sed 's/[=(].*/;/'  |sed 's/^[^\/# ]/extern &/'  |sed 's/^extern extern /extern /'  |update global_declarations_list
+      for f in mu_*.cc
+      do
+        OBJ=`echo $f |sed 's/\.cc$/.o/'`
+        TMP=`mktemp`
+        mv_if_exists $OBJ $TMP
+        (
+          older_than $TMP $f header global_declarations_list function_list test_list && {
+            echo "building $OBJ"
+            $CXX $CXXFLAGS -c $f -o $TMP  ||  quit
+            echo "done building $OBJ"
+          }
+          mv $TMP $OBJ
+        ) &
+      done
+    cd ../termbox
+      TMP=`mktemp`
+      mv_if_exists utf8.o $TMP
+      (
+        older_than $TMP utf8.c && {
+          echo "building termbox/utf8.o"
+          $CC $CFLAGS -c utf8.c -o $TMP  ||  quit
+          echo "done building termbox/utf8.o"
+        }
+        mv $TMP utf8.o
+      ) &
+      TMP=`mktemp`
+      mv_if_exists termbox.o $TMP
+      (
+        older_than $TMP termbox.c termbox.h input.inl output.inl bytebuffer.inl && {
+          echo "building termbox/termbox.o"
+          $CC $CFLAGS -c termbox.c -o $TMP  ||  quit
+          echo "done building termbox/termbox.o"
+        }
+        mv $TMP termbox.o
+      ) &
+      TMP=`mktemp`
+      mv_if_exists libtermbox.a $TMP
+      (
+        wait_for_all termbox.o utf8.o
+        older_than $TMP termbox.o utf8.o && {
+          echo "building termbox/libtermbox.a"
+          rm $TMP;  ar rcs $TMP termbox.o utf8.o  ||  quit  # race condition; later mktemp may end up reusing this file
+          echo "done building termbox/libtermbox.a"
+        }
+        mv $TMP libtermbox.a
+      ) &
+    cd ..
+    MU_OBJS=`echo .build/mu_*.cc |sed 's/\.cc/.o/g'`
+    echo wait_for_all $MU_OBJS termbox/libtermbox.a
+    wait_for_all $MU_OBJS termbox/libtermbox.a
+    echo "building .build/mu_bin"
+    $CXX $CXXFLAGS $MU_OBJS termbox/libtermbox.a -o $TMP  ||  quit
+    echo "done building .build/mu_bin"
+    echo "done building mu_bin"
+  }
+  mv $TMP mu_bin
+) &
+
+## [0-9]*.mu -> core.mu
+
+wait_for_all ../../enumerate/enumerate
+echo "building core.mu"
+MU_LAYERS=$(../../enumerate/enumerate --until $UNTIL_LAYER  |grep '\.mu$') || exit 0  # ok if no .mu files
+cat $MU_LAYERS  |update core.mu
+echo "done building core.mu"
+
+wait_for_all mu_bin
+exit 0
+
+# scenarios considered:
+#   0 status when nothing needs updating
+#   only a single layer is recompiled when changing a C++ function
+#   stop immediately after any failure
diff --git a/archive/2.vm/build_and_test_until b/archive/2.vm/build_and_test_until
new file mode 100755
index 00000000..ed560e36
--- /dev/null
+++ b/archive/2.vm/build_and_test_until
@@ -0,0 +1,18 @@
+#!/bin/sh
+# Run tests for just a subset of layers.
+#
+# Usage:
+#   build_and_test_until [file prefix] [test name]
+# Provide the second arg to run just a single test.
+set -e
+
+# clean previous builds if they were building until a different layer
+touch .until
+PREV_UNTIL=`cat .until`
+if [ "$PREV_UNTIL" != $1 ]
+then
+  ./clean top-level
+  echo $1 > .until
+fi
+
+./build3 --until $1  &&  ./mu_bin test $2
diff --git a/archive/2.vm/cannot_write_tests_for b/archive/2.vm/cannot_write_tests_for
new file mode 100644
index 00000000..444aafd5
--- /dev/null
+++ b/archive/2.vm/cannot_write_tests_for
@@ -0,0 +1,17 @@
+0. main's ingredients
+1. assertion failures or errors inside scenarios
+2. screen background color
+3. has-more-events?
+4. hide/show screen
+5. more touch event types
+6. sandbox isolation
+7. errors in reading/writing files (missing directory, others?)
+
+termbox issues are implementation-specific and not worth testing:
+  whether we clear junk from other processes
+  latency in interpreting low-level escape characters
+
+calls to update-cursor are currently duplicated:
+  render-all calls update-cursor to simplify testing
+  event-loop needs to call update-cursor explicitly to backstop branches doing their own minimal rendering
+  solution: update-cursor after minimal rendering
diff --git a/archive/2.vm/channel.mu b/archive/2.vm/channel.mu
new file mode 100644
index 00000000..4a553148
--- /dev/null
+++ b/archive/2.vm/channel.mu
@@ -0,0 +1,45 @@
+# example program: communicating between routines using channels
+
+def producer sink:&:sink:char -> sink:&:sink:char [
+  # produce characters 1 to 5 on a channel
+  local-scope
+  load-inputs
+  # n = 0
+  n:char <- copy 0
+  {
+    done?:bool <- lesser-than n, 5
+    break-unless done?
+    # other threads might get between these prints
+    $print [produce: ], n, [ 
+]
+    sink <- write sink, n
+    n <- add n, 1
+    loop
+  }
+  close sink
+]
+
+def consumer source:&:source:char -> source:&:source:char [
+  # consume and print integers from a channel
+  local-scope
+  load-inputs
+  {
+    # read an integer from the channel
+    n:char, eof?:bool, source <- read source
+    break-if eof?
+    # other threads might get between these prints
+    $print [consume: ], n:char, [ 
+]
+    loop
+  }
+]
+
+def main [
+  local-scope
+  source:&:source:char, sink:&:sink:char <- new-channel 3/capacity
+  # create two background 'routines' that communicate by a channel
+  routine1:num <- start-running producer, sink
+  routine2:num <- start-running consumer, source
+  wait-for-routine routine1
+  wait-for-routine routine2
+]
diff --git a/archive/2.vm/chessboard.mu b/archive/2.vm/chessboard.mu
new file mode 100644
index 00000000..09c85188
--- /dev/null
+++ b/archive/2.vm/chessboard.mu
@@ -0,0 +1,572 @@
+# Chessboard program: you type in moves in algebraic notation, and it'll
+# display the position after each move.
+
+def main [
+  local-scope
+  open-console  # take control of screen, keyboard and mouse
+  clear-screen null/screen  # non-scrolling app
+
+  # The chessboard function takes keyboard and screen objects as inputs.
+  #
+  # In Mu it is good form (though not required) to explicitly state what
+  # hardware a function needs.
+  #
+  # Here the console and screen are both null, which usually indicates real
+  # hardware rather than a fake for testing as you'll see below.
+  chessboard null/screen, null/console
+
+  close-console  # clean up screen, keyboard and mouse
+]
+
+## But enough about Mu. Here's what it looks like to run the chessboard program.
+
+scenario print-board-and-read-move [
+  local-scope
+  trace-until 100/app
+  # we'll make the screen really wide because the program currently prints out a long line
+  assume-screen 120/width, 20/height
+  # initialize keyboard to type in a move
+  assume-console [
+    type [a2-a4
+]
+  ]
+  run [
+    screen, console <- chessboard screen, console
+    # icon for the cursor
+    cursor-icon:char <- copy 9251/␣
+    screen <- print screen, cursor-icon
+  ]
+  screen-should-contain [
+  #            1         2         3         4         5         6         7         8         9         10        11
+  #  012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
+    .Stupid text-mode chessboard. White pieces in uppercase; black pieces in lowercase. No checking for legal moves.         .
+    .                                                                                                                        .
+    .8 | r n b q k b n r                                                                                                     .
+    .7 | p p p p p p p p                                                                                                     .
+    .6 |                                                                                                                     .
+    .5 |                                                                                                                     .
+    .4 | P                                                                                                                   .
+    .3 |                                                                                                                     .
+    .2 |   P P P P P P P                                                                                                     .
+    .1 | R N B Q K B N R                                                                                                     .
+    .  +----------------                                                                                                     .
+    .    a b c d e f g h                                                                                                     .
+    .                                                                                                                        .
+    .Type in your move as <from square>-<to square>. For example: 'a2-a4'. Then press <enter>.                               .
+    .                                                                                                                        .
+    .Hit 'q' to exit.                                                                                                        .
+    .                                                                                                                        .
+    .move: ␣                                                                                                                 .
+    .                                                                                                                        .
+    .                                                                                                                        .
+  ]
+]
+
+## Here's how 'chessboard' is implemented.
+
+type board = &:@:&:@:char  # a 2-D array of arrays of characters
+
+def chessboard screen:&:screen, console:&:console -> screen:&:screen, console:&:console [
+  local-scope
+  load-inputs
+  board:board <- initial-position
+  # hook up stdin
+  stdin-in:&:source:char, stdin-out:&:sink:char <- new-channel 10/capacity
+  start-running send-keys-to-channel, console, stdin-out, screen
+  # buffer lines in stdin
+  buffered-stdin-in:&:source:char, buffered-stdin-out:&:sink:char <- new-channel 10/capacity
+  start-running buffer-lines, stdin-in, buffered-stdin-out
+  {
+    print screen, [Stupid text-mode chessboard. White pieces in uppercase; black pieces in lowercase. No checking for legal moves.
+]
+    cursor-to-next-line screen
+    print screen, board
+    cursor-to-next-line screen
+    print screen, [Type in your move as <from square>-<to square>. For example: 'a2-a4'. Then press <enter>.
+]
+    cursor-to-next-line screen
+    print screen [Hit 'q' to exit.
+]
+    {
+      cursor-to-next-line screen
+      screen <- print screen, [move: ]
+      m:&:move, quit:bool, error:bool <- read-move buffered-stdin-in, screen
+      break-if quit, +quit
+      buffered-stdin-in <- clear buffered-stdin-in  # cleanup after error. todo: test this?
+      loop-if error
+    }
+    board <- make-move board, m
+    screen <- clear-screen screen
+    loop
+  }
+  +quit
+]
+
+## a board is an array of files, a file is an array of characters (squares)
+
+def new-board initial-position:&:@:char -> board:board [
+  local-scope
+  load-inputs
+  # assert(length(initial-position) == 64)
+  len:num <- length *initial-position
+  correct-length?:bool <- equal len, 64
+  assert correct-length?, [chessboard had incorrect size]
+  # board is an array of pointers to files; file is an array of characters
+  board <- new {(address array character): type}, 8
+  col:num <- copy 0
+  {
+    done?:bool <- equal col, 8
+    break-if done?
+    file:&:@:char <- new-file initial-position, col
+    *board <- put-index *board, col, file
+    col <- add col, 1
+    loop
+  }
+]
+
+def new-file position:&:@:char, index:num -> result:&:@:char [
+  local-scope
+  load-inputs
+  index <- multiply index, 8
+  result <- new character:type, 8
+  row:num <- copy 0
+  {
+    done?:bool <- equal row, 8
+    break-if done?
+    square:char <- index *position, index
+    *result <- put-index *result, row, square
+    row <- add row, 1
+    index <- add index, 1
+    loop
+  }
+]
+
+def print screen:&:screen, board:board -> screen:&:screen [
+  local-scope
+  load-inputs
+  row:num <- copy 7  # start printing from the top of the board
+  space:char <- copy 32/space
+  # print each row
+  {
+    done?:bool <- lesser-than row, 0
+    break-if done?
+    # print rank number as a legend
+    rank:num <- add row, 1
+    print screen, rank
+    print screen, [ | ]
+    # print each square in the row
+    col:num <- copy 0
+    {
+      done?:bool <- equal col:num, 8
+      break-if done?
+      f:&:@:char <- index *board, col
+      c:char <- index *f, row
+      bg:num <- square-color row, col
+      print screen, c, 7/white, bg
+      print screen, space
+      col <- add col, 1
+      loop
+    }
+    row <- subtract row, 1
+    cursor-to-next-line screen
+    loop
+  }
+  # print file letters as legend
+  print screen, [  +----------------]
+  cursor-to-next-line screen
+  print screen, [    a b c d e f g h]
+  cursor-to-next-line screen
+]
+
+def square-color row:num, col:num -> result:num [
+  local-scope
+  load-inputs
+  result <- copy 0/black
+  x:num <- add row, col
+  _, rem:num <- divide-with-remainder x, 2
+  return-if rem, 238
+]
+
+def initial-position -> board:board [
+  local-scope
+  # layout in memory (in raster order):
+  #   R P _ _ _ _ p r
+  #   N P _ _ _ _ p n
+  #   B P _ _ _ _ p b
+  #   Q P _ _ _ _ p q
+  #   K P _ _ _ _ p k
+  #   B P _ _ _ _ p B
+  #   N P _ _ _ _ p n
+  #   R P _ _ _ _ p r
+  initial-position:&:@:char <- new-array 82/R, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 114/r, 78/N, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 110/n, 66/B, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 98/b, 81/Q, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 113/q, 75/K, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 107/k, 66/B, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 98/b, 78/N, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 110/n, 82/R, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 114/r
+#?       82/R, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 114/r,
+#?       78/N, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 110/n,
+#?       66/B, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 98/b, 
+#?       81/Q, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 113/q,
+#?       75/K, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 107/k,
+#?       66/B, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 98/b,
+#?       78/N, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 110/n,
+#?       82/R, 80/P, 32/blank, 32/blank, 32/blank, 32/blank, 112/p, 114/r
+  board <- new-board initial-position
+]
+
+scenario printing-the-board [
+  local-scope
+  board:board <- initial-position
+  assume-screen 30/width, 12/height
+  run [
+    screen <- print screen, board
+  ]
+  screen-should-contain [
+  #  012345678901234567890123456789
+    .8 | r n b q k b n r           .
+    .7 | p p p p p p p p           .
+    .6 |                           .
+    .5 |                           .
+    .4 |                           .
+    .3 |                           .
+    .2 | P P P P P P P P           .
+    .1 | R N B Q K B N R           .
+    .  +----------------           .
+    .    a b c d e f g h           .
+    .                              .
+    .                              .
+  ]
+]
+
+## data structure: move
+
+container move [
+  # valid range: 0-7
+  from-file:num
+  from-rank:num
+  to-file:num
+  to-rank:num
+]
+
+# prints only error messages to screen
+def read-move stdin:&:source:char, screen:&:screen -> result:&:move, quit?:bool, error?:bool, stdin:&:source:char, screen:&:screen [
+  local-scope
+  load-inputs
+  from-file:num, quit?:bool, error?:bool <- read-file stdin, screen
+  return-if quit?, null/dummy
+  return-if error?, null/dummy
+  # construct the move object
+  result:&:move <- new move:type
+  *result <- put *result, from-file:offset, from-file
+  from-rank:num, quit?, error? <- read-rank stdin, screen
+  return-if quit?, null/dummy
+  return-if error?, null/dummy
+  *result <- put *result, from-rank:offset, from-rank
+  error? <- expect-from-channel stdin, 45/dash, screen
+  return-if error?, null/dummy, false/quit
+  to-file:num, quit?, error? <- read-file stdin, screen
+  return-if quit?, null/dummy
+  return-if error?, null/dummy
+  *result <- put *result, to-file:offset, to-file
+  to-rank:num, quit?, error? <- read-rank stdin, screen
+  return-if quit?, null/dummy
+  return-if error?, null/dummy
+  *result <- put *result, to-rank:offset, to-rank
+  error? <- expect-from-channel stdin, 10/newline, screen
+  return-if error?, null/dummy, false/quit
+]
+
+# valid values for file: 0-7
+def read-file stdin:&:source:char, screen:&:screen -> file:num, quit:bool, error:bool, stdin:&:source:char, screen:&:screen [
+  local-scope
+  load-inputs
+  c:char, eof?:bool, stdin <- read stdin
+  return-if eof?, 0/dummy, true/quit, false/no-error
+  q-pressed?:bool <- equal c, 81/Q
+  return-if q-pressed?, 0/dummy, true/quit, false/no-error
+  q-pressed? <- equal c, 113/q
+  return-if q-pressed?, 0/dummy, true/quit, false/no-error
+  empty-fake-keyboard?:bool <- equal c, 0/eof
+  return-if empty-fake-keyboard?, 0/dummy, true/quit, false/no-error
+  {
+    newline?:bool <- equal c, 10/newline
+    break-unless newline?
+    print screen, [that's not enough]
+    return 0/dummy, false/don't-quit, true/error
+  }
+  file:num <- subtract c, 97/a
+  # 'a' <= file <= 'h'
+  {
+    above-min:bool <- greater-or-equal file, 0
+    break-if above-min
+    print screen, [file too low: ]
+    print screen, c
+    cursor-to-next-line screen
+    return 0/dummy, false/don't-quit, true/error
+  }
+  {
+    below-max:bool <- lesser-than file, 8
+    break-if below-max
+    print screen, [file too high: ]
+    print screen, c
+    return 0/dummy, false/don't-quit, true/error
+  }
+  return file, false/don't-quit, false/no-error
+]
+
+# valid values for rank: 0-7
+def read-rank stdin:&:source:char, screen:&:screen -> rank:num, quit?:bool, error?:bool, stdin:&:source:char, screen:&:screen [
+  local-scope
+  load-inputs
+  c:char, eof?:bool, stdin <- read stdin
+  return-if eof?, 0/dummy, true/quit, false/no-error
+  q-pressed?:bool <- equal c, 81/Q
+  return-if q-pressed?, 0/dummy, true/quit, false/no-error
+  q-pressed? <- equal c, 113/q
+  return-if q-pressed?, 0/dummy, true/quit, false/no-error
+  empty-fake-keyboard?:bool <- equal c, 0/eof
+  return-if empty-fake-keyboard?, 0/dummy, true/quit, false/no-error
+  {
+    newline?:bool <- equal c, 10  # newline
+    break-unless newline?
+    print screen, [that's not enough]
+    return 0/dummy, false/don't-quite, true/error
+  }
+  rank:num <- subtract c, 49/'1'
+  # assert'1' <= rank <= '8'
+  {
+    above-min:bool <- greater-or-equal rank, 0
+    break-if above-min
+    print screen, [rank too low: ]
+    print screen, c
+    return 0/dummy, false/don't-quite, true/error
+  }
+  {
+    below-max:bool <- lesser-or-equal rank, 7
+    break-if below-max
+    print screen, [rank too high: ]
+    print screen, c
+    return 0/dummy, false/don't-quite, true/error
+  }
+  return rank, false/don't-quite, false/no-error
+]
+
+# read a character from the given channel and check that it's what we expect
+# return true on error
+def expect-from-channel stdin:&:source:char, expected:char, screen:&:screen -> result:bool, stdin:&:source:char, screen:&:screen [
+  local-scope
+  load-inputs
+  c:char, eof?:bool, stdin <- read stdin
+  return-if eof? true
+  {
+    match?:bool <- equal c, expected
+    break-if match?
+    print screen, [expected character not found]
+  }
+  result <- not match?
+]
+
+scenario read-move-blocking [
+  local-scope
+  assume-screen 20/width, 2/height
+  source:&:source:char, sink:&:sink:char <- new-channel 2/capacity
+  read-move-routine:num/routine <- start-running read-move, source, screen
+  run [
+    # 'read-move' is waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state:num <- routine-state read-move-routine
+    waiting?:bool <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-blocking: routine failed to pause after coming up (before any keys were pressed)]
+    # press 'a'
+    sink <- write sink, 97/a
+    restart read-move-routine
+    # 'read-move' still waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state <- routine-state read-move-routine
+    waiting? <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-blocking: routine failed to pause after rank 'a']
+    # press '2'
+    sink <- write sink, 50/'2'
+    restart read-move-routine
+    # 'read-move' still waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state <- routine-state read-move-routine
+    waiting? <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-blocking: routine failed to pause after file 'a2']
+    # press '-'
+    sink <- write sink, 45/'-'
+    restart read-move-routine
+    # 'read-move' still waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state <- routine-state read-move-routine
+    waiting? <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-blocking: routine failed to pause after hyphen 'a2-']
+    # press 'a'
+    sink <- write sink, 97/a
+    restart read-move-routine
+    # 'read-move' still waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state <- routine-state read-move-routine
+    waiting? <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-blocking: routine failed to pause after rank 'a2-a']
+    # press '4'
+    sink <- write sink, 52/'4'
+    restart read-move-routine
+    # 'read-move' still waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state <- routine-state read-move-routine
+    waiting? <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-blocking: routine failed to pause after file 'a2-a4']
+    # press 'newline'
+    sink <- write sink, 10  # newline
+    restart read-move-routine
+    # 'read-move' now completes
+    wait-for-routine-to-block read-move-routine
+    read-move-state <- routine-state read-move-routine
+    completed?:bool <- equal read-move-state, 1/completed
+    assert completed?, [ 
+F read-move-blocking: routine failed to terminate on newline]
+    trace 1, [test], [reached end]
+  ]
+  trace-should-contain [
+    test: reached end
+  ]
+]
+
+scenario read-move-quit [
+  local-scope
+  assume-screen 20/width, 2/height
+  source:&:source:char, sink:&:sink:char <- new-channel 2/capacity
+  read-move-routine:num <- start-running read-move, source, screen
+  run [
+    # 'read-move' is waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state:num <- routine-state read-move-routine
+    waiting?:bool <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-quit: routine failed to pause after coming up (before any keys were pressed)]
+    # press 'q'
+    sink <- write sink, 113/q
+    restart read-move-routine
+    # 'read-move' completes
+    wait-for-routine-to-block read-move-routine
+    read-move-state <- routine-state read-move-routine
+    completed?:bool <- equal read-move-state, 1/completed
+    assert completed?, [ 
+F read-move-quit: routine failed to terminate on 'q']
+    trace 1, [test], [reached end]
+  ]
+  trace-should-contain [
+    test: reached end
+  ]
+]
+
+scenario read-move-illegal-file [
+  local-scope
+  assume-screen 20/width, 2/height
+  source:&:source:char, sink:&:sink:char <- new-channel 2/capacity
+  read-move-routine:num <- start-running read-move, source, screen
+  run [
+    # 'read-move' is waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state:num <- routine-state read-move-routine
+    waiting?:bool <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-illegal-file: routine failed to pause after coming up (before any keys were pressed)]
+    sink <- write sink, 50/'2'
+    restart read-move-routine
+    wait-for-routine-to-block read-move-routine
+  ]
+  screen-should-contain [
+    .file too low: 2     .
+    .                    .
+  ]
+]
+
+scenario read-move-illegal-rank [
+  local-scope
+  assume-screen 20/width, 2/height
+  source:&:source:char, sink:&:sink:char <- new-channel 2/capacity
+  read-move-routine:num <- start-running read-move, source, screen
+  run [
+    # 'read-move' is waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state:num <- routine-state read-move-routine
+    waiting?:bool <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-illegal-rank: routine failed to pause after coming up (before any keys were pressed)]
+    sink <- write sink, 97/a
+    sink <- write sink, 97/a
+    restart read-move-routine
+    wait-for-routine-to-block read-move-routine
+  ]
+  screen-should-contain [
+    .rank too high: a    .
+    .                    .
+  ]
+]
+
+scenario read-move-empty [
+  local-scope
+  assume-screen 20/width, 2/height
+  source:&:source:char, sink:&:sink:char <- new-channel 2/capacity
+  read-move-routine:num <- start-running read-move, source, screen
+  run [
+    # 'read-move' is waiting for keypress
+    wait-for-routine-to-block read-move-routine
+    read-move-state:num <- routine-state read-move-routine
+    waiting?:bool <- not-equal read-move-state, 2/discontinued
+    assert waiting?, [ 
+F read-move-empty: routine failed to pause after coming up (before any keys were pressed)]
+    sink <- write sink, 10/newline
+    sink <- write sink, 97/a
+    restart read-move-routine
+    wait-for-routine-to-block read-move-routine
+  ]
+  screen-should-contain [
+    .that's not enough   .
+    .                    .
+  ]
+]
+
+def make-move board:board, m:&:move -> board:board [
+  local-scope
+  load-inputs
+  from-file:num <- get *m, from-file:offset
+  from-rank:num <- get *m, from-rank:offset
+  to-file:num <- get *m, to-file:offset
+  to-rank:num <- get *m, to-rank:offset
+  from-f:&:@:char <- index *board, from-file
+  to-f:&:@:char <- index *board, to-file
+  src:char/square <- index *from-f, from-rank
+  *to-f <- put-index *to-f, to-rank, src
+  *from-f <- put-index *from-f, from-rank, 32/space
+]
+
+scenario making-a-move [
+  local-scope
+  assume-screen 30/width, 12/height
+  board:board <- initial-position
+  move:&:move <- new move:type
+  *move <- merge 6/g, 1/'2', 6/g, 3/'4'
+  run [
+    board <- make-move board, move
+    screen <- print screen, board
+  ]
+  screen-should-contain [
+  #  012345678901234567890123456789
+    .8 | r n b q k b n r           .
+    .7 | p p p p p p p p           .
+    .6 |                           .
+    .5 |                           .
+    .4 |             P             .
+    .3 |                           .
+    .2 | P P P P P P   P           .
+    .1 | R N B Q K B N R           .
+    .  +----------------           .
+    .    a b c d e f g h           .
+    .                              .
+  ]
+]
diff --git a/archive/2.vm/clean b/archive/2.vm/clean
new file mode 100755
index 00000000..24a0300f
--- /dev/null
+++ b/archive/2.vm/clean
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -e
+
+set -v
+rm -rf mu.cc core.mu mu_bin* *_list .build
+rm -rf termbox/*.o termbox/libtermbox.a
+rm -rf .until .quit
+test $# -gt 0 && exit 0  # convenience: 'clean top-level' to leave subsidiary tools alone
+rm -rf ../../enumerate/enumerate ../../tangle/tangle ../../tangle/*_list ../../cleave/cleave ../../*/*.dSYM
diff --git a/archive/2.vm/console.mu b/archive/2.vm/console.mu
new file mode 100644
index 00000000..cc81c232
--- /dev/null
+++ b/archive/2.vm/console.mu
@@ -0,0 +1,16 @@
+# example program: reading events from keyboard or mouse
+#
+# Keeps printing 'a' until you press a key or click on the mouse.
+
+def main [
+  local-scope
+  open-console
+  {
+    e:event, found?:bool <- check-for-interaction
+    break-if found?
+    print-character-to-display 97, 7/white
+    loop
+  }
+  close-console
+  $print e, 10/newline
+]
diff --git a/archive/2.vm/continuation1.mu b/archive/2.vm/continuation1.mu
new file mode 100644
index 00000000..8276e188
--- /dev/null
+++ b/archive/2.vm/continuation1.mu
@@ -0,0 +1,25 @@
+# Example program showing that 'return-continuation-until-mark' can 'pause' a
+# function call, returning a continuation, and that calling the continuation
+# can 'resume' the paused function call.
+#
+# To run:
+#   $ git clone https://github.com/akkartik/mu
+#   $ cd mu
+#   $ ./mu continuation1.mu
+#
+# Expected output:
+#   1
+
+def main [
+  local-scope
+  k:continuation <- call-with-continuation-mark 100/mark, create-yielder
+  x:num <- call k  # should return 1
+  $print x 10/newline
+]
+
+def create-yielder -> n:num [
+  local-scope
+  load-inputs
+  return-continuation-until-mark 100/mark
+  return 1
+]
diff --git a/archive/2.vm/continuation2.mu b/archive/2.vm/continuation2.mu
new file mode 100644
index 00000000..45a65e9f
--- /dev/null
+++ b/archive/2.vm/continuation2.mu
@@ -0,0 +1,37 @@
+# Example program showing that a 'paused' continuation can be 'resumed'
+# multiple times from the same point (but with changes to data).
+#
+# To run:
+#   $ git clone https://github.com/akkartik/mu
+#   $ cd mu
+#   $ ./mu continuation2.mu
+#
+# Expected output:
+#   1
+#   2
+#   3
+
+def main [
+  local-scope
+  l:&:list:num <- copy null
+  l <- push 3, l
+  l <- push 2, l
+  l <- push 1, l
+  k:continuation <- call-with-continuation-mark 100/mark, create-yielder, l
+  {
+    x:num, done?:bool <- call k
+    break-if done?
+    $print x 10/newline
+    loop
+  }
+]
+
+def create-yielder l:&:list:num -> n:num, done?:bool [
+  local-scope
+  load-inputs
+  return-continuation-until-mark 100/mark
+  done? <- equal l, null
+  return-if done?, 0/dummy
+  n <- first l
+  l <- rest l
+]
diff --git a/archive/2.vm/continuation3.mu b/archive/2.vm/continuation3.mu
new file mode 100644
index 00000000..cde60958
--- /dev/null
+++ b/archive/2.vm/continuation3.mu
@@ -0,0 +1,34 @@
+# Example program showing that a function call can be 'paused' multiple times,
+# creating different continuation values.
+#
+# To run:
+#   $ git clone https://github.com/akkartik/mu
+#   $ cd mu
+#   $ ./mu continuation3.mu
+#
+# Expected output:
+#   caller 0
+#   callee 0
+#   caller 1
+#   callee 1
+#   caller 2
+#   callee 2
+
+def main [
+  local-scope
+  $print [caller 0] 10/newline
+  k:continuation <- call-with-continuation-mark 100/mark, f
+  $print [caller 1] 10/newline
+  k <- call k
+  $print [caller 2] 10/newline
+  call k
+]
+
+def f [
+  local-scope
+  $print [callee 0] 10/newline
+  return-continuation-until-mark 100/mark
+  $print [callee 1] 10/newline
+  return-continuation-until-mark 100/mark
+  $print [callee 2] 10/newline
+]
diff --git a/archive/2.vm/continuation4.mu b/archive/2.vm/continuation4.mu
new file mode 100644
index 00000000..1a523fe9
--- /dev/null
+++ b/archive/2.vm/continuation4.mu
@@ -0,0 +1,47 @@
+# Example program showing 'return-continuation-until-mark' return other values
+# alongside continuations.
+#
+# Print out a given list of numbers.
+#
+# To run:
+#   $ git clone https://github.com/akkartik/mu
+#   $ cd mu
+#   $ ./mu continuation4.mu
+#
+# Expected output:
+#   1
+#   2
+#   3
+
+def main [
+  local-scope
+  l:&:list:num <- copy null
+  l <- push 3, l
+  l <- push 2, l
+  l <- push 1, l
+  k:continuation, x:num, done?:bool <- call-with-continuation-mark 100/mark, create-yielder, l
+  {
+    break-if done?
+    $print x 10/newline
+    k, x:num, done?:bool <- call k
+    loop
+  }
+]
+
+def create-yielder l:&:list:num -> n:num, done?:bool [
+  local-scope
+  load-inputs
+  {
+    done? <- equal l, null
+    break-if done?
+    n <- first l
+    l <- rest l
+    return-continuation-until-mark 100/mark, n, done?
+    loop
+  }
+  # A function that returns continuations shouldn't get the opportunity to
+  # return. Calling functions should stop calling its continuation after this
+  # point.
+  return-continuation-until-mark 100/mark, -1, done?
+  assert false, [called too many times, ran out of continuations to return]
+]
diff --git a/archive/2.vm/continuation5.mu b/archive/2.vm/continuation5.mu
new file mode 100644
index 00000000..295cb9c9
--- /dev/null
+++ b/archive/2.vm/continuation5.mu
@@ -0,0 +1,49 @@
+# Example program showing that a 'paused' continuation can be 'resumed' with
+# inputs.
+#
+# Print out a list of numbers, first adding 0 to the first, 1 to the second, 2
+# to the third, and so on.
+#
+# To run:
+#   $ git clone https://github.com/akkartik/mu
+#   $ cd mu
+#   $ ./mu continuation5.mu
+#
+# Expected output:
+#   1
+#   3
+#   5
+
+def main [
+  local-scope
+  l:&:list:num <- copy null
+  l <- push 3, l
+  l <- push 2, l
+  l <- push 1, l
+  k:continuation, x:num, done?:bool <- call-with-continuation-mark 100/mark, create-yielder, l
+  a:num <- copy 1
+  {
+    break-if done?
+    $print x 10/newline
+    k, x:num, done?:bool <- call k, a  # resume; x = a + next l value
+    a <- add a, 1
+    loop
+  }
+]
+
+def create-yielder l:&:list:num -> n:num, done?:bool [
+  local-scope
+  load-inputs
+  a:num <- copy 0
+  {
+    done? <- equal l, null
+    break-if done?
+    n <- first l
+    l <- rest l
+    n <- add n, a
+    a <- return-continuation-until-mark 100/mark, n, done?  # pause/resume
+    loop
+  }
+  return-continuation-until-mark 100/mark, -1, done?
+  assert false, [called too many times, ran out of continuations to return]
+]
diff --git a/archive/2.vm/copy_mu b/archive/2.vm/copy_mu
new file mode 100755
index 00000000..cd4da455
--- /dev/null
+++ b/archive/2.vm/copy_mu
@@ -0,0 +1,11 @@
+#!/usr/bin/env zsh
+# Copy binaries across mu directories for different students on a single
+# server, so we only need to build them once.
+
+cp -r $1/enumerate/enumerate $2/enumerate
+cp -r $1/cleave/cleave $2/cleave
+rm -rf $2/.build
+cp -r $1/.build $2
+cp -r $1/mu_bin $2
+cd $2
+./mu  # couple of things still get recompiled, but should now be quick
diff --git a/archive/2.vm/counters.mu b/archive/2.vm/counters.mu
new file mode 100644
index 00000000..ea2fa77d
--- /dev/null
+++ b/archive/2.vm/counters.mu
@@ -0,0 +1,29 @@
+# example program: maintain multiple counters with isolated lexical scopes
+# (spaces)
+
+def new-counter n:num -> default-space:space [
+  default-space <- new location:type, 30
+  load-inputs  # initialize n
+]
+
+def increment-counter outer:space/names:new-counter, x:num -> n:num/space:1 [
+  local-scope
+  load-inputs
+  0:space/names:new-counter <- copy outer  # setup outer space; it *must* come from 'new-counter'
+  n/space:1 <- add n/space:1, x
+]
+
+def main [
+  local-scope
+  # counter A
+  a:space/names:new-counter <- new-counter 34
+  # counter B
+  b:space/names:new-counter <- new-counter 23
+  # increment both by 2 but in different ways
+  increment-counter a, 1
+  b-value:num <- increment-counter b, 2
+  a-value:num <- increment-counter a, 1
+  # check results
+  $print [Contents of counters], 10/newline
+  $print [a: ], a-value, [ b: ], b-value, 10/newline
+]
diff --git a/archive/2.vm/display.mu b/archive/2.vm/display.mu
new file mode 100644
index 00000000..c3844c4b
--- /dev/null
+++ b/archive/2.vm/display.mu
@@ -0,0 +1,25 @@
+# example program: managing the display
+
+def main [
+  open-console
+  clear-display
+  print-character-to-display 97, 1/red, 2/green
+  1:num/raw, 2:num/raw <- cursor-position-on-display
+  wait-for-some-interaction
+  clear-line-on-display
+  move-cursor-on-display 0, 4
+  print-character-to-display 98
+  wait-for-some-interaction
+  move-cursor-on-display 0, 0
+  clear-line-on-display
+  wait-for-some-interaction
+  move-cursor-down-on-display
+  wait-for-some-interaction
+  move-cursor-right-on-display
+  wait-for-some-interaction
+  move-cursor-left-on-display
+  wait-for-some-interaction
+  move-cursor-up-on-display
+  wait-for-some-interaction
+  close-console
+]
diff --git a/archive/2.vm/edit/001-editor.mu b/archive/2.vm/edit/001-editor.mu
new file mode 100644
index 00000000..b3399dbb
--- /dev/null
+++ b/archive/2.vm/edit/001-editor.mu
@@ -0,0 +1,464 @@
+## the basic editor data structure, and how it displays text to the screen
+
+# temporary main for this layer: just render the given text at the given
+# screen dimensions, then stop
+def main text:text [
+  local-scope
+  load-inputs
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  e:&:editor <- new-editor text, 0/left, 5/right
+  render null/screen, e
+  wait-for-event null/console
+  close-console
+]
+
+scenario editor-renders-text-to-screen [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    # top line of screen reserved for menu
+    .          .
+    .abc       .
+    .          .
+  ]
+]
+
+container editor [
+  # editable text: doubly linked list of characters (head contains a special sentinel)
+  data:&:duplex-list:char
+  top-of-screen:&:duplex-list:char
+  bottom-of-screen:&:duplex-list:char
+  # location before cursor inside data
+  before-cursor:&:duplex-list:char
+
+  # raw bounds of display area on screen
+  # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
+  left:num
+  right:num
+  bottom:num
+  # raw screen coordinates of cursor
+  cursor-row:num
+  cursor-column:num
+]
+
+# creates a new editor widget
+#   right is exclusive
+def new-editor s:text, left:num, right:num -> result:&:editor [
+  local-scope
+  load-inputs
+  # no clipping of bounds
+  right <- subtract right, 1
+  result <- new editor:type
+  # initialize screen-related fields
+  *result <- put *result, left:offset, left
+  *result <- put *result, right:offset, right
+  # initialize cursor coordinates
+  *result <- put *result, cursor-row:offset, 1/top
+  *result <- put *result, cursor-column:offset, left
+  # initialize empty contents
+  init:&:duplex-list:char <- push 167/§, null
+  *result <- put *result, data:offset, init
+  *result <- put *result, top-of-screen:offset, init
+  *result <- put *result, before-cursor:offset, init
+  result <- insert-text result, s
+  <editor-initialization>
+]
+
+def insert-text editor:&:editor, text:text -> editor:&:editor [
+  local-scope
+  load-inputs
+  curr:&:duplex-list:char <- get *editor, data:offset
+  insert curr, text
+]
+
+scenario editor-initializes-without-data [
+  local-scope
+  assume-screen 5/width, 3/height
+  run [
+    e:&:editor <- new-editor null/data, 2/left, 5/right
+    1:editor/raw <- copy *e
+  ]
+  memory-should-contain [
+    # 1,2 (data) <- just the § sentinel
+    # 3,4 (top of screen) <- the § sentinel
+    # 5 (bottom of screen) <- null since text fits on screen
+    5 <- 0
+    6 <- 0
+    # 7,8 (before cursor) <- the § sentinel
+    9 <- 2  # left
+    10 <- 4  # right  (inclusive)
+    11 <- 0  # bottom (not set until render)
+    12 <- 1  # cursor row
+    13 <- 2  # cursor column
+  ]
+  screen-should-contain [
+    .     .
+    .     .
+    .     .
+  ]
+]
+
+# Assumes cursor should be at coordinates (cursor-row, cursor-column) and
+# updates before-cursor to match. Might also move coordinates if they're
+# outside text.
+def render screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor, 1/top, 0/left
+  left:num <- get *editor, left:offset
+  screen-height:num <- screen-height screen
+  right:num <- get *editor, right:offset
+  # traversing editor
+  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
+  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
+  curr <- next curr
+  # traversing screen
+  color:num <- copy 7/white
+  row:num <- copy 1/top
+  column:num <- copy left
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  screen <- move-cursor screen, row, column
+  {
+    +next-character
+    break-unless curr
+    off-screen?:bool <- greater-or-equal row, screen-height
+    break-if off-screen?
+    # update editor.before-cursor
+    # Doing so at the start of each iteration ensures it stays one step behind
+    # the current character.
+    {
+      at-cursor-row?:bool <- equal row, cursor-row
+      break-unless at-cursor-row?
+      at-cursor?:bool <- equal column, cursor-column
+      break-unless at-cursor?
+      before-cursor <- copy prev
+    }
+    c:char <- get *curr, value:offset
+    <character-c-received>
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # adjust cursor if necessary
+      {
+        at-cursor-row?:bool <- equal row, cursor-row
+        break-unless at-cursor-row?
+        left-of-cursor?:bool <- lesser-than column, cursor-column
+        break-unless left-of-cursor?
+        cursor-column <- copy column
+        before-cursor <- prev curr
+      }
+      # clear rest of line in this window
+      clear-line-until screen, right
+      # skip to next line
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      curr <- next curr
+      prev <- next prev
+      loop +next-character
+    }
+    {
+      # at right? wrap. even if there's only one more letter left; we need
+      # room for clicking on the cursor after it.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment curr
+      loop +next-character
+    }
+    print screen, c, color
+    curr <- next curr
+    prev <- next prev
+    column <- add column, 1
+    loop
+  }
+  # save first character off-screen
+  *editor <- put *editor, bottom-of-screen:offset, curr
+  # is cursor to the right of the last line? move to end
+  {
+    at-cursor-row?:bool <- equal row, cursor-row
+    cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
+    before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
+    above-cursor-row?:bool <- lesser-than row, cursor-row
+    before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
+    break-unless before-cursor?
+    cursor-row <- copy row
+    cursor-column <- copy column
+    before-cursor <- copy prev
+  }
+  *editor <- put *editor, bottom:offset, row
+  *editor <- put *editor, cursor-row:offset, cursor-row
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  clear-line-until screen, right
+  row <- add row, 1
+  return row, left/column
+]
+
+def clear-screen-from screen:&:screen, row:num, column:num, left:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  # if it's the real screen, use the optimized primitive
+  {
+    break-if screen
+    clear-display-from row, column, left, right
+    return
+  }
+  # if not, go the slower route
+  screen <- move-cursor screen, row, column
+  clear-line-until screen, right
+  clear-rest-of-screen screen, row, left, right
+]
+
+def clear-rest-of-screen screen:&:screen, row:num, left:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  row <- add row, 1
+  # if it's the real screen, use the optimized primitive
+  {
+    break-if screen
+    clear-display-from row, left, left, right
+    return
+  }
+  screen <- move-cursor screen, row, left
+  screen-height:num <- screen-height screen
+  {
+    at-bottom-of-screen?:bool <- greater-or-equal row, screen-height
+    break-if at-bottom-of-screen?
+    screen <- move-cursor screen, row, left
+    clear-line-until screen, right
+    row <- add row, 1
+    loop
+  }
+]
+
+scenario editor-prints-multiple-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    .abc  .
+    .def  .
+    .     .
+  ]
+]
+
+scenario editor-handles-offsets [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [abc], 1/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    . abc .
+    .     .
+  ]
+]
+
+scenario editor-prints-multiple-lines-at-offset [
+  local-scope
+  assume-screen 5/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 1/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    . abc .
+    . def .
+    .     .
+  ]
+]
+
+scenario editor-wraps-long-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [abc def], 0/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    .abc ↩.
+    .def  .
+    .     .
+  ]
+  screen-should-contain-in-color 245/grey [
+    .     .
+    .    ↩.
+    .     .
+    .     .
+  ]
+]
+
+scenario editor-wraps-barely-long-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  run [
+    render screen, e
+  ]
+  # still wrap, even though the line would fit. We need room to click on the
+  # end of the line
+  screen-should-contain [
+    .     .
+    .abcd↩.
+    .e    .
+    .     .
+  ]
+  screen-should-contain-in-color 245/grey [
+    .     .
+    .    ↩.
+    .     .
+    .     .
+  ]
+]
+
+scenario editor-with-empty-text [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 5/right
+  run [
+    render screen, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .     .
+    .     .
+    .     .
+  ]
+  memory-should-contain [
+    3 <- 1  # cursor row
+    4 <- 0  # cursor column
+  ]
+]
+
+# just a little color for Mu code
+
+scenario render-colors-comments [
+  local-scope
+  assume-screen 5/width, 5/height
+  s:text <- new [abc
+# de
+f]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    .abc  .
+    .# de .
+    .f    .
+    .     .
+  ]
+  screen-should-contain-in-color 12/lightblue, [
+    .     .
+    .     .
+    .# de .
+    .     .
+    .     .
+  ]
+  screen-should-contain-in-color 7/white, [
+    .     .
+    .abc  .
+    .     .
+    .f    .
+    .     .
+  ]
+]
+
+after <character-c-received> [
+  color <- get-color color, c
+]
+
+# so far the previous color is all the information we need; that may change
+def get-color color:num, c:char -> color:num [
+  local-scope
+  load-inputs
+  color-is-white?:bool <- equal color, 7/white
+  # if color is white and next character is '#', switch color to blue
+  {
+    break-unless color-is-white?
+    starting-comment?:bool <- equal c, 35/#
+    break-unless starting-comment?
+    trace 90, [app], [switch color back to blue]
+    return 12/lightblue
+  }
+  # if color is blue and next character is newline, switch color to white
+  {
+    color-is-blue?:bool <- equal color, 12/lightblue
+    break-unless color-is-blue?
+    ending-comment?:bool <- equal c, 10/newline
+    break-unless ending-comment?
+    trace 90, [app], [switch color back to white]
+    return 7/white
+  }
+  # if color is white (no comments) and next character is '<', switch color to red
+  {
+    break-unless color-is-white?
+    starting-assignment?:bool <- equal c, 60/<
+    break-unless starting-assignment?
+    return 1/red
+  }
+  # if color is red and next character is space, switch color to white
+  {
+    color-is-red?:bool <- equal color, 1/red
+    break-unless color-is-red?
+    ending-assignment?:bool <- equal c, 32/space
+    break-unless ending-assignment?
+    return 7/white
+  }
+  # otherwise no change
+  return color
+]
+
+scenario render-colors-assignment [
+  local-scope
+  assume-screen 8/width, 5/height
+  s:text <- new [abc
+d <- e
+f]
+  e:&:editor <- new-editor s, 0/left, 8/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .        .
+    .abc     .
+    .d <- e  .
+    .f       .
+    .        .
+  ]
+  screen-should-contain-in-color 1/red, [
+    .        .
+    .        .
+    .  <-    .
+    .        .
+    .        .
+  ]
+]
diff --git a/archive/2.vm/edit/002-typing.mu b/archive/2.vm/edit/002-typing.mu
new file mode 100644
index 00000000..ef3f25d2
--- /dev/null
+++ b/archive/2.vm/edit/002-typing.mu
@@ -0,0 +1,1144 @@
+## handling events from the keyboard, mouse, touch screen, ...
+
+# temporary main: interactive editor
+# hit ctrl-c to exit
+def! main text:text [
+  local-scope
+  load-inputs
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  editor:&:editor <- new-editor text, 5/left, 45/right
+  editor-render null/screen, editor
+  editor-event-loop null/screen, null/console, editor
+  close-console
+]
+
+def editor-event-loop screen:&:screen, console:&:console, editor:&:editor -> screen:&:screen, console:&:console, editor:&:editor [
+  local-scope
+  load-inputs
+  {
+    # looping over each (keyboard or touch) event as it occurs
+    +next-event
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-column:offset
+    screen <- move-cursor screen, cursor-row, cursor-column
+    e:event, found?:bool, quit?:bool, console <- read-event console
+    loop-unless found?
+    break-if quit?  # only in tests
+    trace 10, [app], [next-event]
+    # 'touch' event
+    t:touch-event, is-touch?:bool <- maybe-convert e, touch:variant
+    {
+      break-unless is-touch?
+      move-cursor editor, screen, t
+      loop +next-event
+    }
+    # keyboard events
+    {
+      break-if is-touch?
+      go-render?:bool <- handle-keyboard-event screen, editor, e
+      {
+        break-unless go-render?
+        screen <- editor-render screen, editor
+      }
+    }
+    loop
+  }
+]
+
+# process click, return if it was on current editor
+def move-cursor editor:&:editor, screen:&:screen, t:touch-event -> in-focus?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor, false
+  click-row:num <- get t, row:offset
+  return-unless click-row, false  # ignore clicks on 'menu'
+  click-column:num <- get t, column:offset
+  left:num <- get *editor, left:offset
+  too-far-left?:bool <- lesser-than click-column, left
+  return-if too-far-left?, false
+  right:num <- get *editor, right:offset
+  too-far-right?:bool <- greater-than click-column, right
+  return-if too-far-right?, false
+  # position cursor
+  <begin-move-cursor>
+  editor <- snap-cursor editor, screen, click-row, click-column
+  undo-coalesce-tag:num <- copy 0/never
+  <end-move-cursor>
+  # gain focus
+  return true
+]
+
+# Variant of 'render' that only moves the cursor (coordinates and
+# before-cursor). If it's past the end of a line, it 'slides' it left. If it's
+# past the last line it positions at end of last line.
+def snap-cursor editor:&:editor, screen:&:screen, target-row:num, target-column:num -> editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  screen-height:num <- screen-height screen
+  # count newlines until screen row
+  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
+  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
+  curr <- next curr
+  row:num <- copy 1/top
+  column:num <- copy left
+  *editor <- put *editor, cursor-row:offset, target-row
+  cursor-row:num <- copy target-row
+  *editor <- put *editor, cursor-column:offset, target-column
+  cursor-column:num <- copy target-column
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  {
+    +next-character
+    break-unless curr
+    off-screen?:bool <- greater-or-equal row, screen-height
+    break-if off-screen?
+    # update editor.before-cursor
+    # Doing so at the start of each iteration ensures it stays one step behind
+    # the current character.
+    {
+      at-cursor-row?:bool <- equal row, cursor-row
+      break-unless at-cursor-row?
+      at-cursor?:bool <- equal column, cursor-column
+      break-unless at-cursor?
+      before-cursor <- copy prev
+      *editor <- put *editor, before-cursor:offset, before-cursor
+    }
+    c:char <- get *curr, value:offset
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # adjust cursor if necessary
+      {
+        at-cursor-row?:bool <- equal row, cursor-row
+        break-unless at-cursor-row?
+        left-of-cursor?:bool <- lesser-than column, cursor-column
+        break-unless left-of-cursor?
+        cursor-column <- copy column
+        *editor <- put *editor, cursor-column:offset, cursor-column
+        before-cursor <- copy prev
+        *editor <- put *editor, before-cursor:offset, before-cursor
+      }
+      # skip to next line
+      row <- add row, 1
+      column <- copy left
+      curr <- next curr
+      prev <- next prev
+      loop +next-character
+    }
+    {
+      # at right? wrap. even if there's only one more letter left; we need
+      # room for clicking on the cursor after it.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      column <- copy left
+      row <- add row, 1
+      # don't increment curr/prev
+      loop +next-character
+    }
+    curr <- next curr
+    prev <- next prev
+    column <- add column, 1
+    loop
+  }
+  # is cursor to the right of the last line? move to end
+  {
+    at-cursor-row?:bool <- equal row, cursor-row
+    cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
+    before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
+    above-cursor-row?:bool <- lesser-than row, cursor-row
+    before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
+    break-unless before-cursor?
+    cursor-row <- copy row
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- copy column
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    before-cursor <- copy prev
+    *editor <- put *editor, before-cursor:offset, before-cursor
+  }
+]
+
+# Process an event 'e' and try to minimally update the screen.
+# Set 'go-render?' to true to indicate the caller must perform a non-minimal update.
+def handle-keyboard-event screen:&:screen, editor:&:editor, e:event -> go-render?:bool, screen:&:screen, editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor, false/don't-render
+  screen-width:num <- screen-width screen
+  screen-height:num <- screen-height screen
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  save-row:num <- copy cursor-row
+  save-column:num <- copy cursor-column
+  # character
+  {
+    c:char, is-unicode?:bool <- maybe-convert e, text:variant
+    break-unless is-unicode?
+    trace 10, [app], [handle-keyboard-event: special character]
+    # exceptions for special characters go here
+    <handle-special-character>
+    # ignore any other special characters
+    regular-character?:bool <- greater-or-equal c, 32/space
+    return-unless regular-character?, false/don't-render
+    # otherwise type it in
+    <begin-insert-character>
+    go-render? <- insert-at-cursor editor, c, screen
+    <end-insert-character>
+    return
+  }
+  # special key to modify the text or move the cursor
+  k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
+  assert is-keycode?, [event was of unknown type; neither keyboard nor mouse]
+  # handlers for each special key will go here
+  <handle-special-key>
+  return true/go-render
+]
+
+def insert-at-cursor editor:&:editor, c:char, screen:&:screen -> go-render?:bool, editor:&:editor, screen:&:screen [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  insert c, before-cursor
+  before-cursor <- next before-cursor
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  save-row:num <- copy cursor-row
+  save-column:num <- copy cursor-column
+  screen-width:num <- screen-width screen
+  screen-height:num <- screen-height screen
+  # occasionally we'll need to mess with the cursor
+  <insert-character-special-case>
+  # but mostly we'll just move the cursor right
+  cursor-column <- add cursor-column, 1
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  next:&:duplex-list:char <- next before-cursor
+  {
+    # at end of all text? no need to scroll? just print the character and leave
+    at-end?:bool <- equal next, null
+    break-unless at-end?
+    bottom:num <- subtract screen-height, 1
+    at-bottom?:bool <- equal save-row, bottom
+    at-right?:bool <- equal save-column, right
+    overflow?:bool <- and at-bottom?, at-right?
+    break-if overflow?
+    move-cursor screen, save-row, save-column
+    print screen, c
+    return false/don't-render
+  }
+  {
+    # not at right margin? print the character and rest of line
+    break-unless next
+    at-right?:bool <- greater-or-equal cursor-column, screen-width
+    break-if at-right?
+    curr:&:duplex-list:char <- copy before-cursor
+    move-cursor screen, save-row, save-column
+    curr-column:num <- copy save-column
+    {
+      # hit right margin? give up and let caller render
+      at-right?:bool <- greater-than curr-column, right
+      return-if at-right?, true/go-render
+      break-unless curr
+      # newline? done.
+      currc:char <- get *curr, value:offset
+      at-newline?:bool <- equal currc, 10/newline
+      break-if at-newline?
+      print screen, currc
+      curr-column <- add curr-column, 1
+      curr <- next curr
+      loop
+    }
+    return false/don't-render
+  }
+  return true/go-render
+]
+
+# helper for tests
+def editor-render screen:&:screen, editor:&:editor -> screen:&:screen, editor:&:editor [
+  local-scope
+  load-inputs
+  old-top-idx:num <- save-top-idx screen
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  row:num, column:num <- render screen, editor
+  draw-horizontal screen, row, left, right, 9480/horizontal-dotted
+  row <- add row, 1
+  clear-screen-from screen, row, left, left, right
+  assert-no-scroll screen, old-top-idx
+]
+
+scenario editor-handles-empty-event-queue [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  assume-console []
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-handles-mouse-clicks [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 1  # on the 'b'
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1  # cursor is at row 0..
+    4 <- 1  # ..and column 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-outside-text [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  $clear-trace
+  assume-console [
+    left-click 1, 7  # last line, to the right of text
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1  # cursor row
+    4 <- 3  # cursor column
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-outside-text-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  $clear-trace
+  assume-console [
+    left-click 1, 7  # interior line, to the right of text
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1  # cursor row
+    4 <- 3  # cursor column
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-outside-text-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  $clear-trace
+  assume-console [
+    left-click 3, 7  # below text
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2  # cursor row
+    4 <- 3  # cursor column
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-outside-column [
+  local-scope
+  assume-screen 10/width, 5/height
+  # editor occupies only left half of screen
+  e:&:editor <- new-editor [abc], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    # click on right half of screen
+    left-click 3, 8
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1  # no change to cursor row
+    4 <- 0  # ..or column
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-in-menu-area [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    # click on first, 'menu' row
+    left-click 0, 3
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # no change to cursor
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+]
+
+scenario editor-inserts-characters-into-empty-editor [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    type [abc]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  check-trace-count-for-label 3, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # type two letters at different places
+  assume-console [
+    type [0]
+    left-click 1, 2
+    type [d]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0adbc     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 7, [print-character]  # 4 for first letter, 3 for second
+]
+
+scenario editor-inserts-characters-at-cursor-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 5  # right of last line
+    type [d]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor-5 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 5  # right of non-last line
+    type [e]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abce      .
+    .d         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 3, 5  # below all text
+    type [d]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor-4 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 3, 5  # below all text
+    type [e]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .de        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor-6 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 3, 5  # below all text
+    type [ef]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 2, [print-character]
+]
+
+scenario editor-moves-cursor-after-inserting-characters [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [ab], 0/left, 5/right
+  editor-render screen, e
+  assume-console [
+    type [01]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .01ab      .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+]
+
+# if the cursor reaches the right margin, wrap the line
+
+scenario editor-wraps-line-on-insert [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 5/right
+  editor-render screen, e
+  # type a letter
+  assume-console [
+    type [e]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # no wrap yet
+  screen-should-contain [
+    .     .
+    .eabc .
+    .┈┈┈┈┈.
+    .     .
+    .     .
+  ]
+  # type a second letter
+  assume-console [
+    type [f]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # now wrap
+  screen-should-contain [
+    .     .
+    .efab↩.
+    .c    .
+    .┈┈┈┈┈.
+    .     .
+  ]
+]
+
+scenario editor-wraps-line-on-insert-2 [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  s:text <- new [abcdefg
+defg]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  # type more text at the start
+  assume-console [
+    left-click 3, 0
+    type [abc]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor is not wrapped
+  memory-should-contain [
+    3 <- 3
+    4 <- 3
+  ]
+  # but line is wrapped
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .efg       .
+    .abcd↩     .
+    .efg       .
+  ]
+]
+
+after <insert-character-special-case> [
+  # if the line wraps at the cursor, move cursor to start of next row
+  {
+    # if either:
+    # a) we're at the end of the line and at the column of the wrap indicator, or
+    # b) we're not at end of line and just before the column of the wrap indicator
+    wrap-column:num <- copy right
+    before-wrap-column:num <- subtract wrap-column, 1
+    at-wrap?:bool <- greater-or-equal cursor-column, wrap-column
+    just-before-wrap?:bool <- greater-or-equal cursor-column, before-wrap-column
+    next:&:duplex-list:char <- next before-cursor
+    # at end of line? next == 0 || next.value == 10/newline
+    at-end-of-line?:bool <- equal next, null
+    {
+      break-if at-end-of-line?
+      next-character:char <- get *next, value:offset
+      at-end-of-line? <- equal next-character, 10/newline
+    }
+    # break unless ((eol? and at-wrap?) or (~eol? and just-before-wrap?))
+    move-cursor-to-next-line?:bool <- copy false
+    {
+      break-if at-end-of-line?
+      move-cursor-to-next-line? <- copy just-before-wrap?
+      # if we're moving the cursor because it's in the middle of a wrapping
+      # line, adjust it to left-most column
+      potential-new-cursor-column:num <- copy left
+    }
+    {
+      break-unless at-end-of-line?
+      move-cursor-to-next-line? <- copy at-wrap?
+      # if we're moving the cursor because it's at the end of a wrapping line,
+      # adjust it to one past the left-most column to make room for the
+      # newly-inserted wrap-indicator
+      potential-new-cursor-column:num <- add left, 1/make-room-for-wrap-indicator
+    }
+    break-unless move-cursor-to-next-line?
+    cursor-column <- copy potential-new-cursor-column
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    cursor-row <- add cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    # if we're out of the screen, scroll down
+    {
+      below-screen?:bool <- greater-or-equal cursor-row, screen-height
+      break-unless below-screen?
+      <scroll-down>
+    }
+    return true/go-render
+  }
+]
+
+scenario editor-wraps-cursor-after-inserting-characters-in-middle-of-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  assume-console [
+    left-click 1, 3  # right before the wrap icon
+    type [f]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abcf↩     .
+    .de        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 2  # cursor row
+    4 <- 0  # cursor column
+  ]
+]
+
+scenario editor-wraps-cursor-after-inserting-characters-at-end-of-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # create an editor containing two lines
+  s:text <- new [abc
+xyz]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abc       .
+    .xyz       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    left-click 1, 4  # at end of first line
+    type [de]  # trigger wrap
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .e         .
+    .xyz       .
+    .┈┈┈┈┈     .
+  ]
+]
+
+scenario editor-wraps-cursor-to-left-margin [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcde], 2/left, 7/right
+  assume-console [
+    left-click 1, 5  # line is full; no wrap icon yet
+    type [01]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .  abc0↩   .
+    .  1de     .
+    .  ┈┈┈┈┈   .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 2  # cursor row
+    4 <- 3  # cursor column
+  ]
+]
+
+# if newline, move cursor to start of next line, and maybe align indent with previous line
+
+container editor [
+  indent?:bool
+]
+
+after <editor-initialization> [
+  *result <- put *result, indent?:offset, true
+]
+
+scenario editor-moves-cursor-down-after-inserting-newline [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  assume-console [
+    type [0
+1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0         .
+    .1abc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <handle-special-character> [
+  {
+    newline?:bool <- equal c, 10/newline
+    break-unless newline?
+    <begin-insert-enter>
+    insert-new-line-and-indent editor, screen
+    <end-insert-enter>
+    return true/go-render
+  }
+]
+
+def insert-new-line-and-indent editor:&:editor, screen:&:screen -> editor:&:editor, screen:&:screen [
+  local-scope
+  load-inputs
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  screen-height:num <- screen-height screen
+  # update cursor coordinates
+  at-start-of-wrapped-line?:bool <- at-start-of-wrapped-line? editor
+  {
+    break-if at-start-of-wrapped-line?
+    cursor-row <- add cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+  }
+  cursor-column <- copy left
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  # maybe scroll
+  {
+    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal, never greater
+    break-unless below-screen?
+    <scroll-down2>
+    cursor-row <- subtract cursor-row, 1  # bring back into screen range
+    *editor <- put *editor, cursor-row:offset, cursor-row
+  }
+  # insert newline
+  insert 10/newline, before-cursor
+  before-cursor <- next before-cursor
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  # indent if necessary
+  indent?:bool <- get *editor, indent?:offset
+  return-unless indent?
+  d:&:duplex-list:char <- get *editor, data:offset
+  end-of-previous-line:&:duplex-list:char <- prev before-cursor
+  indent:num <- line-indent end-of-previous-line, d
+  i:num <- copy 0
+  {
+    indent-done?:bool <- greater-or-equal i, indent
+    break-if indent-done?
+    insert-at-cursor editor, 32/space, screen
+    i <- add i, 1
+    loop
+  }
+]
+
+def at-start-of-wrapped-line? editor:&:editor -> result:bool [
+  local-scope
+  load-inputs
+  left:num <- get *editor, left:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  cursor-at-left?:bool <- equal cursor-column, left
+  return-unless cursor-at-left?, false
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  before-before-cursor:&:duplex-list:char <- prev before-cursor
+  return-unless before-before-cursor, false  # cursor is at start of editor
+  char-before-cursor:char <- get *before-cursor, value:offset
+  cursor-after-newline?:bool <- equal char-before-cursor, 10/newline
+  return-if cursor-after-newline?, false
+  # if cursor is at left margin and not at start, but previous character is not a newline,
+  # then we're at start of a wrapped line
+  return true
+]
+
+# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
+# the number of spaces at the start of the line containing 'curr'.
+def line-indent curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
+  local-scope
+  load-inputs
+  result:num <- copy 0
+  return-unless curr
+  at-start?:bool <- equal curr, start
+  return-if at-start?
+  {
+    curr <- prev curr
+    break-unless curr
+    at-start?:bool <- equal curr, start
+    break-if at-start?
+    c:char <- get *curr, value:offset
+    at-newline?:bool <- equal c, 10/newline
+    break-if at-newline?
+    # if c is a space, increment result
+    is-space?:bool <- equal c, 32/space
+    {
+      break-unless is-space?
+      result <- add result, 1
+    }
+    # if c is not a space, reset result
+    {
+      break-if is-space?
+      result <- copy 0
+    }
+    loop
+  }
+]
+
+scenario editor-moves-cursor-down-after-inserting-newline-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 1/left, 10/right
+  assume-console [
+    type [0
+1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    . 0        .
+    . 1abc     .
+    . ┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-clears-previous-line-completely-after-inserting-newline [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    press enter
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # line should be fully cleared
+  screen-should-contain [
+    .          .
+    .          .
+    .abcd↩     .
+    .e         .
+    .┈┈┈┈┈     .
+  ]
+]
+
+scenario editor-splits-wrapped-line-after-inserting-newline [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcdef], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    left-click 2, 0
+    press enter
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .ef        .
+    .┈┈┈┈┈     .
+  ]
+  memory-should-contain [
+    10 <- 2  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+]
+
+scenario editor-inserts-indent-after-newline [
+  local-scope
+  assume-screen 10/width, 10/height
+  s:text <- new [ab
+  cd
+ef]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  # position cursor after 'cd' and hit 'newline'
+  assume-console [
+    left-click 2, 8
+    type [
+]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor should be below start of previous line
+  memory-should-contain [
+    3 <- 3  # cursor row
+    4 <- 2  # cursor column (indented)
+  ]
+]
+
+scenario editor-skips-indent-around-paste [
+  local-scope
+  assume-screen 10/width, 10/height
+  s:text <- new [ab
+  cd
+ef]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  # position cursor after 'cd' and hit 'newline' surrounded by paste markers
+  assume-console [
+    left-click 2, 8
+    press 65507  # start paste
+    press enter
+    press 65506  # end paste
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor should be below start of previous line
+  memory-should-contain [
+    3 <- 3  # cursor row
+    4 <- 0  # cursor column (not indented)
+  ]
+]
+
+after <handle-special-key> [
+  {
+    paste-start?:bool <- equal k, 65507/paste-start
+    break-unless paste-start?
+    *editor <- put *editor, indent?:offset, false
+    return true/go-render
+  }
+]
+
+after <handle-special-key> [
+  {
+    paste-end?:bool <- equal k, 65506/paste-end
+    break-unless paste-end?
+    *editor <- put *editor, indent?:offset, true
+    return true/go-render
+  }
+]
+
+## helpers
+
+def draw-horizontal screen:&:screen, row:num, x:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  height:num <- screen-height screen
+  past-bottom?:bool <- greater-or-equal row, height
+  return-if past-bottom?
+  style:char, style-found?:bool <- next-input
+  {
+    break-if style-found?
+    style <- copy 9472/horizontal
+  }
+  color:num, color-found?:bool <- next-input
+  {
+    # default color to white
+    break-if color-found?
+    color <- copy 245/grey
+  }
+  bg-color:num, bg-color-found?:bool <- next-input
+  {
+    break-if bg-color-found?
+    bg-color <- copy 0/black
+  }
+  screen <- move-cursor screen, row, x
+  {
+    continue?:bool <- lesser-or-equal x, right  # right is inclusive, to match editor semantics
+    break-unless continue?
+    print screen, style, color, bg-color
+    x <- add x, 1
+    loop
+  }
+]
diff --git a/archive/2.vm/edit/003-shortcuts.mu b/archive/2.vm/edit/003-shortcuts.mu
new file mode 100644
index 00000000..872dfcea
--- /dev/null
+++ b/archive/2.vm/edit/003-shortcuts.mu
@@ -0,0 +1,4462 @@
+## special shortcuts for manipulating the editor
+# Some keys on the keyboard generate unicode characters, others generate
+# terminfo key codes. We need to modify different places in the two cases.
+
+# tab - insert two spaces
+
+scenario editor-inserts-two-spaces-on-tab [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [ab
+cd]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press tab
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .  ab      .
+    .cd        .
+  ]
+  # we render at most two editor rows worth (one row for each space)
+  check-trace-count-for-label-lesser-than 10, [print-character]
+]
+
+scenario editor-inserts-two-spaces-and-wraps-line-on-tab [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcd], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press tab
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .  ab↩     .
+    .cd        .
+  ]
+  # we re-render the whole editor
+  check-trace-count-for-label-greater-than 10, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    tab?:bool <- equal c, 9/tab
+    break-unless tab?
+    <begin-insert-character>
+    # todo: decompose insert-at-cursor into editor update and screen update,
+    # so that 'tab' doesn't render the current line multiple times
+    insert-at-cursor editor, 32/space, screen
+    go-render? <- insert-at-cursor editor, 32/space, screen
+    <end-insert-character>
+    return
+  }
+]
+
+# backspace - delete character before cursor
+
+scenario editor-handles-backspace-key [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 1
+    press backspace
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .bc        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 0
+  ]
+  check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
+]
+
+after <handle-special-character> [
+  {
+    delete-previous-character?:bool <- equal c, 8/backspace
+    break-unless delete-previous-character?
+    <begin-backspace-character>
+    go-render?:bool, backspaced-cell:&:duplex-list:char <- delete-before-cursor editor, screen
+    <end-backspace-character>
+    return
+  }
+]
+
+# return values:
+#   go-render? - whether caller needs to update the screen
+#   backspaced-cell - value deleted (or 0 if nothing was deleted) so we can save it for undo, etc.
+def delete-before-cursor editor:&:editor, screen:&:screen -> go-render?:bool, backspaced-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  data:&:duplex-list:char <- get *editor, data:offset
+  # if at start of text (before-cursor at § sentinel), return
+  prev:&:duplex-list:char <- prev before-cursor
+  return-unless prev, false/no-more-render, null/nothing-deleted
+  trace 10, [app], [delete-before-cursor]
+  original-row:num <- get *editor, cursor-row:offset
+  scroll?:bool <- move-cursor-coordinates-left editor
+  backspaced-cell:&:duplex-list:char <- copy before-cursor
+  data <- remove before-cursor, data  # will also neatly trim next/prev pointers in backspaced-cell/before-cursor
+  before-cursor <- copy prev
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  return-if scroll?, true/go-render
+  screen-width:num <- screen-width screen
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  # did we just backspace over a newline?
+  same-row?:bool <- equal cursor-row, original-row
+  return-unless same-row?, true/go-render
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  curr:&:duplex-list:char <- next before-cursor
+  screen <- move-cursor screen, cursor-row, cursor-column
+  curr-column:num <- copy cursor-column
+  {
+    # hit right margin? give up and let caller render
+    at-right?:bool <- greater-or-equal curr-column, right
+    return-if at-right?, true/go-render
+    break-unless curr
+    # newline? done.
+    currc:char <- get *curr, value:offset
+    at-newline?:bool <- equal currc, 10/newline
+    break-if at-newline?
+    screen <- print screen, currc
+    curr-column <- add curr-column, 1
+    curr <- next curr
+    loop
+  }
+  # we're guaranteed not to be at the right margin
+  space:char <- copy 32/space
+  screen <- print screen, space
+  go-render? <- copy false
+]
+
+def move-cursor-coordinates-left editor:&:editor -> go-render?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  go-render?:bool <- copy false
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  left:num <- get *editor, left:offset
+  # if not at left margin, move one character left
+  {
+    at-left-margin?:bool <- equal cursor-column, left
+    break-if at-left-margin?
+    trace 10, [app], [decrementing cursor column]
+    cursor-column <- subtract cursor-column, 1
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    return
+  }
+  # if at left margin, we must move to previous row:
+  top-of-screen?:bool <- equal cursor-row, 1  # exclude menu bar
+  {
+    break-if top-of-screen?
+    cursor-row <- subtract cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+  }
+  {
+    break-unless top-of-screen?
+    <scroll-up>
+    go-render? <- copy true
+  }
+  {
+    # case 1: if previous character was newline, figure out how long the previous line is
+    previous-character:char <- get *before-cursor, value:offset
+    previous-character-is-newline?:bool <- equal previous-character, 10/newline
+    break-unless previous-character-is-newline?
+    # compute length of previous line
+    trace 10, [app], [switching to previous line]
+    d:&:duplex-list:char <- get *editor, data:offset
+    end-of-line:num <- previous-line-length before-cursor, d
+    right:num <- get *editor, right:offset
+    width:num <- subtract right, left
+    wrap?:bool <- greater-than end-of-line, width
+    {
+      break-unless wrap?
+      _, column-offset:num <- divide-with-remainder end-of-line, width
+      cursor-column <- add left, column-offset
+      *editor <- put *editor, cursor-column:offset, cursor-column
+    }
+    {
+      break-if wrap?
+      cursor-column <- add left, end-of-line
+      *editor <- put *editor, cursor-column:offset, cursor-column
+    }
+    return
+  }
+  # case 2: if previous-character was not newline, we're just at a wrapped line
+  trace 10, [app], [wrapping to previous line]
+  right:num <- get *editor, right:offset
+  cursor-column <- subtract right, 1  # leave room for wrap icon
+  *editor <- put *editor, cursor-column:offset, cursor-column
+]
+
+# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
+# the length of the previous line before the 'curr' pointer.
+def previous-line-length curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
+  local-scope
+  load-inputs
+  result:num <- copy 0
+  return-unless curr
+  at-start?:bool <- equal curr, start
+  return-if at-start?
+  {
+    curr <- prev curr
+    break-unless curr
+    at-start?:bool <- equal curr, start
+    break-if at-start?
+    c:char <- get *curr, value:offset
+    at-newline?:bool <- equal c, 10/newline
+    break-if at-newline?
+    result <- add result, 1
+    loop
+  }
+]
+
+scenario editor-clears-last-line-on-backspace [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [ab
+cd]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  assume-console [
+    left-click 2, 0
+    press backspace
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 2
+  ]
+]
+
+scenario editor-joins-and-wraps-lines-on-backspace [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with two long-ish but non-wrapping lines
+  s:text <- new [abc def
+ghi jkl]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # position the cursor at the start of the second and hit backspace
+  assume-console [
+    left-click 2, 0
+    press backspace
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # resulting single line should wrap correctly
+  screen-should-contain [
+    .          .
+    .abc defgh↩.
+    .i jkl     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-wraps-long-lines-on-backspace [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor in part of the screen with a long line
+  e:&:editor <- new-editor [abc def ghij], 0/left, 8/right
+  editor-render screen, e
+  # confirm that it wraps
+  screen-should-contain [
+    .          .
+    .abc def↩  .
+    . ghij     .
+    .┈┈┈┈┈┈┈┈  .
+  ]
+  $clear-trace
+  # position the cursor somewhere in the middle of the top screen line and hit backspace
+  assume-console [
+    left-click 1, 4
+    press backspace
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # resulting single line should wrap correctly and not overflow its bounds
+  screen-should-contain [
+    .          .
+    .abcdef ↩  .
+    .ghij      .
+    .┈┈┈┈┈┈┈┈  .
+    .          .
+  ]
+]
+
+# delete - delete character at cursor
+
+scenario editor-handles-delete-key [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press delete
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .bc        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
+  $clear-trace
+  assume-console [
+    press delete
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .c         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 2, [print-character]  # new length to overwrite
+]
+
+after <handle-special-key> [
+  {
+    delete-next-character?:bool <- equal k, 65522/delete
+    break-unless delete-next-character?
+    <begin-delete-character>
+    go-render?:bool, deleted-cell:&:duplex-list:char <- delete-at-cursor editor, screen
+    <end-delete-character>
+    return
+  }
+]
+
+def delete-at-cursor editor:&:editor, screen:&:screen -> go-render?:bool, deleted-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  data:&:duplex-list:char <- get *editor, data:offset
+  deleted-cell:&:duplex-list:char <- next before-cursor
+  return-unless deleted-cell, false/don't-render
+  currc:char <- get *deleted-cell, value:offset
+  data <- remove deleted-cell, data
+  deleted-newline?:bool <- equal currc, 10/newline
+  return-if deleted-newline?, true/go-render
+  # wasn't a newline? render rest of line
+  curr:&:duplex-list:char <- next before-cursor  # refresh after remove above
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  screen <- move-cursor screen, cursor-row, cursor-column
+  curr-column:num <- copy cursor-column
+  screen-width:num <- screen-width screen
+  {
+    # hit right margin? give up and let caller render
+    at-right?:bool <- greater-or-equal curr-column, screen-width
+    return-if at-right?, true/go-render
+    break-unless curr
+    currc:char <- get *curr, value:offset
+    at-newline?:bool <- equal currc, 10/newline
+    break-if at-newline?
+    screen <- print screen, currc
+    curr-column <- add curr-column, 1
+    curr <- next curr
+    loop
+  }
+  # we're guaranteed not to be at the right margin
+  space:char <- copy 32/space
+  screen <- print screen, space
+  go-render? <- copy false
+]
+
+# right arrow
+
+scenario editor-moves-cursor-right-with-key [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press right-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .a0bc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 3, [print-character]  # 0 and following characters
+]
+
+after <handle-special-key> [
+  {
+    move-to-next-character?:bool <- equal k, 65514/right-arrow
+    break-unless move-to-next-character?
+    # if not at end of text
+    next-cursor:&:duplex-list:char <- next before-cursor
+    break-unless next-cursor
+    # scan to next character
+    <begin-move-cursor>
+    before-cursor <- copy next-cursor
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    go-render?:bool <- move-cursor-coordinates-right editor, screen-height
+    screen <- move-cursor screen, cursor-row, cursor-column
+    undo-coalesce-tag:num <- copy 2/right-arrow
+    <end-move-cursor>
+    return
+  }
+]
+
+def move-cursor-coordinates-right editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor before-cursor:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  # if crossed a newline, move cursor to start of next row
+  {
+    old-cursor-character:char <- get *before-cursor, value:offset
+    was-at-newline?:bool <- equal old-cursor-character, 10/newline
+    break-unless was-at-newline?
+    cursor-row <- add cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- copy left
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
+    return-unless below-screen?, false/don't-render
+    <scroll-down>
+    cursor-row <- subtract cursor-row, 1  # bring back into screen range
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    return true/go-render
+  }
+  # if the line wraps, move cursor to start of next row
+  {
+    # if we're at the column just before the wrap indicator
+    wrap-column:num <- subtract right, 1
+    at-wrap?:bool <- equal cursor-column, wrap-column
+    break-unless at-wrap?
+    # and if next character isn't newline
+    next:&:duplex-list:char <- next before-cursor
+    break-unless next
+    next-character:char <- get *next, value:offset
+    newline?:bool <- equal next-character, 10/newline
+    break-if newline?
+    cursor-row <- add cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- copy left
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
+    return-unless below-screen?, false/no-more-render
+    <scroll-down>
+    cursor-row <- subtract cursor-row, 1  # bring back into screen range
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    return true/go-render
+  }
+  # otherwise move cursor one character right
+  cursor-column <- add cursor-column, 1
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  go-render? <- copy false
+]
+
+scenario editor-moves-cursor-to-next-line-with-right-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # type right-arrow a few times to get to start of second line
+  assume-console [
+    press right-arrow
+    press right-arrow
+    press right-arrow
+    press right-arrow  # next line
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  check-trace-count-for-label 0, [print-character]
+  # type something and ensure it goes where it should
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0d        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 2, [print-character]  # new length of second line
+]
+
+scenario editor-moves-cursor-to-next-line-with-right-arrow-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 1/left, 10/right
+  editor-render screen, e
+  assume-console [
+    press right-arrow
+    press right-arrow
+    press right-arrow
+    press right-arrow  # next line
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    . abc      .
+    . 0d       .
+    . ┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcdef], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 3
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  # line just barely wrapping
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  # position cursor at last character before wrap and hit right-arrow
+  assume-console [
+    left-click 1, 3
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  # now hit right arrow again
+  assume-console [
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcdef], 1/left, 6/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 4
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    . abcd↩    .
+    . ef       .
+    . ┈┈┈┈┈    .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # move to end of line, press right-arrow, type a character
+  assume-console [
+    left-click 1, 3
+    press right-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # new character should be in next line
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0d        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 2, [print-character]
+]
+
+# todo: ctrl-right: next word-end
+
+# left arrow
+
+scenario editor-moves-cursor-left-with-key [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 2
+    press left-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .a0bc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 3, [print-character]
+]
+
+after <handle-special-key> [
+  {
+    move-to-previous-character?:bool <- equal k, 65515/left-arrow
+    break-unless move-to-previous-character?
+    trace 10, [app], [left arrow]
+    # if not at start of text (before-cursor at § sentinel)
+    prev:&:duplex-list:char <- prev before-cursor
+    return-unless prev, false/don't-render
+    <begin-move-cursor>
+    go-render? <- move-cursor-coordinates-left editor
+    before-cursor <- copy prev
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    undo-coalesce-tag:num <- copy 1/left-arrow
+    <end-move-cursor>
+    return
+  }
+]
+
+scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with two lines
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # position cursor at start of second line (so there's no previous newline)
+  assume-console [
+    left-click 2, 0
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with three lines
+  s:text <- new [abc
+def
+g]
+  e:&:editor <- new-editor s:text, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # position cursor further down (so there's a newline before the character at
+  # the cursor)
+  assume-console [
+    left-click 3, 0
+    press left-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def0      .
+    .g         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  check-trace-count-for-label 1, [print-character]  # just the '0'
+]
+
+scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def
+g]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # position cursor at start of text, press left-arrow, then type a character
+  assume-console [
+    left-click 1, 0
+    press left-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # left-arrow should have had no effect
+  screen-should-contain [
+    .          .
+    .0abc      .
+    .def       .
+    .g         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  check-trace-count-for-label 4, [print-character]  # length of first line
+]
+
+scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-4 [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with text containing an empty line
+  s:text <- new [abc
+
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e:&:editor
+  $clear-trace
+  # position cursor right after empty line
+  assume-console [
+    left-click 3, 0
+    press left-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0         .
+    .d         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  check-trace-count-for-label 1, [print-character]  # just the '0'
+]
+
+scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with a wrapping line
+  e:&:editor <- new-editor [abcdef], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # position cursor right after empty line
+  assume-console [
+    left-click 2, 0
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1  # previous row
+    4 <- 3  # right margin except wrap icon
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-across-screen-lines-to-wrapping-line-with-left-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with a wrapping line followed by a second line
+  s:text <- new [abcdef
+g]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .g         .
+    .┈┈┈┈┈     .
+  ]
+  # position cursor right after empty line
+  assume-console [
+    left-click 3, 0
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2  # previous row
+    4 <- 2  # end of wrapped line
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-across-screen-lines-to-non-wrapping-line-with-left-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with a line on the verge of wrapping, followed by a second line
+  s:text <- new [abcd
+e]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # position cursor right after empty line
+  assume-console [
+    left-click 2, 0
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1  # previous row
+    4 <- 4  # end of wrapped line
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+# todo: ctrl-left: previous word-start
+
+# up arrow
+
+scenario editor-moves-to-previous-line-with-up-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 2, 1
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .a0bc      .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <handle-special-key> [
+  {
+    move-to-previous-line?:bool <- equal k, 65517/up-arrow
+    break-unless move-to-previous-line?
+    <begin-move-cursor>
+    go-render? <- move-to-previous-line editor
+    undo-coalesce-tag:num <- copy 3/up-arrow
+    <end-move-cursor>
+    return
+  }
+]
+
+def move-to-previous-line editor:&:editor -> go-render?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  go-render?:bool <- copy false
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  already-at-top?:bool <- lesser-or-equal cursor-row, 1/top
+  {
+    # if cursor not at top, move it
+    break-if already-at-top?
+    # if not at start of screen line, move to start of screen line (previous newline)
+    # then scan back another line
+    # if either step fails, give up without modifying cursor or coordinates
+    curr:&:duplex-list:char <- copy before-cursor
+    old:&:duplex-list:char <- copy curr
+    {
+      at-left?:bool <- equal cursor-column, left
+      break-if at-left?
+      curr <- before-previous-screen-line curr, editor
+      no-motion?:bool <- equal curr, old
+      return-if no-motion?
+    }
+    {
+      curr <- before-previous-screen-line curr, editor
+      no-motion?:bool <- equal curr, old
+      return-if no-motion?
+    }
+    before-cursor <- copy curr
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    cursor-row <- subtract cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    # scan ahead to right column or until end of line
+    target-column:num <- copy cursor-column
+    cursor-column <- copy left
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    {
+      done?:bool <- greater-or-equal cursor-column, target-column
+      break-if done?
+      curr:&:duplex-list:char <- next before-cursor
+      break-unless curr
+      currc:char <- get *curr, value:offset
+      at-newline?:bool <- equal currc, 10/newline
+      break-if at-newline?
+      #
+      before-cursor <- copy curr
+      *editor <- put *editor, before-cursor:offset, before-cursor
+      cursor-column <- add cursor-column, 1
+      *editor <- put *editor, cursor-column:offset, cursor-column
+      loop
+    }
+    return
+  }
+  {
+    # if cursor already at top, scroll up
+    break-unless already-at-top?
+    <scroll-up>
+    return true/go-render
+  }
+]
+
+# Takes a pointer into the doubly-linked list, scans back to before start of
+# previous *wrapped* line.
+# Returns original if no next newline.
+# Beware: never return null pointer.
+def before-previous-screen-line in:&:duplex-list:char, editor:&:editor -> out:&:duplex-list:char [
+  local-scope
+  load-inputs
+  curr:&:duplex-list:char <- copy in
+  c:char <- get *curr, value:offset
+  # compute max, number of characters to skip
+  #   1 + len%(width-1)
+  #   except rotate second term to vary from 1 to width-1 rather than 0 to width-2
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  max-line-length:num <- subtract right, left, -1/exclusive-right, 1/wrap-icon
+  sentinel:&:duplex-list:char <- get *editor, data:offset
+  len:num <- previous-line-length curr, sentinel
+  {
+    break-if len
+    # empty line; just skip this newline
+    prev:&:duplex-list:char <- prev curr
+    return-unless prev, curr
+    return prev
+  }
+  _, max:num <- divide-with-remainder len, max-line-length
+  # remainder 0 => scan one width-worth
+  {
+    break-if max
+    max <- copy max-line-length
+  }
+  max <- add max, 1
+  count:num <- copy 0
+  # skip 'max' characters
+  {
+    done?:bool <- greater-or-equal count, max
+    break-if done?
+    prev:&:duplex-list:char <- prev curr
+    break-unless prev
+    curr <- copy prev
+    count <- add count, 1
+    loop
+  }
+  return curr
+]
+
+scenario editor-adjusts-column-at-previous-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [ab
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 2, 3
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .ab0       .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-adjusts-column-at-empty-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 2, 3
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-moves-to-previous-line-from-zero-margin [
+  local-scope
+  assume-screen 10/width, 5/height
+  # start out with three lines
+  s:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # click on the third line and hit up-arrow, so you end up just after a newline
+  assume-console [
+    left-click 3, 0
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0def      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-moves-to-previous-line-from-left-margin [
+  local-scope
+  assume-screen 10/width, 5/height
+  # start out with three lines
+  s:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor s, 1/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # click on the third line and hit up-arrow, so you end up just after a newline
+  assume-console [
+    left-click 3, 1
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    . abc      .
+    . 0def     .
+    . ghi      .
+    . ┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-moves-to-top-line-in-presence-of-wrapped-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .e         .
+    .┈┈┈┈┈     .
+  ]
+  $clear-trace
+  assume-console [
+    left-click 2, 0
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0abc↩     .
+    .de        .
+    .┈┈┈┈┈     .
+  ]
+]
+
+scenario editor-moves-to-top-line-in-presence-of-wrapped-line-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+defgh]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abc       .
+    .defg↩     .
+    .h         .
+    .┈┈┈┈┈     .
+  ]
+  $clear-trace
+  assume-console [
+    left-click 3, 0
+    press up-arrow
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0abc      .
+    .defg↩     .
+    .h         .
+    .┈┈┈┈┈     .
+  ]
+]
+
+# down arrow
+
+scenario editor-moves-to-next-line-with-down-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # cursor starts out at (1, 0)
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # ..and ends at (2, 0)
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0def      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <handle-special-key> [
+  {
+    move-to-next-line?:bool <- equal k, 65516/down-arrow
+    break-unless move-to-next-line?
+    <begin-move-cursor>
+    go-render? <- move-to-next-line editor, screen-height
+    undo-coalesce-tag:num <- copy 4/down-arrow
+    <end-move-cursor>
+    return
+  }
+]
+
+def move-to-next-line editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  last-line:num <- subtract screen-height, 1
+  bottom:num <- get *editor, bottom:offset
+  at-bottom-of-screen?:bool <- greater-or-equal bottom, last-line
+  {
+    break-if before-cursor
+    {
+      break-if at-bottom-of-screen?
+      return false/don't-render
+    }
+    {
+      break-unless at-bottom-of-screen?
+      jump +try-to-scroll
+    }
+  }
+  next:&:duplex-list:char <- next before-cursor
+  {
+    break-if next
+    {
+      break-if at-bottom-of-screen?
+      return false/don't-render
+    }
+    {
+      break-unless at-bottom-of-screen?
+      jump +try-to-scroll
+    }
+  }
+  already-at-bottom?:bool <- greater-or-equal cursor-row, last-line
+  {
+    # if cursor not at bottom, move it
+    break-if already-at-bottom?
+    target-column:num <- copy cursor-column
+    # scan to start of next line
+    {
+      next:&:duplex-list:char <- next before-cursor
+      break-unless next
+      done?:bool <- greater-or-equal cursor-column, right
+      break-if done?
+      cursor-column <- add cursor-column, 1
+      before-cursor <- copy next
+      c:char <- get *next, value:offset
+      at-newline?:bool <- equal c, 10/newline
+      break-if at-newline?
+      loop
+    }
+    {
+      break-if next
+      {
+        break-if at-bottom-of-screen?
+        return false/don't-render
+      }
+      {
+        break-unless at-bottom-of-screen?
+        jump +try-to-scroll
+      }
+    }
+    cursor-row <- add cursor-row, 1
+    cursor-column <- copy left
+    {
+      next:&:duplex-list:char <- next before-cursor
+      break-unless next
+      c:char <- get *next, value:offset
+      at-newline?:bool <- equal c, 10/newline
+      break-if at-newline?
+      done?:bool <- greater-or-equal cursor-column, target-column
+      break-if done?
+      cursor-column <- add cursor-column, 1
+      before-cursor <- copy next
+      loop
+    }
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    return false/don't-render
+  }
+  +try-to-scroll
+  <scroll-down>
+  go-render? <- copy true
+]
+
+scenario editor-adjusts-column-at-next-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # second line is shorter than first
+  s:text <- new [abcde
+fg
+hi]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # move to end of first line, then press down
+  assume-console [
+    left-click 1, 8
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor doesn't go vertically down, it goes to end of shorter line
+  memory-should-contain [
+    3 <- 2
+    4 <- 2
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abcde     .
+    .fg0       .
+    .hi        .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-moves-down-within-wrapped-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcdefghijklmno], 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcdefghi↩.
+    .jklmno    .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # position cursor on first screen line, but past end of second screen line
+  assume-console [
+    left-click 1, 8
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor should be at end of second screen line
+  memory-should-contain [
+    3 <- 2
+    4 <- 6
+  ]
+]
+
+# ctrl-a/home - move cursor to start of line
+
+scenario editor-moves-to-start-of-line-with-ctrl-a [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line, press ctrl-a
+  assume-console [
+    left-click 2, 3
+    press ctrl-a
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of line
+  memory-should-contain [
+    4 <- 2
+    5 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    move-to-start-of-line?:bool <- equal c, 1/ctrl-a
+    break-unless move-to-start-of-line?
+    <begin-move-cursor>
+    move-to-start-of-screen-line editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return false/don't-render
+  }
+]
+
+after <handle-special-key> [
+  {
+    move-to-start-of-line?:bool <- equal k, 65521/home
+    break-unless move-to-start-of-line?
+    <begin-move-cursor>
+    move-to-start-of-screen-line editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return false/don't-render
+  }
+]
+
+# handles wrapped lines
+# precondition: cursor-column should be in a consistent state
+def move-to-start-of-screen-line editor:&:editor -> editor:&:editor [
+  local-scope
+  load-inputs
+  # update cursor column
+  left:num <- get *editor, left:offset
+  col:num <- get *editor, cursor-column:offset
+  # update before-cursor
+  curr:&:duplex-list:char <- get *editor, before-cursor:offset
+  # while not at start of line, move
+  {
+    done?:bool <- equal col, left
+    break-if done?
+    assert curr, [move-to-start-of-line tried to move before start of text]
+    curr <- prev curr
+    col <- subtract col, 1
+    loop
+  }
+  *editor <- put *editor, cursor-column:offset, col
+  *editor <- put *editor, before-cursor:offset, curr
+]
+
+scenario editor-moves-to-start-of-line-with-ctrl-a-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line (no newline before), press ctrl-a
+  assume-console [
+    left-click 1, 3
+    press ctrl-a
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of line
+  memory-should-contain [
+    4 <- 1
+    5 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-start-of-line-with-home [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  $clear-trace
+  # start on second line, press 'home'
+  assume-console [
+    left-click 2, 3
+    press home
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of line
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-start-of-line-with-home-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line (no newline before), press 'home'
+  assume-console [
+    left-click 1, 3
+    press home
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of line
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-start-of-screen-line-with-ctrl-a [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [123456], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .1234↩     .
+    .56        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  $clear-trace
+  # start on second line, press ctrl-a then up
+  assume-console [
+    left-click 2, 1
+    press ctrl-a
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of first line
+  memory-should-contain [
+    4 <- 1  # cursor-row
+    5 <- 0  # cursor-column
+  ]
+  check-trace-count-for-label 0, [print-character]
+  # make sure before-cursor is in sync
+  assume-console [
+    type [a]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .a123↩     .
+    .456       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1  # cursor-row
+    5 <- 1  # cursor-column
+  ]
+]
+
+# ctrl-e/end - move cursor to end of line
+
+scenario editor-moves-to-end-of-line-with-ctrl-e [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line, press ctrl-e
+  assume-console [
+    left-click 1, 1
+    press ctrl-e
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    4 <- 1
+    5 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+  # editor inserts future characters at cursor
+  assume-console [
+    type [z]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 4
+  ]
+  screen-should-contain [
+    .          .
+    .123z      .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    move-to-end-of-line?:bool <- equal c, 5/ctrl-e
+    break-unless move-to-end-of-line?
+    <begin-move-cursor>
+    move-to-end-of-line editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return false/don't-render
+  }
+]
+
+after <handle-special-key> [
+  {
+    move-to-end-of-line?:bool <- equal k, 65520/end
+    break-unless move-to-end-of-line?
+    <begin-move-cursor>
+    move-to-end-of-line editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return false/don't-render
+  }
+]
+
+def move-to-end-of-line editor:&:editor -> editor:&:editor [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  right:num <- get *editor, right:offset
+  # while not at end of line, move
+  {
+    next:&:duplex-list:char <- next before-cursor
+    break-unless next  # end of text
+    nextc:char <- get *next, value:offset
+    at-end-of-line?:bool <- equal nextc, 10/newline
+    break-if at-end-of-line?
+    cursor-column <- add cursor-column, 1
+    at-right?:bool <- equal cursor-column, right
+    break-if at-right?
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    before-cursor <- copy next
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    loop
+  }
+]
+
+scenario editor-moves-to-end-of-line-with-ctrl-e-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line (no newline after), press ctrl-e
+  assume-console [
+    left-click 2, 1
+    press ctrl-e
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    4 <- 2
+    5 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-end-of-line-with-end [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line, press 'end'
+  assume-console [
+    left-click 1, 1
+    press end
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-end-of-line-with-end-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line (no newline after), press 'end'
+  assume-console [
+    left-click 2, 1
+    press end
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    3 <- 2
+    4 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-end-of-wrapped-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123456
+789]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line, press 'end'
+  assume-console [
+    left-click 1, 1
+    press end
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    10 <- 1
+    11 <- 3
+  ]
+  # no prints
+  check-trace-count-for-label 0, [print-character]
+  # before-cursor is also consistent
+  assume-console [
+    type [a]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .123a↩     .
+    .456       .
+    .789       .
+    .┈┈┈┈┈     .
+  ]
+]
+
+# ctrl-u - delete text from start of line until (but not at) cursor
+
+scenario editor-deletes-to-start-of-line-with-ctrl-u [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line, press ctrl-u
+  assume-console [
+    left-click 2, 2
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to start of line
+  screen-should-contain [
+    .          .
+    .123       .
+    .6         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    delete-to-start-of-line?:bool <- equal c, 21/ctrl-u
+    break-unless delete-to-start-of-line?
+    <begin-delete-to-start-of-line>
+    deleted-cells:&:duplex-list:char <- delete-to-start-of-line editor
+    <end-delete-to-start-of-line>
+    go-render?:bool <- minimal-render-for-ctrl-u screen, editor, deleted-cells
+    return
+  }
+]
+
+def minimal-render-for-ctrl-u screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
+  local-scope
+  load-inputs
+  curr-column:num <- get *editor, cursor-column:offset
+  # accumulate the current line as text and render it
+  buf:&:buffer:char <- new-buffer 30  # accumulator for the text we need to render
+  curr:&:duplex-list:char <- get *editor, before-cursor:offset
+  i:num <- copy curr-column
+  right:num <- get *editor, right:offset
+  {
+    # if we have a wrapped line, give up and render the whole screen
+    wrap?:bool <- greater-or-equal i, right
+    return-if wrap?, true/go-render
+    curr <- next curr
+    break-unless curr
+    c:char <- get *curr, value:offset
+    b:bool <- equal c, 10
+    break-if b
+    buf <- append buf, c
+    i <- add i, 1
+    loop
+  }
+  # if the line used to be wrapped, give up and render the whole screen
+  num-deleted-cells:num <- length deleted-cells
+  old-row-len:num <- add i, num-deleted-cells
+  left:num <- get *editor, left:offset
+  end:num <- subtract right, left
+  wrap?:bool <- greater-or-equal old-row-len, end
+  return-if wrap?, true/go-render
+  curr-line:text <- buffer-to-array buf
+  curr-row:num <- get *editor, cursor-row:offset
+  render-code screen, curr-line, curr-column, right, curr-row
+  return false/dont-render
+]
+
+def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
+  local-scope
+  load-inputs
+  # compute range to delete
+  init:&:duplex-list:char <- get *editor, data:offset
+  top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+  update-top-of-screen?:bool <- copy false
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  start:&:duplex-list:char <- copy before-cursor
+  end:&:duplex-list:char <- next before-cursor
+  {
+    at-start-of-text?:bool <- equal start, init
+    break-if at-start-of-text?
+    curr:char <- get *start, value:offset
+    at-start-of-line?:bool <- equal curr, 10/newline
+    break-if at-start-of-line?
+    # if we went past top-of-screen, make a note to update it as well
+    at-top-of-screen?:bool <- equal start, top-of-screen
+    update-top-of-screen?:bool <- or update-top-of-screen?, at-top-of-screen?
+    start <- prev start
+    assert start, [delete-to-start-of-line tried to move before start of text]
+    loop
+  }
+  # snip it out
+  result:&:duplex-list:char <- next start
+  remove-between start, end
+  # update top-of-screen if it's just been invalidated
+  {
+    break-unless update-top-of-screen?
+    put *editor, top-of-screen:offset, start
+  }
+  # adjust cursor
+  before-cursor <- copy start
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  left:num <- get *editor, left:offset
+  *editor <- put *editor, cursor-column:offset, left
+  # if the line wrapped before, we may need to adjust cursor-row as well
+  right:num <- get *editor, right:offset
+  width:num <- subtract right, left
+  num-deleted:num <- length result
+  cursor-row-adjustment:num <- divide-with-remainder num-deleted, width
+  return-unless cursor-row-adjustment
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-row-in-editor:num <- subtract cursor-row, 1  # ignore menubar
+  at-top?:bool <- lesser-or-equal cursor-row-in-editor, cursor-row-adjustment
+  {
+    break-unless at-top?
+    cursor-row <- copy 1  # top of editor, below menubar
+  }
+  {
+    break-if at-top?
+    cursor-row <- subtract cursor-row, cursor-row-adjustment
+  }
+  put *editor, cursor-row:offset, cursor-row
+]
+
+def render-code screen:&:screen, s:text, left:num, right:num, row:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  return-unless s
+  color:num <- copy 7/white
+  column:num <- copy left
+  screen <- move-cursor screen, row, column
+  screen-height:num <- screen-height screen
+  i:num <- copy 0
+  len:num <- length *s
+  {
+    +next-character
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    done? <- greater-or-equal row, screen-height
+    break-if done?
+    c:char <- index *s, i
+    <character-c-received>
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # clear rest of line in this window
+      {
+        done?:bool <- greater-than column, right
+        break-if done?
+        space:char <- copy 32/space
+        print screen, space
+        column <- add column, 1
+        loop
+      }
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      i <- add i, 1
+      loop +next-character
+    }
+    {
+      # at right? wrap.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment i
+      loop +next-character
+    }
+    i <- add i, 1
+    print screen, c, color
+    column <- add column, 1
+    loop
+  }
+  was-at-left?:bool <- equal column, left
+  clear-line-until screen, right
+  {
+    break-if was-at-left?
+    row <- add row, 1
+  }
+  move-cursor screen, row, left
+]
+
+scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line (no newline before), press ctrl-u
+  assume-console [
+    left-click 1, 2
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to start of line
+  screen-should-contain [
+    .          .
+    .3         .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start past end of line, press ctrl-u
+  assume-console [
+    left-click 1, 3
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to start of line
+  screen-should-contain [
+    .          .
+    .          .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-deletes-to-start-of-final-line-with-ctrl-u [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start past end of final line, press ctrl-u
+  assume-console [
+    left-click 2, 3
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to start of line
+  screen-should-contain [
+    .          .
+    .123       .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u [
+  local-scope
+  assume-screen 10/width, 10/height
+  # first line starts out wrapping
+  s:text <- new [123456
+789]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .1234↩     .
+    .56        .
+    .789       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  $clear-trace
+  # ctrl-u enough of the first line that it's no longer wrapping
+  assume-console [
+    left-click 1, 3
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # entire screen needs to be refreshed
+  screen-should-contain [
+    .          .
+    .456       .
+    .789       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  check-trace-count-for-label 45, [print-character]
+]
+
+# sometimes hitting ctrl-u needs to adjust the cursor row
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-2 [
+  local-scope
+  assume-screen 10/width, 10/height
+  # third line starts out wrapping
+  s:text <- new [1
+2
+345678
+9]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .1         .
+    .2         .
+    .3456↩     .
+    .78        .
+    .9         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # position cursor on screen line after the wrap and hit ctrl-u
+  assume-console [
+    left-click 4, 1  # on '8'
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .1         .
+    .2         .
+    .8         .
+    .9         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # cursor moves up one screen line
+  memory-should-contain [
+    10 <- 3  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+]
+
+# line wrapping twice (taking up 3 screen lines)
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-3 [
+  local-scope
+  assume-screen 10/width, 10/height
+  # third line starts out wrapping
+  s:text <- new [1
+2
+3456789abcd
+e]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  assume-console [
+    left-click 4, 1  # on '8'
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .1         .
+    .2         .
+    .3456↩     .
+    .789a↩     .
+    .bcd       .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    left-click 5, 1
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .1         .
+    .2         .
+    .cd        .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # make sure we adjusted cursor-row
+  memory-should-contain [
+    10 <- 3  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+]
+
+# adjusting cursor row at the top of the screen
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-4 [
+  local-scope
+  assume-screen 10/width, 10/height
+  # first line starts out wrapping
+  s:text <- new [1234567
+89]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .1234↩     .
+    .567       .
+    .89        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # position cursor on second screen line (after the wrap) and hit ctrl-u
+  assume-console [
+    left-click 2, 1
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .67        .
+    .89        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # cursor moves up to screen line 1
+  memory-should-contain [
+    10 <- 1  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+]
+
+# screen begins part-way through a wrapping line
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-5 [
+  local-scope
+  assume-screen 10/width, 10/height
+  # third line starts out wrapping
+  s:text <- new [1
+2
+345678
+9]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  # position the '78' line at the top of the screen
+  assume-console [
+    left-click 4, 1  # on '8'
+    press ctrl-t
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .78        .
+    .9         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    left-click 1, 1
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  # make sure we updated top-of-screen correctly
+  screen-should-contain [
+    .          .
+    .8         .
+    .9         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    10 <- 1  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+  # the entire line is deleted, even the part not shown on screen
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .2         .
+    .8         .
+    .9         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+]
+
+# screen begins part-way through a line wrapping twice (taking up 3 screen lines)
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-6 [
+  local-scope
+  assume-screen 10/width, 10/height
+  # third line starts out wrapping
+  s:text <- new [1
+2
+3456789abcd
+e]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  # position the 'bcd' line at the top of the screen
+  assume-console [
+    left-click 4, 1  # on '8'
+    press ctrl-t
+    press ctrl-s  # now on 'c'
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .bcd       .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    left-click 1, 1
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  # make sure we updated top-of-screen correctly
+  screen-should-contain [
+    .          .
+    .cd        .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    10 <- 1  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+  # the entire line is deleted, even the part not shown on screen
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .2         .
+    .cd        .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+]
+
+# ctrl-k - delete text from cursor to end of line (but not the newline)
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line, press ctrl-k
+  assume-console [
+    left-click 1, 1
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to end of line
+  screen-should-contain [
+    .          .
+    .1         .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 9, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    delete-to-end-of-line?:bool <- equal c, 11/ctrl-k
+    break-unless delete-to-end-of-line?
+    <begin-delete-to-end-of-line>
+    deleted-cells:&:duplex-list:char <- delete-to-end-of-line editor
+    <end-delete-to-end-of-line>
+    # checks if we can do a minimal render and if we can it will do a minimal render
+    go-render?:bool <- minimal-render-for-ctrl-k screen, editor, deleted-cells
+    return
+  }
+]
+
+def minimal-render-for-ctrl-k screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
+  local-scope
+  load-inputs
+  # if we deleted nothing, there's nothing to render
+  return-unless deleted-cells, false/dont-render
+  # if the line used to wrap before, give up and render the whole screen
+  curr-column:num <- get *editor, cursor-column:offset
+  num-deleted-cells:num <- length deleted-cells
+  old-row-len:num <- add curr-column, num-deleted-cells
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  end:num <- subtract right, left
+  wrap?:bool <- greater-or-equal old-row-len, end
+  return-if wrap?, true/go-render
+  clear-line-until screen, right
+  return false/dont-render
+]
+
+def delete-to-end-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
+  local-scope
+  load-inputs
+  # compute range to delete
+  start:&:duplex-list:char <- get *editor, before-cursor:offset
+  end:&:duplex-list:char <- next start
+  {
+    at-end-of-text?:bool <- equal end, null
+    break-if at-end-of-text?
+    curr:char <- get *end, value:offset
+    at-end-of-line?:bool <- equal curr, 10/newline
+    break-if at-end-of-line?
+    end <- next end
+    loop
+  }
+  # snip it out
+  result <- next start
+  remove-between start, end
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line (no newline after), press ctrl-k
+  assume-console [
+    left-click 2, 1
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to end of line
+  screen-should-contain [
+    .          .
+    .123       .
+    .4         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 9, [print-character]
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start at end of line
+  assume-console [
+    left-click 1, 2
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes just last character
+  screen-should-contain [
+    .          .
+    .12        .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 8, [print-character]
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start past end of line
+  assume-console [
+    left-click 1, 3
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes nothing
+  screen-should-contain [
+    .          .
+    .123       .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 7, [print-character]
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start at end of text
+  assume-console [
+    left-click 2, 2
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes just the final character
+  screen-should-contain [
+    .          .
+    .123       .
+    .45        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 8, [print-character]
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start past end of text
+  assume-console [
+    left-click 2, 3
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes nothing
+  screen-should-contain [
+    .          .
+    .123       .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # no prints necessary
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-deletes-to-end-of-wrapped-line-with-ctrl-k [
+  local-scope
+  assume-screen 10/width, 5/height
+  # create an editor with the first line wrapping to a second screen row
+  s:text <- new [1234
+567]
+  e:&:editor <- new-editor s, 0/left, 4/right
+  editor-render screen, e
+  $clear-trace
+  # delete all of the first wrapped line
+  assume-console [
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows an empty unwrapped first line
+  screen-should-contain [
+    .          .
+    .          .
+    .567       .
+    .┈┈┈┈      .
+    .          .
+  ]
+  # entire screen is refreshed
+  check-trace-count-for-label 16, [print-character]
+]
+
+# scroll down if necessary
+
+scenario editor-can-scroll-down-using-arrow-keys [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with >3 lines
+  s:text <- new [a
+b
+c
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+  # position cursor at last line, then try to move further down
+  assume-console [
+    left-click 3, 0
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen slides by one line
+  screen-should-contain [
+    .          .
+    .b         .
+    .c         .
+    .d         .
+  ]
+]
+
+after <scroll-down> [
+  trace 10, [app], [scroll down]
+  top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  max:num <- subtract right, left
+  old-top:&:duplex-list:char <- copy top-of-screen
+  top-of-screen <- before-start-of-next-line top-of-screen, max
+  *editor <- put *editor, top-of-screen:offset, top-of-screen
+  no-movement?:bool <- equal old-top, top-of-screen
+  return-if no-movement?, false/don't-render
+]
+
+after <scroll-down2> [
+  trace 10, [app], [scroll down]
+  top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  max:num <- subtract right, left
+  old-top:&:duplex-list:char <- copy top-of-screen
+  top-of-screen <- before-start-of-next-line top-of-screen, max
+  *editor <- put *editor, top-of-screen:offset, top-of-screen
+  no-movement?:bool <- equal old-top, top-of-screen
+  return-if no-movement?
+]
+
+# Takes a pointer into the doubly-linked list, scans ahead at most 'max'
+# positions until the next newline.
+# Returns original if no next newline.
+# Beware: never return null pointer.
+def before-start-of-next-line original:&:duplex-list:char, max:num -> curr:&:duplex-list:char [
+  local-scope
+  load-inputs
+  count:num <- copy 0
+  curr:&:duplex-list:char <- copy original
+  # skip the initial newline if it exists
+  {
+    c:char <- get *curr, value:offset
+    at-newline?:bool <- equal c, 10/newline
+    break-unless at-newline?
+    curr <- next curr
+    count <- add count, 1
+  }
+  {
+    return-unless curr, original
+    done?:bool <- greater-or-equal count, max
+    break-if done?
+    c:char <- get *curr, value:offset
+    at-newline?:bool <- equal c, 10/newline
+    break-if at-newline?
+    curr <- next curr
+    count <- add count, 1
+    loop
+  }
+  return-unless curr, original
+  return curr
+]
+
+scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with a long, wrapped line and more than a screen of
+  # other lines
+  s:text <- new [abcdef
+g
+h
+i]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .g         .
+  ]
+  # position cursor at last line, then try to move further down
+  assume-console [
+    left-click 3, 0
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows partial wrapped line
+  screen-should-contain [
+    .          .
+    .ef        .
+    .g         .
+    .h         .
+  ]
+]
+
+scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys-2 [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # editor starts with a long line wrapping twice
+  s:text <- new [abcdefghij
+k
+l
+m]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  # position cursor at last line, then try to move further down
+  assume-console [
+    left-click 3, 0
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows partial wrapped line containing a wrap icon
+  screen-should-contain [
+    .          .
+    .efgh↩     .
+    .ij        .
+    .k         .
+  ]
+  # scroll down again
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows partial wrapped line
+  screen-should-contain [
+    .          .
+    .ij        .
+    .k         .
+    .l         .
+  ]
+]
+
+scenario editor-scrolls-down-when-line-wraps [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 5/width, 4/height
+  # editor contains a long line in the third line
+  s:text <- new [a
+b
+cdef]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  # position cursor at end, type a character
+  assume-console [
+    left-click 3, 4
+    type [g]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # screen scrolls
+  screen-should-contain [
+    .     .
+    .b    .
+    .cdef↩.
+    .g    .
+  ]
+  memory-should-contain [
+    3 <- 3
+    4 <- 1
+  ]
+]
+
+scenario editor-stops-scrolling-once-bottom-is-visible [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with 2 lines
+  s:text <- new [a
+b]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  # position cursor at last line, then try to move further down
+  assume-console [
+    left-click 3, 0
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # no change since the bottom border was already visible
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-scrolls-down-on-newline [
+  local-scope
+  assume-screen 5/width, 4/height
+  # position cursor after last line and type newline
+  s:text <- new [a
+b
+c]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  assume-console [
+    left-click 3, 4
+    type [
+]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # screen scrolls
+  screen-should-contain [
+    .     .
+    .b    .
+    .c    .
+    .     .
+  ]
+  memory-should-contain [
+    3 <- 3
+    4 <- 0
+  ]
+]
+
+scenario editor-scrolls-down-on-right-arrow [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 5/width, 4/height
+  # editor contains a wrapped line
+  s:text <- new [a
+b
+cdefgh]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  # position cursor at end of screen and try to move right
+  assume-console [
+    left-click 3, 3
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # screen scrolls
+  screen-should-contain [
+    .     .
+    .b    .
+    .cdef↩.
+    .gh   .
+  ]
+  memory-should-contain [
+    3 <- 3
+    4 <- 0
+  ]
+]
+
+scenario editor-scrolls-down-on-right-arrow-2 [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 5/width, 4/height
+  # editor contains more lines than can fit on screen
+  s:text <- new [a
+b
+c
+d]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  # position cursor at end of screen and try to move right
+  assume-console [
+    left-click 3, 3
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # screen scrolls
+  screen-should-contain [
+    .     .
+    .b    .
+    .c    .
+    .d    .
+  ]
+  memory-should-contain [
+    3 <- 3
+    4 <- 0
+  ]
+]
+
+scenario editor-scrolls-at-end-on-down-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+de]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # try to move down past end of text
+  assume-console [
+    left-click 2, 0
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # no change
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+]
+
+scenario editor-combines-page-and-line-scroll [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with a few pages of lines
+  s:text <- new [a
+b
+c
+d
+e
+f
+g]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  # scroll down one page and one line
+  assume-console [
+    press page-down
+    left-click 3, 0
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen scrolls down 3 lines
+  screen-should-contain [
+    .          .
+    .d         .
+    .e         .
+    .f         .
+  ]
+]
+
+# scroll up if necessary
+
+scenario editor-can-scroll-up-using-arrow-keys [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with >3 lines
+  s:text <- new [a
+b
+c
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+  # position cursor at top of second page, then try to move up
+  assume-console [
+    press page-down
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen slides by one line
+  screen-should-contain [
+    .          .
+    .b         .
+    .c         .
+    .d         .
+  ]
+]
+
+after <scroll-up> [
+  trace 10, [app], [scroll up]
+  top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+  old-top:&:duplex-list:char <- copy top-of-screen
+  top-of-screen <- before-previous-screen-line top-of-screen, editor
+  *editor <- put *editor, top-of-screen:offset, top-of-screen
+  no-movement?:bool <- equal old-top, top-of-screen
+  return-if no-movement?, false/don't-render
+]
+
+scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with a long, wrapped line and more than a screen of
+  # other lines
+  s:text <- new [abcdef
+g
+h
+i]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .g         .
+  ]
+  # position cursor at top of second page, just below wrapped line
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .g         .
+    .h         .
+    .i         .
+  ]
+  # now move up one line
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows partial wrapped line
+  screen-should-contain [
+    .          .
+    .ef        .
+    .g         .
+    .h         .
+  ]
+]
+
+scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-2 [
+  local-scope
+  # screen has 1 line for menu + 4 lines
+  assume-screen 10/width, 5/height
+  # editor starts with a long line wrapping twice, occupying 3 of the 4 lines
+  s:text <- new [abcdefghij
+k
+l
+m]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  # position cursor at top of second page
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .k         .
+    .l         .
+    .m         .
+    .┈┈┈┈┈     .
+  ]
+  # move up one line
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows partial wrapped line
+  screen-should-contain [
+    .          .
+    .ij        .
+    .k         .
+    .l         .
+    .m         .
+  ]
+  # move up a second line
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows partial wrapped line
+  screen-should-contain [
+    .          .
+    .efgh↩     .
+    .ij        .
+    .k         .
+    .l         .
+  ]
+  # move up a third line
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows partial wrapped line
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .efgh↩     .
+    .ij        .
+    .k         .
+  ]
+]
+
+# same as editor-scrolls-up-past-wrapped-line-using-arrow-keys but length
+# slightly off, just to prevent over-training
+scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-3 [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with a long, wrapped line and more than a screen of
+  # other lines
+  s:text <- new [abcdef
+g
+h
+i]
+  e:&:editor <- new-editor s, 0/left, 6/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcde↩    .
+    .f         .
+    .g         .
+  ]
+  # position cursor at top of second page, just below wrapped line
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .g         .
+    .h         .
+    .i         .
+  ]
+  # now move up one line
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows partial wrapped line
+  screen-should-contain [
+    .          .
+    .f         .
+    .g         .
+    .h         .
+  ]
+]
+
+# check empty lines
+scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-4 [
+  local-scope
+  assume-screen 10/width, 4/height
+  # initialize editor with some lines around an empty line
+  s:text <- new [a
+b
+
+c
+d
+e]
+  e:&:editor <- new-editor s, 0/left, 6/right
+  editor-render screen, e
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .          .
+    .c         .
+    .d         .
+  ]
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .d         .
+    .e         .
+    .┈┈┈┈┈┈    .
+  ]
+  assume-console [
+    press page-up
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .          .
+    .c         .
+    .d         .
+  ]
+]
+
+scenario editor-scrolls-up-on-left-arrow [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 5/width, 4/height
+  # editor contains >3 lines
+  s:text <- new [a
+b
+c
+d
+e]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  # position cursor at top of second page
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .     .
+    .c    .
+    .d    .
+    .e    .
+  ]
+  # now try to move left
+  assume-console [
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # screen scrolls
+  screen-should-contain [
+    .     .
+    .b    .
+    .c    .
+    .d    .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+]
+
+scenario editor-can-scroll-up-to-start-of-file [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with >3 lines
+  s:text <- new [a
+b
+c
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+  # position cursor at top of second page, then try to move up to start of
+  # text
+  assume-console [
+    press page-down
+    press up-arrow
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen slides by one line
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+  # try to move up again
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen remains unchanged
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+]
+
+# ctrl-f/page-down - render next page if it exists
+
+scenario editor-can-scroll [
+  local-scope
+  assume-screen 10/width, 4/height
+  s:text <- new [a
+b
+c
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+  # scroll down
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows next page
+  screen-should-contain [
+    .          .
+    .c         .
+    .d         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+after <handle-special-character> [
+  {
+    page-down?:bool <- equal c, 6/ctrl-f
+    break-unless page-down?
+    old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
+    <begin-move-cursor>
+    page-down editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+    movement?:bool <- not-equal top-of-screen, old-top
+    return movement?/go-render
+  }
+]
+
+after <handle-special-key> [
+  {
+    page-down?:bool <- equal k, 65518/page-down
+    break-unless page-down?
+    old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
+    <begin-move-cursor>
+    page-down editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+    movement?:bool <- not-equal top-of-screen, old-top
+    return movement?/go-render
+  }
+]
+
+# page-down skips entire wrapped lines, so it can't scroll past lines
+# taking up the entire screen
+def page-down editor:&:editor -> editor:&:editor [
+  local-scope
+  load-inputs
+  # if editor contents don't overflow screen, do nothing
+  bottom-of-screen:&:duplex-list:char <- get *editor, bottom-of-screen:offset
+  return-unless bottom-of-screen
+  # if not, position cursor at final character
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  before-cursor:&:duplex-list:char <- prev bottom-of-screen
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  # keep one line in common with previous page
+  {
+    last:char <- get *before-cursor, value:offset
+    newline?:bool <- equal last, 10/newline
+    break-unless newline?:bool
+    before-cursor <- prev before-cursor
+    *editor <- put *editor, before-cursor:offset, before-cursor
+  }
+  # move cursor and top-of-screen to start of that line
+  move-to-start-of-line editor
+  before-cursor <- get *editor, before-cursor:offset
+  *editor <- put *editor, top-of-screen:offset, before-cursor
+]
+
+# jump to previous newline
+def move-to-start-of-line editor:&:editor -> editor:&:editor [
+  local-scope
+  load-inputs
+  # update cursor column
+  left:num <- get *editor, left:offset
+  cursor-column:num <- copy left
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  # update before-cursor
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  init:&:duplex-list:char <- get *editor, data:offset
+  # while not at start of line, move
+  {
+    at-start-of-text?:bool <- equal before-cursor, init
+    break-if at-start-of-text?
+    prev:char <- get *before-cursor, value:offset
+    at-start-of-line?:bool <- equal prev, 10/newline
+    break-if at-start-of-line?
+    before-cursor <- prev before-cursor
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    assert before-cursor, [move-to-start-of-line tried to move before start of text]
+    loop
+  }
+]
+
+scenario editor-does-not-scroll-past-end [
+  local-scope
+  assume-screen 10/width, 4/height
+  s:text <- new [a
+b]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  # scroll down
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen remains unmodified
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-starts-next-page-at-start-of-wrapped-line [
+  local-scope
+  # screen has 1 line for menu + 3 lines for text
+  assume-screen 10/width, 4/height
+  # editor contains a long last line
+  s:text <- new [a
+b
+cdefgh]
+  # editor screen triggers wrap of last line
+  e:&:editor <- new-editor s, 0/left, 4/right
+  editor-render screen, e
+  # some part of last line is not displayed
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .cde↩      .
+  ]
+  # scroll down
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows entire wrapped line
+  screen-should-contain [
+    .          .
+    .cde↩      .
+    .fgh       .
+    .┈┈┈┈      .
+  ]
+]
+
+scenario editor-starts-next-page-at-start-of-wrapped-line-2 [
+  local-scope
+  # screen has 1 line for menu + 3 lines for text
+  assume-screen 10/width, 4/height
+  # editor contains a very long line that occupies last two lines of screen
+  # and still has something left over
+  s:text <- new [a
+bcdefgh]
+  e:&:editor <- new-editor s, 0/left, 4/right
+  editor-render screen, e
+  # some part of last line is not displayed
+  screen-should-contain [
+    .          .
+    .a         .
+    .bcd↩      .
+    .efg↩      .
+  ]
+  # scroll down
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows entire wrapped line
+  screen-should-contain [
+    .          .
+    .bcd↩      .
+    .efg↩      .
+    .h         .
+  ]
+]
+
+# ctrl-b/page-up - render previous page if it exists
+
+scenario editor-can-scroll-up [
+  local-scope
+  assume-screen 10/width, 4/height
+  s:text <- new [a
+b
+c
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+  # scroll down
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows next page
+  screen-should-contain [
+    .          .
+    .c         .
+    .d         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  # scroll back up
+  assume-console [
+    press page-up
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows original page again
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+]
+
+after <handle-special-character> [
+  {
+    page-up?:bool <- equal c, 2/ctrl-b
+    break-unless page-up?
+    old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
+    <begin-move-cursor>
+    editor <- page-up editor, screen-height
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+    movement?:bool <- not-equal top-of-screen, old-top
+    return movement?/go-render
+  }
+]
+
+after <handle-special-key> [
+  {
+    page-up?:bool <- equal k, 65519/page-up
+    break-unless page-up?
+    old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
+    <begin-move-cursor>
+    editor <- page-up editor, screen-height
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+    movement?:bool <- not-equal top-of-screen, old-top
+    # don't bother re-rendering if nothing changed. todo: test this
+    return movement?/go-render
+  }
+]
+
+def page-up editor:&:editor, screen-height:num -> editor:&:editor [
+  local-scope
+  load-inputs
+  max:num <- subtract screen-height, 1/menu-bar, 1/overlapping-line
+  count:num <- copy 0
+  top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+  {
+    done?:bool <- greater-or-equal count, max
+    break-if done?
+    prev:&:duplex-list:char <- before-previous-screen-line top-of-screen, editor
+    break-unless prev
+    top-of-screen <- copy prev
+    *editor <- put *editor, top-of-screen:offset, top-of-screen
+    count <- add count, 1
+    loop
+  }
+]
+
+scenario editor-can-scroll-up-multiple-pages [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 10/width, 4/height
+  # initialize editor with 8 lines
+  s:text <- new [a
+b
+c
+d
+e
+f
+g
+h]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+  # scroll down two pages
+  assume-console [
+    press page-down
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows third page
+  screen-should-contain [
+    .          .
+    .e         .
+    .f         .
+    .g         .
+  ]
+  # scroll up
+  assume-console [
+    press page-up
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows second page
+  screen-should-contain [
+    .          .
+    .c         .
+    .d         .
+    .e         .
+  ]
+  # scroll up again
+  assume-console [
+    press page-up
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows original page again
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+  ]
+]
+
+scenario editor-can-scroll-up-wrapped-lines [
+  local-scope
+  # screen has 1 line for menu + 5 lines for text
+  assume-screen 10/width, 6/height
+  # editor contains a long line in the first page
+  s:text <- new [a
+b
+cdefgh
+i
+j
+k
+l
+m
+n
+o]
+  # editor screen triggers wrap of last line
+  e:&:editor <- new-editor s, 0/left, 4/right
+  editor-render screen, e
+  # some part of last line is not displayed
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .cde↩      .
+    .fgh       .
+    .i         .
+  ]
+  # scroll down a page and a line
+  assume-console [
+    press page-down
+    left-click 5, 0
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows entire wrapped line
+  screen-should-contain [
+    .          .
+    .j         .
+    .k         .
+    .l         .
+    .m         .
+    .n         .
+  ]
+  # now scroll up one page
+  assume-console [
+    press page-up
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen resets
+  screen-should-contain [
+    .          .
+    .b         .
+    .cde↩      .
+    .fgh       .
+    .i         .
+    .j         .
+  ]
+]
+
+scenario editor-can-scroll-up-wrapped-lines-2 [
+  local-scope
+  # screen has 1 line for menu + 3 lines for text
+  assume-screen 10/width, 4/height
+  # editor contains a very long line that occupies last two lines of screen
+  # and still has something left over
+  s:text <- new [a
+bcdefgh]
+  e:&:editor <- new-editor s, 0/left, 4/right
+  editor-render screen, e
+  # some part of last line is not displayed
+  screen-should-contain [
+    .          .
+    .a         .
+    .bcd↩      .
+    .efg↩      .
+  ]
+  # scroll down
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows entire wrapped line
+  screen-should-contain [
+    .          .
+    .bcd↩      .
+    .efg↩      .
+    .h         .
+  ]
+  # scroll back up
+  assume-console [
+    press page-up
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen resets
+  screen-should-contain [
+    .          .
+    .a         .
+    .bcd↩      .
+    .efg↩      .
+  ]
+]
+
+scenario editor-can-scroll-up-past-nonempty-lines [
+  local-scope
+  assume-screen 10/width, 4/height
+  # text with empty line in second screen
+  s:text <- new [axx
+bxx
+cxx
+dxx
+exx
+fxx
+gxx
+hxx
+]
+  e:&:editor <- new-editor s, 0/left, 4/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .axx       .
+    .bxx       .
+    .cxx       .
+  ]
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .cxx       .
+    .dxx       .
+    .exx       .
+  ]
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .exx       .
+    .fxx       .
+    .gxx       .
+  ]
+  # scroll back up past empty line
+  assume-console [
+    press page-up
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .cxx       .
+    .dxx       .
+    .exx       .
+  ]
+]
+
+scenario editor-can-scroll-up-past-empty-lines [
+  local-scope
+  assume-screen 10/width, 4/height
+  # text with empty line in second screen
+  s:text <- new [axy
+bxy
+cxy
+
+dxy
+exy
+fxy
+gxy
+]
+  e:&:editor <- new-editor s, 0/left, 4/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .axy       .
+    .bxy       .
+    .cxy       .
+  ]
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .cxy       .
+    .          .
+    .dxy       .
+  ]
+  assume-console [
+    press page-down
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .dxy       .
+    .exy       .
+    .fxy       .
+  ]
+  # scroll back up past empty line
+  assume-console [
+    press page-up
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .cxy       .
+    .          .
+    .dxy       .
+  ]
+]
+
+# ctrl-s - scroll up by one line
+# todo: scenarios
+
+after <handle-special-character> [
+  {
+    scroll-up?:bool <- equal c, 19/ctrl-s
+    break-unless scroll-up?
+    <begin-move-cursor>
+    go-render?:bool, editor <- line-up editor, screen-height
+    undo-coalesce-tag:num <- copy 5/line-up
+    <end-move-cursor>
+    return go-render?
+  }
+]
+
+def line-up editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  max:num <- subtract right, left
+  old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
+  new-top:&:duplex-list:char <- before-start-of-next-line old-top, max
+  movement?:bool <- not-equal old-top, new-top
+  {
+    break-unless movement?
+    *editor <- put *editor, top-of-screen:offset, new-top
+  }
+  return movement?
+]
+
+# ctrl-x - scroll down by one line
+# todo: scenarios
+
+after <handle-special-character> [
+  {
+    scroll-down?:bool <- equal c, 24/ctrl-x
+    break-unless scroll-down?
+    <begin-move-cursor>
+    go-render?:bool, editor <- line-down editor, screen-height
+    undo-coalesce-tag:num <- copy 6/line-down
+    <end-move-cursor>
+    return go-render?
+  }
+]
+
+def line-down editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
+  new-top:&:duplex-list:char <- before-previous-screen-line old-top, editor
+  movement?:bool <- not-equal old-top, new-top
+  {
+    break-unless movement?
+    *editor <- put *editor, top-of-screen:offset, new-top
+  }
+  return movement?
+]
+
+# ctrl-t - move current line to top of screen
+# todo: scenarios
+
+after <handle-special-character> [
+  {
+    scroll-down?:bool <- equal c, 20/ctrl-t
+    break-unless scroll-down?
+    <begin-move-cursor>
+    old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
+    cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    cursor <- next cursor
+    new-top:&:duplex-list:char <- before-previous-screen-line cursor, editor
+    *editor <- put *editor, top-of-screen:offset, new-top
+    *editor <- put *editor, cursor-row:offset, 1
+    go-render?:bool <- not-equal new-top, old-top
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return go-render?
+  }
+]
+
+# ctrl-/ - comment/uncomment current line
+
+after <handle-special-character> [
+  {
+    comment-toggle?:bool <- equal c, 31/ctrl-slash
+    break-unless comment-toggle?
+    cursor-column:num <- get *editor, cursor-column:offset
+    data:&:duplex-list:char <- get *editor, data:offset
+    <begin-insert-character>
+    before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
+    line-start:&:duplex-list:char <- next before-line-start
+    commented-out?:bool <- match line-start, [#? ]  # comment prefix
+    {
+      break-unless commented-out?
+      # uncomment
+      data <- remove line-start, 3/length-comment-prefix, data
+      cursor-column <- subtract cursor-column, 3/length-comment-prefix
+      *editor <- put *editor, cursor-column:offset, cursor-column
+      go-render? <- render-line-from-start screen, editor, 3/size-of-comment-leader
+    }
+    {
+      break-if commented-out?
+      # comment
+      insert before-line-start, [#? ]
+      cursor-column <- add cursor-column, 3/length-comment-prefix
+      *editor <- put *editor, cursor-column:offset, cursor-column
+      go-render? <- render-line-from-start screen, editor, 0
+    }
+    <end-insert-character>
+    return
+  }
+]
+
+# Render just from the start of the current line, and only if it wasn't
+# wrapping before (include margin) and isn't wrapping now. Otherwise just tell
+# the caller to go-render? the entire screen.
+def render-line-from-start screen:&:screen, editor:&:editor, right-margin:num -> go-render?:bool, screen:&:screen [
+  local-scope
+  load-inputs
+  before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
+  line-start:&:duplex-list:char <- next before-line-start
+  color:num <- copy 7/white
+  left:num <- get *editor, left:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  screen <- move-cursor screen, cursor-row, left
+  right:num <- get *editor, right:offset
+  end:num <- subtract right, right-margin
+  i:num <- copy 0
+  curr:&:duplex-list:char <- copy line-start
+  {
+    render-all?:bool <- greater-or-equal i, end
+    return-if render-all?, true/go-render
+    break-unless curr
+    c:char <- get *curr, value:offset
+    newline?:bool <- equal c, 10/newline
+    break-if newline?
+    color <- get-color color, c
+    print screen, c, color
+    curr <- next curr
+    i <- add i, 1
+    loop
+  }
+  clear-line-until screen, right
+  return false/dont-render
+]
+
+def before-start-of-screen-line editor:&:editor -> result:&:duplex-list:char [
+  local-scope
+  load-inputs
+  cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  {
+    next:&:duplex-list:char <- next cursor
+    break-unless next
+    cursor <- copy next
+  }
+  result <- before-previous-screen-line cursor, editor
+]
+
+scenario editor-comments-empty-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .#?        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 3
+  ]
+  check-trace-count-for-label 5, [print-character]
+]
+
+scenario editor-comments-at-start-of-contents [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [ab], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .#? ab     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 3
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-comments-at-end-of-contents [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [ab], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 7
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .#? ab     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 5
+  ]
+  check-trace-count-for-label 10, [print-character]
+  # toggle to uncomment
+  $clear-trace
+  assume-console [
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .ab        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-comments-almost-wrapping-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # editor starts out with a non-wrapping line
+  e:&:editor <- new-editor [abcd], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  $clear-trace
+  # on commenting the line is now wrapped
+  assume-console [
+    left-click 1, 7
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .#? a↩     .
+    .bcd       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+]
+
+scenario editor-uncomments-just-wrapping-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # editor starts out with a comment that wraps the line
+  e:&:editor <- new-editor [#? ab], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .#? a↩     .
+    .b         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  $clear-trace
+  # on uncommenting the line is no longer wrapped
+  assume-console [
+    left-click 1, 7
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .ab        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+]
diff --git a/archive/2.vm/edit/004-programming-environment.mu b/archive/2.vm/edit/004-programming-environment.mu
new file mode 100644
index 00000000..dec8a2d5
--- /dev/null
+++ b/archive/2.vm/edit/004-programming-environment.mu
@@ -0,0 +1,549 @@
+## putting the environment together out of editors
+#
+# Consists of one editor on the left for recipes and one on the right for the
+# sandbox.
+
+def! main [
+  local-scope
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  env:&:environment <- new-programming-environment null/filesystem, null/screen
+  render-all null/screen, env, render
+  event-loop null/screen, null/console, env, null/filesystem
+]
+
+container environment [
+  recipes:&:editor
+  current-sandbox:&:editor
+  sandbox-in-focus?:bool  # false => cursor in recipes; true => cursor in current-sandbox
+]
+
+def new-programming-environment resources:&:resources, screen:&:screen, test-sandbox-editor-contents:text -> result:&:environment [
+  local-scope
+  load-inputs
+  width:num <- screen-width screen
+  result <- new environment:type
+  # recipe editor on the left
+  initial-recipe-contents:text <- slurp resources, [lesson/recipes.mu]  # ignore errors
+  divider:num, _ <- divide-with-remainder width, 2
+  recipes:&:editor <- new-editor initial-recipe-contents, 0/left, divider/right
+  # sandbox editor on the right
+  sandbox-left:num <- add divider, 1
+  current-sandbox:&:editor <- new-editor test-sandbox-editor-contents, sandbox-left, width/right
+  *result <- put *result, recipes:offset, recipes
+  *result <- put *result, current-sandbox:offset, current-sandbox
+  *result <- put *result, sandbox-in-focus?:offset, false
+  <programming-environment-initialization>
+]
+
+def event-loop screen:&:screen, console:&:console, env:&:environment, resources:&:resources -> screen:&:screen, console:&:console, env:&:environment, resources:&:resources [
+  local-scope
+  load-inputs
+  recipes:&:editor <- get *env, recipes:offset
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
+  # if we fall behind we'll stop updating the screen, but then we have to
+  # render the entire screen when we catch up.
+  # todo: test this
+  render-recipes-on-no-more-events?:bool <- copy false
+  render-sandboxes-on-no-more-events?:bool <- copy false
+  {
+    # looping over each (keyboard or touch) event as it occurs
+    +next-event
+    e:event, found?:bool, quit?:bool, console <- read-event console
+    loop-unless found?
+    break-if quit?  # only in tests
+    trace 10, [app], [next-event]
+    <handle-event>
+    # check for global events that will trigger regardless of which editor has focus
+    {
+      k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
+      break-unless is-keycode?
+      <global-keypress>
+    }
+    {
+      c:char, is-unicode?:bool <- maybe-convert e:event, text:variant
+      break-unless is-unicode?
+      <global-type>
+    }
+    # 'touch' event - send to both sides, see what picks it up
+    {
+      t:touch-event, is-touch?:bool <- maybe-convert e:event, touch:variant
+      break-unless is-touch?
+      # ignore all but 'left-click' events for now
+      # todo: test this
+      touch-type:num <- get t, type:offset
+      is-left-click?:bool <- equal touch-type, 65513/mouse-left
+      loop-unless is-left-click?, +next-event
+      click-row:num <- get t, row:offset
+      click-column:num <- get t, column:offset
+      # later exceptions for non-editor touches will go here
+      <global-touch>
+      # send to both editors
+      _ <- move-cursor recipes, screen, t
+      sandbox-in-focus?:bool <- move-cursor current-sandbox, screen, t
+      *env <- put *env, sandbox-in-focus?:offset, sandbox-in-focus?
+      screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+      loop +next-event
+    }
+    # 'resize' event - redraw editor
+    # todo: test this after supporting resize in assume-console
+    {
+      r:resize-event, is-resize?:bool <- maybe-convert e:event, resize:variant
+      break-unless is-resize?
+      env, screen <- resize screen, env
+      screen <- render-all screen, env, render-without-moving-cursor
+      loop +next-event
+    }
+    # if it's not global and not a touch event, send to appropriate editor
+    {
+      sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
+      {
+        break-if sandbox-in-focus?
+        render?:bool <- handle-keyboard-event screen, recipes, e:event
+        render-recipes-on-no-more-events? <- or render?, render-recipes-on-no-more-events?
+      }
+      {
+        break-unless sandbox-in-focus?
+        render?:bool <- handle-keyboard-event screen, current-sandbox, e:event
+        render-sandboxes-on-no-more-events? <- or render?, render-sandboxes-on-no-more-events?
+      }
+      more-events?:bool <- has-more-events? console
+      {
+        break-if more-events?
+        {
+          break-unless render-recipes-on-no-more-events?
+          render-recipes-on-no-more-events? <- copy false
+          screen <- render-recipes screen, env, render
+        }
+        {
+          break-unless render-sandboxes-on-no-more-events?
+          render-sandboxes-on-no-more-events? <- copy false
+          screen <- render-sandbox-side screen, env, render
+        }
+      }
+      screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    }
+    loop
+  }
+]
+
+def resize screen:&:screen, env:&:environment -> env:&:environment, screen:&:screen [
+  local-scope
+  load-inputs
+  clear-screen screen  # update screen dimensions
+  width:num <- screen-width screen
+  divider:num, _ <- divide-with-remainder width, 2
+  # update recipe editor
+  recipes:&:editor <- get *env, recipes:offset
+  right:num <- subtract divider, 1
+  *recipes <- put *recipes, right:offset, right
+  # reset cursor (later we'll try to preserve its position)
+  *recipes <- put *recipes, cursor-row:offset, 1
+  *recipes <- put *recipes, cursor-column:offset, 0
+  # update sandbox editor
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  left:num <- add divider, 1
+  *current-sandbox <- put *current-sandbox, left:offset, left
+  right:num <- subtract width, 1
+  *current-sandbox <- put *current-sandbox, right:offset, right
+  # reset cursor (later we'll try to preserve its position)
+  *current-sandbox <- put *current-sandbox, cursor-row:offset, 1
+  *current-sandbox <- put *current-sandbox, cursor-column:offset, left
+]
+
+# Variant of 'render' that updates cursor-row and cursor-column based on
+# before-cursor (rather than the other way around). If before-cursor moves
+# off-screen, it resets cursor-row and cursor-column.
+def render-without-moving-cursor screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor, 1/top, 0/left
+  left:num <- get *editor, left:offset
+  screen-height:num <- screen-height screen
+  right:num <- get *editor, right:offset
+  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
+  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
+  curr <- next curr
+  color:num <- copy 7/white
+  row:num <- copy 1/top
+  column:num <- copy left
+  # save before-cursor
+  old-before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  # initialze cursor-row/cursor-column/before-cursor to the top of the screen
+  # by default
+  *editor <- put *editor, cursor-row:offset, row
+  *editor <- put *editor, cursor-column:offset, column
+  top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+  *editor <- put *editor, before-cursor:offset, top-of-screen
+  screen <- move-cursor screen, row, column
+  {
+    +next-character
+    break-unless curr
+    off-screen?:bool <- greater-or-equal row, screen-height
+    break-if off-screen?
+    # if we find old-before-cursor still on the new resized screen, update
+    # editor.cursor-row and editor.cursor-column based on
+    # old-before-cursor
+    {
+      at-cursor?:bool <- equal old-before-cursor, prev
+      break-unless at-cursor?
+      *editor <- put *editor, cursor-row:offset, row
+      *editor <- put *editor, cursor-column:offset, column
+      *editor <- put *editor, before-cursor:offset, old-before-cursor
+    }
+    c:char <- get *curr, value:offset
+    <character-c-received>
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # clear rest of line in this window
+      clear-line-until screen, right
+      # skip to next line
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      curr <- next curr
+      prev <- next prev
+      loop +next-character
+    }
+    {
+      # at right? wrap. even if there's only one more letter left; we need
+      # room for clicking on the cursor after it.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment curr
+      loop +next-character
+    }
+    print screen, c, color
+    curr <- next curr
+    prev <- next prev
+    column <- add column, 1
+    loop
+  }
+  # save first character off-screen
+  *editor <- put *editor, bottom-of-screen:offset, curr
+  *editor <- put *editor, bottom:offset, row
+  return row, column
+]
+
+scenario point-at-multiple-editors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 5/height
+  # initialize both halves of screen
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |abc|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [def]  # contents of sandbox editor
+  # focus on both sides
+  assume-console [
+    left-click 1, 1
+    left-click 1, 17
+  ]
+  # check cursor column in each
+  run [
+    event-loop screen, console, env, resources
+    recipes:&:editor <- get *env, recipes:offset
+    5:num/raw <- get *recipes, cursor-column:offset
+    sandbox:&:editor <- get *env, current-sandbox:offset
+    7:num/raw <- get *sandbox, cursor-column:offset
+  ]
+  memory-should-contain [
+    5 <- 1
+    7 <- 17
+  ]
+]
+
+scenario edit-multiple-editors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 5/height
+  # initialize both halves of screen
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |abc|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [def]  # contents of sandbox
+  render-all screen, env, render
+  # type one letter in each of them
+  assume-console [
+    left-click 1, 1
+    type [0]
+    left-click 1, 17
+    type [1]
+  ]
+  run [
+    event-loop screen, console, env, resources
+    recipes:&:editor <- get *env, recipes:offset
+    5:num/raw <- get *recipes, cursor-column:offset
+    sandbox:&:editor <- get *env, current-sandbox:offset
+    7:num/raw <- get *sandbox, cursor-column:offset
+  ]
+  screen-should-contain [
+    .           run (F4)           .  # this line has a different background, but we don't test that yet
+    .a0bc           ┊d1ef          .
+    .               ┊──────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊              .
+    .               ┊              .
+  ]
+  memory-should-contain [
+    5 <- 2  # cursor column of recipe editor
+    7 <- 18  # cursor column of sandbox editor
+  ]
+  # show the cursor at the right window
+  run [
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  screen-should-contain [
+    .           run (F4)           .
+    .a0bc           ┊d1␣f          .
+    .               ┊──────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊              .
+    .               ┊              .
+  ]
+]
+
+scenario editor-in-focus-keeps-cursor [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 5/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |abc|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [def]
+  render-all screen, env, render
+  # initialize programming environment and highlight cursor
+  assume-console []
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # is cursor at the right place?
+  screen-should-contain [
+    .           run (F4)           .
+    .␣bc            ┊def           .
+    .               ┊──────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊              .
+    .               ┊              .
+  ]
+  # now try typing a letter
+  assume-console [
+    type [z]
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # cursor should still be right
+  screen-should-contain [
+    .           run (F4)           .
+    .z␣bc           ┊def           .
+    .               ┊──────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊              .
+    .               ┊              .
+  ]
+]
+
+scenario backspace-in-sandbox-editor-joins-lines [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 5/height
+  assume-resources [
+  ]
+  # initialize sandbox side with two lines
+  test-sandbox-editor-contents:text <- new [abc
+def]
+  env:&:environment <- new-programming-environment resources, screen, test-sandbox-editor-contents
+  render-all screen, env, render
+  screen-should-contain [
+    .           run (F4)           .
+    .               ┊abc           .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊def           .
+    .               ┊──────────────.
+    .               ┊              .
+  ]
+  # position cursor at start of second line and hit backspace
+  assume-console [
+    left-click 2, 16
+    press backspace
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # cursor moves to end of old line
+  screen-should-contain [
+    .           run (F4)           .
+    .               ┊abc␣ef        .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊──────────────.
+    .               ┊              .
+  ]
+]
+
+type render-recipe = (recipe (address screen) (address editor) -> number number (address screen) (address editor))
+
+def render-all screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
+  local-scope
+  load-inputs
+  trace 10, [app], [render all]
+  # top menu
+  trace 11, [app], [render top menu]
+  width:num <- screen-width screen
+  draw-horizontal screen, 0, 0/left, width, 32/space, 0/black, 238/grey
+  button-start:num <- subtract width, 20
+  button-on-screen?:bool <- greater-or-equal button-start, 0
+  assert button-on-screen?, [screen too narrow for menu]
+  screen <- move-cursor screen, 0/row, button-start
+  print screen, [ run (F4) ], 255/white, 161/reddish
+  # dotted line down the middle
+  trace 11, [app], [render divider]
+  divider:num, _ <- divide-with-remainder width, 2
+  height:num <- screen-height screen
+  draw-vertical screen, divider, 1/top, height, 9482/vertical-dotted
+  #
+  screen <- render-recipes screen, env, render-editor
+  screen <- render-sandbox-side screen, env, render-editor
+  <end-render-components>  # no early returns permitted
+  #
+  recipes:&:editor <- get *env, recipes:offset
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
+  screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+]
+
+def render-recipes screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
+  local-scope
+  load-inputs
+  trace 11, [app], [render recipes]
+  old-top-idx:num <- save-top-idx screen
+  recipes:&:editor <- get *env, recipes:offset
+  # render recipes
+  left:num <- get *recipes, left:offset
+  right:num <- get *recipes, right:offset
+  row:num, column:num, screen <- call render-editor, screen, recipes
+  <end-render-recipe-components>
+  # draw dotted line after recipes
+  draw-horizontal screen, row, left, right, 9480/horizontal-dotted
+  row <- add row, 1
+  clear-screen-from screen, row, left, left, right
+  #
+  assert-no-scroll screen, old-top-idx
+]
+
+# replaced in a later layer
+def render-sandbox-side screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
+  local-scope
+  load-inputs
+  trace 11, [app], [render sandboxes]
+  old-top-idx:num <- save-top-idx screen
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  left:num <- get *current-sandbox, left:offset
+  right:num <- get *current-sandbox, right:offset
+  row:num, column:num, screen, current-sandbox <- call render-editor, screen, current-sandbox
+  # draw solid line after code (you'll see why in later layers)
+  draw-horizontal screen, row, left, right
+  row <- add row, 1
+  clear-screen-from screen, row, left, left, right
+  #
+  assert-no-scroll screen, old-top-idx
+]
+
+def update-cursor screen:&:screen, recipes:&:editor, current-sandbox:&:editor, sandbox-in-focus?:bool, env:&:environment -> screen:&:screen [
+  local-scope
+  load-inputs
+  <update-cursor-special-cases>
+  {
+    break-if sandbox-in-focus?
+    cursor-row:num <- get *recipes, cursor-row:offset
+    cursor-column:num <- get *recipes, cursor-column:offset
+  }
+  {
+    break-unless sandbox-in-focus?
+    cursor-row:num <- get *current-sandbox, cursor-row:offset
+    cursor-column:num <- get *current-sandbox, cursor-column:offset
+  }
+  screen <- move-cursor screen, cursor-row, cursor-column
+]
+
+# ctrl-n - switch focus
+# todo: test this
+
+after <global-type> [
+  {
+    switch-side?:bool <- equal c, 14/ctrl-n
+    break-unless switch-side?
+    sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
+    sandbox-in-focus? <- not sandbox-in-focus?
+    *env <- put *env, sandbox-in-focus?:offset, sandbox-in-focus?
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+## helpers
+
+def draw-vertical screen:&:screen, col:num, y:num, bottom:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  style:char, style-found?:bool <- next-input
+  {
+    break-if style-found?
+    style <- copy 9474/vertical
+  }
+  color:num, color-found?:bool <- next-input
+  {
+    # default color to white
+    break-if color-found?
+    color <- copy 245/grey
+  }
+  {
+    continue?:bool <- lesser-than y, bottom
+    break-unless continue?
+    screen <- move-cursor screen, y, col
+    print screen, style, color
+    y <- add y, 1
+    loop
+  }
+]
+
+scenario backspace-over-text [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 15/height
+  # recipes.mu is empty
+  assume-resources [
+  ]
+  # sandbox editor contains an instruction without storing outputs
+  env:&:environment <- new-programming-environment resources, screen, []
+  # run the code in the editors
+  assume-console [
+    type [a]
+    press backspace
+  ]
+  run [
+    event-loop screen, console, env, resources
+    10:num/raw <- get *screen, cursor-row:offset
+    11:num/raw <- get *screen, cursor-column:offset
+  ]
+  memory-should-contain [
+    10 <- 1
+    11 <- 0
+  ]
+]
diff --git a/archive/2.vm/edit/005-sandbox.mu b/archive/2.vm/edit/005-sandbox.mu
new file mode 100644
index 00000000..96ec804d
--- /dev/null
+++ b/archive/2.vm/edit/005-sandbox.mu
@@ -0,0 +1,1193 @@
+## running code from the editor and creating sandboxes
+#
+# Running code in the sandbox editor prepends its contents to a list of
+# (non-editable) sandboxes below the editor, showing the result and maybe a
+# few other things (later layers).
+#
+# This layer draws the menubar buttons in non-editable sandboxes but they
+# don't do anything yet. Later layers implement each button.
+
+def! main [
+  local-scope
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  env:&:environment <- new-programming-environment null/filesystem, null/screen
+  env <- restore-sandboxes env, null/filesystem
+  render-all null/screen, env, render
+  event-loop null/screen, null/console, env, null/filesystem
+]
+
+container environment [
+  sandbox:&:sandbox  # list of sandboxes, from top to bottom. TODO: switch to &:list:sandbox
+  render-from:num
+  number-of-sandboxes:num
+]
+
+after <programming-environment-initialization> [
+  *result <- put *result, render-from:offset, -1
+]
+
+container sandbox [
+  data:text
+  response:text
+  # coordinates to track clicks
+  # constraint: will be 0 for sandboxes at positions before env.render-from
+  starting-row-on-screen:num
+  code-ending-row-on-screen:num  # past end of code
+  screen:&:screen  # prints in the sandbox go here
+  next-sandbox:&:sandbox
+]
+
+scenario run-and-show-results [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  # recipe editor is empty
+  assume-resources [
+  ]
+  # sandbox editor contains an instruction without storing outputs
+  env:&:environment <- new-programming-environment resources, screen, [divide-with-remainder 11, 3]
+  render-all screen, env, render
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints the results
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊divide-with-remainder 11, 3                      .
+    .                                                  ┊3                                                .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  screen-should-contain-in-color 7/white, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   divide-with-remainder 11, 3                      .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+  ]
+  screen-should-contain-in-color 245/grey, [
+    .                                                                                                    .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+    .                                                  ┊3                                                .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # sandbox menu in reverse video
+  screen-should-contain-in-color 232/black, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   0   edit       copy       to recipe    delete    .
+  ]
+  # run another command
+  assume-console [
+    left-click 1, 80
+    type [add 2, 2]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints both sandboxes
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊divide-with-remainder 11, 3                      .
+    .                                                  ┊3                                                .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+after <global-keypress> [
+  # F4? load all code and run all sandboxes.
+  {
+    do-run?:bool <- equal k, 65532/F4
+    break-unless do-run?
+    screen <- update-status screen, [running...       ], 245/grey
+    <begin-run-sandboxes-on-F4>
+    error?:bool <- run-sandboxes env, resources, screen
+    # we could just render-all, but we do some work to minimize the number of prints to screen
+    <end-run-sandboxes-on-F4>
+    screen <- render-sandbox-side screen, env, render
+    {
+      break-if error?
+      screen <- update-status screen, [                 ], 245/grey
+    }
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+def run-sandboxes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, resources:&:resources, screen:&:screen [
+  local-scope
+  load-inputs
+  errors-found?:bool <- update-recipes env, resources, screen
+  jump-if errors-found?, +return
+  # check contents of right editor (sandbox)
+  <begin-run-sandboxes>
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  {
+    sandbox-contents:text <- editor-contents current-sandbox
+    break-unless sandbox-contents
+    # if contents exist, first save them
+    # run them and turn them into a new sandbox
+    new-sandbox:&:sandbox <- new sandbox:type
+    *new-sandbox <- put *new-sandbox, data:offset, sandbox-contents
+    # push to head of sandbox list
+    dest:&:sandbox <- get *env, sandbox:offset
+    *new-sandbox <- put *new-sandbox, next-sandbox:offset, dest
+    *env <- put *env, sandbox:offset, new-sandbox
+    # update sandbox count
+    sandbox-count:num <- get *env, number-of-sandboxes:offset
+    sandbox-count <- add sandbox-count, 1
+    *env <- put *env, number-of-sandboxes:offset, sandbox-count
+    # save all sandboxes
+    # needs to be before running them, in case we die when running
+    save-sandboxes env, resources
+    # clear sandbox editor
+    init:&:duplex-list:char <- push 167/§, null
+    *current-sandbox <- put *current-sandbox, data:offset, init
+    *current-sandbox <- put *current-sandbox, top-of-screen:offset, init
+  }
+  # run all sandboxes
+  curr:&:sandbox <- get *env, sandbox:offset
+  idx:num <- copy 0
+  {
+    break-unless curr
+    curr <- update-sandbox curr, env, idx
+    curr <- get *curr, next-sandbox:offset
+    idx <- add idx, 1
+    loop
+  }
+  <end-run-sandboxes>
+  +return
+  {
+    break-if resources  # ignore this in tests
+    $system [./snapshot_lesson]
+  }
+]
+
+# load code from disk
+# replaced in a later layer (whereupon errors-found? will actually be set)
+def update-recipes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, resources:&:resources, screen:&:screen [
+  local-scope
+  load-inputs
+  recipes:&:editor <- get *env, recipes:offset
+  in:text <- editor-contents recipes
+  resources <- dump resources, [lesson/recipes.mu], in
+  reload in
+  errors-found? <- copy false
+]
+
+# replaced in a later layer
+def update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  response:text, _, fake-screen:&:screen <- run-sandboxed data
+  *sandbox <- put *sandbox, response:offset, response
+  *sandbox <- put *sandbox, screen:offset, fake-screen
+]
+
+def update-status screen:&:screen, msg:text, color:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  screen <- move-cursor screen, 0, 2
+  screen <- print screen, msg, color, 238/grey/background
+]
+
+def save-sandboxes env:&:environment, resources:&:resources -> resources:&:resources [
+  local-scope
+  load-inputs
+  trace 11, [app], [save sandboxes]
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  # first clear previous versions, in case we deleted some sandbox
+  $system [rm lesson/[0-9]* >/dev/null 2>/dev/null]  # some shells can't handle '>&'
+  curr:&:sandbox <- get *env, sandbox:offset
+  idx:num <- copy 0
+  {
+    break-unless curr
+    resources <- save-sandbox resources, curr, idx
+    idx <- add idx, 1
+    curr <- get *curr, next-sandbox:offset
+    loop
+  }
+]
+
+def save-sandbox resources:&:resources, sandbox:&:sandbox, sandbox-index:num -> resources:&:resources [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  filename:text <- append [lesson/], sandbox-index
+  resources <- dump resources, filename, data
+  <end-save-sandbox>
+]
+
+def! render-sandbox-side screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
+  local-scope
+  load-inputs
+  trace 11, [app], [render sandbox side]
+  old-top-idx:num <- save-top-idx screen
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  row:num, column:num <- copy 1, 0
+  left:num <- get *current-sandbox, left:offset
+  right:num <- get *current-sandbox, right:offset
+  # render sandbox editor
+  render-from:num <- get *env, render-from:offset
+  {
+    render-current-sandbox?:bool <- equal render-from, -1
+    break-unless render-current-sandbox?
+    row, column, screen, current-sandbox <- call render-editor, screen, current-sandbox
+  }
+  # render sandboxes
+  draw-horizontal screen, row, left, right
+  sandbox:&:sandbox <- get *env, sandbox:offset
+  row, screen <- render-sandboxes screen, sandbox, left, right, row, render-from
+  clear-rest-of-screen screen, row, left, right
+  #
+  assert-no-scroll screen, old-top-idx
+]
+
+def render-sandboxes screen:&:screen, sandbox:&:sandbox, left:num, right:num, row:num, render-from:num, idx:num -> row:num, screen:&:screen, sandbox:&:sandbox [
+  local-scope
+  load-inputs
+  return-unless sandbox
+  screen-height:num <- screen-height screen
+  hidden?:bool <- lesser-than idx, render-from
+  {
+    break-if hidden?
+    # render sandbox menu
+    row <- add row, 1
+    at-bottom?:bool <- greater-or-equal row, screen-height
+    return-if at-bottom?
+    screen <- move-cursor screen, row, left
+    screen <- render-sandbox-menu screen, idx, left, right
+    # save menu row so we can detect clicks to it later
+    *sandbox <- put *sandbox, starting-row-on-screen:offset, row
+    # render sandbox contents
+    row <- add row, 1
+    screen <- move-cursor screen, row, left
+    sandbox-data:text <- get *sandbox, data:offset
+    row, screen <- render-code screen, sandbox-data, left, right, row
+    *sandbox <- put *sandbox, code-ending-row-on-screen:offset, row
+    # render sandbox warnings, screen or response, in that order
+    sandbox-response:text <- get *sandbox, response:offset
+    <render-sandbox-results>
+    {
+      sandbox-screen:&:screen <- get *sandbox, screen:offset
+      empty-screen?:bool <- fake-screen-is-empty? sandbox-screen
+      break-if empty-screen?
+      row, screen <- render-screen screen, sandbox-screen, left, right, row
+    }
+    {
+      break-unless empty-screen?
+      <render-sandbox-response>
+      row, screen <- render-text screen, sandbox-response, left, right, 245/grey, row
+    }
+    +render-sandbox-end
+    at-bottom?:bool <- greater-or-equal row, screen-height
+    return-if at-bottom?
+    # draw solid line after sandbox
+    draw-horizontal screen, row, left, right
+  }
+  # if hidden, reset row attributes
+  {
+    break-unless hidden?
+    *sandbox <- put *sandbox, starting-row-on-screen:offset, 0
+    *sandbox <- put *sandbox, code-ending-row-on-screen:offset, 0
+    <end-render-sandbox-reset-hidden>
+  }
+  # draw next sandbox
+  next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
+  next-idx:num <- add idx, 1
+  row, screen <- render-sandboxes screen, next-sandbox, left, right, row, render-from, next-idx
+]
+
+def render-sandbox-menu screen:&:screen, sandbox-index:num, left:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  move-cursor-to-column screen, left
+  edit-button-left:num, edit-button-right:num, copy-button-left:num, copy-button-right:num, recipe-button-left:num, recipe-button-right:num, delete-button-left:num <- sandbox-menu-columns left, right
+  print screen, sandbox-index, 232/dark-grey, 245/grey
+  start-buttons:num <- subtract edit-button-left, 1
+  clear-line-until screen, start-buttons, 245/grey
+  print screen, [edit], 232/black, 25/background-blue
+  clear-line-until screen, edit-button-right, 25/background-blue
+  print screen, [copy], 232/black, 58/background-green
+  clear-line-until screen, copy-button-right, 58/background-green
+  print screen, [to recipe], 232/black, 94/background-orange
+  clear-line-until screen, recipe-button-right, 94/background-orange
+  print screen, [delete], 232/black, 52/background-red
+  clear-line-until screen, right, 52/background-red
+]
+
+scenario skip-rendering-sandbox-menu-past-bottom-row [
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 6/height
+  # recipe editor is empty
+  assume-resources [
+    [lesson/0] <- [|add 2, 2|]
+    [lesson/1] <- [|add 1, 1|]
+  ]
+  # create two sandboxes such that the top one just barely fills the screen
+  env:&:environment <- new-programming-environment resources, screen, []
+  env <- restore-sandboxes env, resources
+  run [
+    render-all screen, env, render
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊─────────────────────────────────────────────────.
+  ]
+]
+
+# divide up the menu bar for a sandbox into 3 segments, for edit/copy/delete buttons
+# delete-button-right == right
+# all left/right pairs are inclusive
+def sandbox-menu-columns left:num, right:num -> edit-button-left:num, edit-button-right:num, copy-button-left:num, copy-button-right:num, recipe-button-left:num, recipe-button-right:num, delete-button-left:num [
+  local-scope
+  load-inputs
+  start-buttons:num <- add left, 4/space-for-sandbox-index
+  buttons-space:num <- subtract right, start-buttons
+  button-width:num <- divide-with-remainder buttons-space, 4  # integer division
+  buttons-wide-enough?:bool <- greater-or-equal button-width, 10
+  assert buttons-wide-enough?, [sandbox must be at least 40 or so characters wide]
+  edit-button-left:num <- copy start-buttons
+  copy-button-left:num <- add start-buttons, button-width
+  edit-button-right:num <- subtract copy-button-left, 1
+  recipe-button-left:num <- add copy-button-left, button-width
+  copy-button-right:num <- subtract recipe-button-left, 1
+  delete-button-left:num <- subtract right, button-width, -2  # because 'to recipe' is wider than 'delete'
+  recipe-button-right:num <- subtract delete-button-left, 1
+]
+
+# print a text 's' to 'editor' in 'color' starting at 'row'
+# clear rest of last line, move cursor to next line
+# like 'render-code' but without syntax-based colorization
+def render-text screen:&:screen, s:text, left:num, right:num, color:num, row:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  return-unless s
+  column:num <- copy left
+  screen <- move-cursor screen, row, column
+  screen-height:num <- screen-height screen
+  i:num <- copy 0
+  len:num <- length *s
+  {
+    +next-character
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    done? <- greater-or-equal row, screen-height
+    break-if done?
+    c:char <- index *s, i
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # clear rest of line in this window
+      {
+        done?:bool <- greater-than column, right
+        break-if done?
+        space:char <- copy 32/space
+        print screen, space
+        column <- add column, 1
+        loop
+      }
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      i <- add i, 1
+      loop +next-character
+    }
+    {
+      # at right? wrap.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment i
+      loop +next-character
+    }
+    i <- add i, 1
+    print screen, c, color
+    column <- add column, 1
+    loop
+  }
+  was-at-left?:bool <- equal column, left
+  clear-line-until screen, right
+  {
+    break-if was-at-left?
+    row <- add row, 1
+  }
+  move-cursor screen, row, left
+]
+
+scenario render-text-wraps-barely-long-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  run [
+    render-text screen, [abcde], 0/left, 4/right, 7/white, 1/row
+  ]
+  screen-should-contain [
+    .     .
+    .abcd↩.
+    .e    .
+    .     .
+  ]
+]
+
+# assumes programming environment has no sandboxes; restores them from previous session
+def restore-sandboxes env:&:environment, resources:&:resources -> env:&:environment [
+  local-scope
+  load-inputs
+  # read all scenarios, pushing them to end of a list of scenarios
+  idx:num <- copy 0
+  curr:&:sandbox <- copy null
+  prev:&:sandbox <- copy null
+  {
+    filename:text <- append [lesson/], idx
+    contents:text <- slurp resources, filename
+    break-unless contents  # stop at first error; assuming file didn't exist
+                           # todo: handle empty sandbox
+    # create new sandbox for file
+    curr <- new sandbox:type
+    *curr <- put *curr, data:offset, contents
+    <end-restore-sandbox>
+    {
+      break-if idx
+      *env <- put *env, sandbox:offset, curr
+    }
+    {
+      break-unless idx
+      *prev <- put *prev, next-sandbox:offset, curr
+    }
+    idx <- add idx, 1
+    prev <- copy curr
+    loop
+  }
+  # update sandbox count
+  *env <- put *env, number-of-sandboxes:offset, idx
+]
+
+# print the fake sandbox screen to 'screen' with appropriate delimiters
+# leave cursor at start of next line
+def render-screen screen:&:screen, sandbox-screen:&:screen, left:num, right:num, row:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  return-unless sandbox-screen
+  # print 'screen:'
+  row <- render-text screen, [screen:], left, right, 245/grey, row
+  screen <- move-cursor screen, row, left
+  # start printing sandbox-screen
+  column:num <- copy left
+  s-width:num <- screen-width sandbox-screen
+  s-height:num <- screen-height sandbox-screen
+  buf:&:@:screen-cell <- get *sandbox-screen, data:offset
+  stop-printing:num <- add left, s-width, 3
+  max-column:num <- min stop-printing, right
+  i:num <- copy 0
+  len:num <- length *buf
+  screen-height:num <- screen-height screen
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    done? <- greater-or-equal row, screen-height
+    break-if done?
+    column <- copy left
+    screen <- move-cursor screen, row, column
+    # initial leader for each row: two spaces and a '.'
+    space:char <- copy 32/space
+    print screen, space, 245/grey
+    print screen, space, 245/grey
+    full-stop:char <- copy 46/period
+    print screen, full-stop, 245/grey
+    column <- add left, 3
+    {
+      # print row
+      row-done?:bool <- greater-or-equal column, max-column
+      break-if row-done?
+      curr:screen-cell <- index *buf, i
+      c:char <- get curr, contents:offset
+      color:num <- get curr, color:offset
+      {
+        # damp whites down to grey
+        white?:bool <- equal color, 7/white
+        break-unless white?
+        color <- copy 245/grey
+      }
+      print screen, c, color
+      column <- add column, 1
+      i <- add i, 1
+      loop
+    }
+    # print final '.'
+    print screen, full-stop, 245/grey
+    column <- add column, 1
+    {
+      # clear rest of current line
+      line-done?:bool <- greater-than column, right
+      break-if line-done?
+      print screen, space
+      column <- add column, 1
+      loop
+    }
+    row <- add row, 1
+    loop
+  }
+]
+
+scenario run-updates-results [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 12/height
+  # define a recipe (no indent for the 'add' line below so column numbers are more obvious)
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      ||
+      |recipe foo [|
+      |  local-scope|
+      |  z:num <- add 2, 2|
+      |  reply z|
+      |]|
+    ]
+  ]
+  # sandbox editor contains an instruction without storing outputs
+  env:&:environment <- new-programming-environment resources, screen, [foo]  # contents of sandbox editor
+  render-all screen, env, render
+  $clear-trace
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .recipe foo [                                      ┊─────────────────────────────────────────────────.
+    .  local-scope                                     ┊0   edit       copy       to recipe    delete    .
+    .  z:num <- add 2, 2                               ┊foo                                              .
+    .  reply z                                         ┊4                                                .
+    .]                                                 ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # the new sandbox should be saved to disk
+  trace-should-contain [
+    app: save sandboxes
+  ]
+  # no need to update editor
+  trace-should-not-contain [
+    app: render recipes
+  ]
+  # make a change (incrementing one of the args to 'add'), then rerun
+  $clear-trace
+  assume-console [
+    left-click 4, 28  # one past the value of the second arg
+    press backspace
+    type [3]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen updates the result on the right
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .recipe foo [                                      ┊─────────────────────────────────────────────────.
+    .  local-scope                                     ┊0   edit       copy       to recipe    delete    .
+    .  z:num <- add 2, 3                               ┊foo                                              .
+    .  reply z                                         ┊5                                                .
+    .]                                                 ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # no need to save sandboxes all over again
+  trace-should-not-contain [
+    app: save sandboxes
+  ]
+]
+
+scenario run-instruction-manages-screen-per-sandbox [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 20/height
+  # empty recipes
+  assume-resources [
+  ]
+  # sandbox editor contains an instruction
+  env:&:environment <- new-programming-environment resources, screen, [print screen, 4]  # contents of sandbox editor
+  render-all screen, env, render
+  # run the code in the editor
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that it prints a little toy screen
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊print screen, 4                                  .
+    .                                                  ┊screen:                                          .
+    .                                                  ┊  .4                             .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+def editor-contents editor:&:editor -> result:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 80
+  curr:&:duplex-list:char <- get *editor, data:offset
+  # skip § sentinel
+  assert curr, [editor without data is illegal; must have at least a sentinel]
+  curr <- next curr
+  return-unless curr, null
+  {
+    break-unless curr
+    c:char <- get *curr, value:offset
+    buf <- append buf, c
+    curr <- next curr
+    loop
+  }
+  result <- buffer-to-array buf
+]
+
+scenario editor-provides-edited-contents [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  assume-console [
+    left-click 1, 2
+    type [def]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    s:text <- editor-contents e
+    1:@:char/raw <- copy *s
+  ]
+  memory-should-contain [
+    1:array:character <- [abdefc]
+  ]
+]
+
+# keep the bottom of recipes from scrolling off the screen
+
+scenario scrolling-down-past-bottom-of-recipe-editor [
+  local-scope
+  trace-until 100/app
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    press enter
+    press down-arrow
+  ]
+  event-loop screen, console, env, resources
+  # no scroll
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario cursor-down-in-recipe-editor [
+  local-scope
+  trace-until 100/app
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    press enter
+    press up-arrow
+    press down-arrow  # while cursor isn't at bottom
+  ]
+  event-loop screen, console, env, resources
+  cursor:char <- copy 9251/␣
+  print screen, cursor
+  # cursor moves back to bottom
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .␣                                                 ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario scrolling-down-past-bottom-of-recipe-editor-2 [
+  local-scope
+  trace-until 100/app
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    # add a line
+    press enter
+    # cursor back to top line
+    press up-arrow
+    # try to scroll
+    press page-down  # or ctrl-f
+  ]
+  event-loop screen, console, env, resources
+  # no scroll, and cursor remains at top line
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario scrolling-down-past-bottom-of-recipe-editor-3 [
+  local-scope
+  trace-until 100/app
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [ab
+cd]
+  render-all screen, env, render
+  assume-console [
+    # add a line
+    press enter
+    # switch to sandbox
+    press ctrl-n
+    # move cursor
+    press down-arrow
+  ]
+  event-loop screen, console, env, resources
+  cursor:char <- copy 9251/␣
+  print screen, cursor
+  # no scroll on recipe side, cursor moves on sandbox side
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊ab                                               .
+    .                                                  ┊␣d                                               .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+# scrolling through sandboxes
+
+scenario scrolling-down-past-bottom-of-sandbox-editor [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
+  render-all screen, env, render
+  assume-console [
+    # create a sandbox
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+  ]
+  # switch to sandbox window and hit 'page-down'
+  assume-console [
+    press ctrl-n
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-up'
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor displays again, cursor is in editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊␣                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+  ]
+]
+
+# page-down on sandbox side updates render-from to scroll sandboxes
+after <global-keypress> [
+  {
+    break-unless sandbox-in-focus?
+    page-down?:bool <- equal k, 65518/page-down
+    break-unless page-down?
+    sandbox:&:sandbox <- get *env, sandbox:offset
+    break-unless sandbox
+    # slide down if possible
+    {
+      render-from:num <- get *env, render-from:offset
+      number-of-sandboxes:num <- get *env, number-of-sandboxes:offset
+      max:num <- subtract number-of-sandboxes, 1
+      at-end?:bool <- greater-or-equal render-from, max
+      loop-if at-end?, +next-event  # render nothing
+      render-from <- add render-from, 1
+      *env <- put *env, render-from:offset, render-from
+    }
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+# update-cursor takes render-from into account
+after <update-cursor-special-cases> [
+  {
+    break-unless sandbox-in-focus?
+    render-from:num <- get *env, render-from:offset
+    scrolling?:bool <- greater-or-equal render-from, 0
+    break-unless scrolling?
+    cursor-column:num <- get *current-sandbox, left:offset
+    screen <- move-cursor screen, 2/row, cursor-column  # highlighted sandbox will always start at row 2
+    return
+  }
+]
+
+# 'page-up' on sandbox side is like 'page-down': updates render-from when necessary
+after <global-keypress> [
+  {
+    break-unless sandbox-in-focus?
+    page-up?:bool <- equal k, 65519/page-up
+    break-unless page-up?
+    render-from:num <- get *env, render-from:offset
+    at-beginning?:bool <- equal render-from, -1
+    break-if at-beginning?
+    render-from <- subtract render-from, 1
+    *env <- put *env, render-from:offset, render-from
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+# sandbox belonging to 'env' whose next-sandbox is 'in'
+# return null if there's no such sandbox, either because 'in' doesn't exist in 'env', or because it's the first sandbox
+def previous-sandbox env:&:environment, in:&:sandbox -> out:&:sandbox [
+  local-scope
+  load-inputs
+  curr:&:sandbox <- get *env, sandbox:offset
+  return-unless curr, null
+  next:&:sandbox <- get *curr, next-sandbox:offset
+  {
+    return-unless next, null
+    found?:bool <- equal next, in
+    break-if found?
+    curr <- copy next
+    next <- get *curr, next-sandbox:offset
+    loop
+  }
+  return curr
+]
+
+scenario scrolling-through-multiple-sandboxes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  cursor:char <- copy 9251/␣
+  print screen, cursor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊␣                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-down'
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-down' again
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # just second sandbox displayed
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-down' again
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # no change
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-up'
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # back to displaying both sandboxes without editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # back to displaying both sandboxes as well as editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊␣                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # no change
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊␣                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+]
+
+scenario scrolling-manages-sandbox-index-correctly [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create a sandbox
+  assume-console [
+    press ctrl-n
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-down' and 'page-up' a couple of times. sandbox index should be stable
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # back to displaying both sandboxes as well as editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-down'
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
diff --git a/archive/2.vm/edit/006-sandbox-copy.mu b/archive/2.vm/edit/006-sandbox-copy.mu
new file mode 100644
index 00000000..6af72f77
--- /dev/null
+++ b/archive/2.vm/edit/006-sandbox-copy.mu
@@ -0,0 +1,395 @@
+## the 'copy' button makes it easy to duplicate a sandbox, and thence to
+## see code operate in multiple situations
+
+scenario copy-a-sandbox-to-editor [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # click at left edge of 'copy' button
+  assume-console [
+    left-click 3, 69
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # it copies into editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊add 1, 1                                         .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊0add 1, 1                                        .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario copy-a-sandbox-to-editor-2 [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # click at right edge of 'copy' button (just before 'delete')
+  assume-console [
+    left-click 3, 76
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # it copies into editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊add 1, 1                                         .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊0add 1, 1                                        .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+after <global-touch> [
+  # support 'copy' button
+  {
+    copy?:bool <- should-attempt-copy? click-row, click-column, env
+    break-unless copy?
+    copy?, env <- try-copy-sandbox click-row, env
+    break-unless copy?
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+# some preconditions for attempting to copy a sandbox
+def should-attempt-copy? click-row:num, click-column:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  # are we below the sandbox editor?
+  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, click-column, env
+  return-unless click-sandbox-area?, false
+  # narrower, is the click in the columns spanning the 'copy' button?
+  first-sandbox:&:editor <- get *env, current-sandbox:offset
+  assert first-sandbox, [!!]
+  sandbox-left-margin:num <- get *first-sandbox, left:offset
+  sandbox-right-margin:num <- get *first-sandbox, right:offset
+  _, _, copy-button-left:num, copy-button-right:num <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
+  copy-button-vertical-area?:bool <- within-range? click-column, copy-button-left, copy-button-right
+  return-unless copy-button-vertical-area?, false
+  # finally, is sandbox editor empty?
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  result <- empty-editor? current-sandbox
+]
+
+def try-copy-sandbox click-row:num, env:&:environment -> clicked-on-copy-button?:bool, env:&:environment [
+  local-scope
+  load-inputs
+  # identify the sandbox to copy, if the click was actually on the 'copy' button
+  sandbox:&:sandbox <- find-sandbox env, click-row
+  return-unless sandbox, false
+  clicked-on-copy-button? <- copy true
+  text:text <- get *sandbox, data:offset
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  current-sandbox <- insert-text current-sandbox, text
+  # reset scroll
+  *env <- put *env, render-from:offset, -1
+  # position cursor in sandbox editor
+  *env <- put *env, sandbox-in-focus?:offset, true
+]
+
+def find-sandbox env:&:environment, click-row:num -> result:&:sandbox [
+  local-scope
+  load-inputs
+  curr-sandbox:&:sandbox <- get *env, sandbox:offset
+  {
+    break-unless curr-sandbox
+    start:num <- get *curr-sandbox, starting-row-on-screen:offset
+    found?:bool <- equal click-row, start
+    return-if found?, curr-sandbox
+    curr-sandbox <- get *curr-sandbox, next-sandbox:offset
+    loop
+  }
+  return null/not-found
+]
+
+def click-on-sandbox-area? click-row:num, click-column:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  sandbox-left-margin:num <- get *current-sandbox, left:offset
+  on-sandbox-side?:bool <- greater-or-equal click-column, sandbox-left-margin
+  return-unless on-sandbox-side?, false
+  first-sandbox:&:sandbox <- get *env, sandbox:offset
+  return-unless first-sandbox, false
+  first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
+  result <- greater-or-equal click-row, first-sandbox-begins
+]
+
+def empty-editor? editor:&:editor -> result:bool [
+  local-scope
+  load-inputs
+  head:&:duplex-list:char <- get *editor, data:offset
+  first:&:duplex-list:char <- next head
+  result <- not first
+]
+
+def within-range? x:num, low:num, high:num -> result:bool [
+  local-scope
+  load-inputs
+  not-too-far-left?:bool <- greater-or-equal x, low
+  not-too-far-right?:bool <- lesser-or-equal x, high
+  result <- and not-too-far-left? not-too-far-right?
+]
+
+scenario copy-fails-if-sandbox-editor-not-empty [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # type something into the sandbox editor, then click on the 'copy' button
+  assume-console [
+    left-click 2, 70  # put cursor in sandbox editor
+    type [0]  # type something
+    left-click 3, 70  # click 'copy' button
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # copy doesn't happen
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊0                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊01                                               .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+## the 'to recipe' button makes it easy to create a function out of a sandbox
+
+scenario copy-a-sandbox-to-recipe-side [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # click at left edge of 'copy' button
+  assume-console [
+    left-click 3, 78
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # it copies into recipe side
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .add 1, 1                                          ┊                                                 .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # cursor should be at the top left of the recipe side
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .0add 1, 1                                         ┊                                                 .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+after <global-touch> [
+  # support 'copy to recipe' button
+  {
+    copy?:bool <- should-copy-to-recipe? click-row, click-column, env
+    break-unless copy?
+    modified?:bool <- prepend-sandbox-into-recipe-side click-row, env
+    break-unless modified?
+    *env <- put *env, sandbox-in-focus?:offset, false
+    screen <- render-recipes screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+# some preconditions for attempting to copy a sandbox into the recipe side
+def should-copy-to-recipe? click-row:num, click-column:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  # are we below the sandbox editor?
+  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, click-column, env
+  return-unless click-sandbox-area?, false
+  # narrower, is the click in the columns spanning the 'copy' button?
+  first-sandbox:&:editor <- get *env, current-sandbox:offset
+  assert first-sandbox, [!!]
+  sandbox-left-margin:num <- get *first-sandbox, left:offset
+  sandbox-right-margin:num <- get *first-sandbox, right:offset
+  _, _, _, _, recipe-button-left:num, recipe-button-right:num <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
+  result <- within-range? click-column, recipe-button-left, recipe-button-right
+]
+
+def prepend-sandbox-into-recipe-side click-row:num, env:&:environment -> clicked-on-copy-to-recipe-button?:bool, env:&:environment [
+  local-scope
+  load-inputs
+  sandbox:&:sandbox <- find-sandbox env, click-row
+  return-unless sandbox, false
+  recipe-editor:&:editor <- get *env, recipes:offset
+  recipe-data:&:duplex-list:char <- get *recipe-editor, data:offset
+  # make the newly inserted code easy to delineate
+  newline:char <- copy 10
+  insert newline, recipe-data
+  insert newline, recipe-data
+  # insert code from the selected sandbox
+  sandbox-data:text <- get *sandbox, data:offset
+  insert recipe-data, sandbox-data
+  # reset cursor
+  *recipe-editor <- put *recipe-editor, top-of-screen:offset, recipe-data
+  *recipe-editor <- put *recipe-editor, before-cursor:offset, recipe-data
+  *recipe-editor <- put *recipe-editor, cursor-row:offset, 1
+  *recipe-editor <- put *recipe-editor, cursor-column:offset, 0
+  return true
+]
diff --git a/archive/2.vm/edit/007-sandbox-delete.mu b/archive/2.vm/edit/007-sandbox-delete.mu
new file mode 100644
index 00000000..5e7989fb
--- /dev/null
+++ b/archive/2.vm/edit/007-sandbox-delete.mu
@@ -0,0 +1,342 @@
+## deleting sandboxes
+
+scenario deleting-sandboxes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # run a few commands
+  assume-console [
+    left-click 1, 75
+    type [divide-with-remainder 11, 3]
+    press F4
+    type [add 2, 2]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊divide-with-remainder 11, 3                      .
+    .                                                  ┊3                                                .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # delete second sandbox by clicking on left edge of 'delete' button
+  assume-console [
+    left-click 7, 90
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # delete first sandbox by clicking at right edge of 'delete' button
+  assume-console [
+    left-click 3, 99
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+after <global-touch> [
+  # support 'delete' button
+  {
+    delete?:bool <- should-attempt-delete? click-row, click-column, env
+    break-unless delete?
+    delete?, env <- try-delete-sandbox click-row, env
+    break-unless delete?
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+# some preconditions for attempting to delete a sandbox
+def should-attempt-delete? click-row:num, click-column:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  # are we below the sandbox editor?
+  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, click-column, env
+  return-unless click-sandbox-area?, false
+  # narrower, is the click in the columns spanning the 'copy' button?
+  first-sandbox:&:editor <- get *env, current-sandbox:offset
+  assert first-sandbox, [!!]
+  sandbox-left-margin:num <- get *first-sandbox, left:offset
+  sandbox-right-margin:num <- get *first-sandbox, right:offset
+  _, _, _, _, _, _, delete-button-left:num <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
+  result <- within-range? click-column, delete-button-left, sandbox-right-margin
+]
+
+def try-delete-sandbox click-row:num, env:&:environment -> clicked-on-delete-button?:bool, env:&:environment [
+  local-scope
+  load-inputs
+  # identify the sandbox to delete, if the click was actually on the 'delete' button
+  sandbox:&:sandbox <- find-sandbox env, click-row
+  return-unless sandbox, false
+  clicked-on-delete-button? <- copy true
+  env <- delete-sandbox env, sandbox
+]
+
+def delete-sandbox env:&:environment, sandbox:&:sandbox -> env:&:environment [
+  local-scope
+  load-inputs
+  curr-sandbox:&:sandbox <- get *env, sandbox:offset
+  first-sandbox?:bool <- equal curr-sandbox, sandbox
+  {
+    # first sandbox? pop
+    break-unless first-sandbox?
+    next-sandbox:&:sandbox <- get *curr-sandbox, next-sandbox:offset
+    *env <- put *env, sandbox:offset, next-sandbox
+  }
+  {
+    # not first sandbox?
+    break-if first-sandbox?
+    prev-sandbox:&:sandbox <- copy curr-sandbox
+    curr-sandbox <- get *curr-sandbox, next-sandbox:offset
+    {
+      assert curr-sandbox, [sandbox not found! something is wrong.]
+      found?:bool <- equal curr-sandbox, sandbox
+      break-if found?
+      prev-sandbox <- copy curr-sandbox
+      curr-sandbox <- get *curr-sandbox, next-sandbox:offset
+      loop
+    }
+    # snip sandbox out of its list
+    next-sandbox:&:sandbox <- get *curr-sandbox, next-sandbox:offset
+    *prev-sandbox <- put *prev-sandbox, next-sandbox:offset, next-sandbox
+  }
+  # update sandbox count
+  sandbox-count:num <- get *env, number-of-sandboxes:offset
+  sandbox-count <- subtract sandbox-count, 1
+  *env <- put *env, number-of-sandboxes:offset, sandbox-count
+  # reset scroll if deleted sandbox was last
+  {
+    break-if next-sandbox
+    render-from:num <- get *env, render-from:offset
+    reset-scroll?:bool <- equal render-from, sandbox-count
+    break-unless reset-scroll?
+    *env <- put *env, render-from:offset, -1
+  }
+]
+
+scenario deleting-sandbox-after-scroll [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press page-down
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 6, 99
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario deleting-top-sandbox-after-scroll [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press page-down
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 2, 99
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario deleting-final-sandbox-after-scroll [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press page-down
+    press page-down
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 2, 99
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # implicitly scroll up to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario deleting-updates-sandbox-count [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # delete the second sandbox, then try to scroll down twice
+  assume-console [
+    left-click 3, 99
+    press page-down
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # shouldn't go past last sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
diff --git a/archive/2.vm/edit/008-sandbox-edit.mu b/archive/2.vm/edit/008-sandbox-edit.mu
new file mode 100644
index 00000000..57cdbc0d
--- /dev/null
+++ b/archive/2.vm/edit/008-sandbox-edit.mu
@@ -0,0 +1,325 @@
+## editing sandboxes after they've been created
+
+scenario clicking-on-sandbox-edit-button-moves-it-to-editor [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # click at left edge of 'edit' button
+  assume-console [
+    left-click 3, 55
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # it pops back into editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊add 2, 2                                         .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊0add 2, 2                                        .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario clicking-on-sandbox-edit-button-moves-it-to-editor-2 [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # click at right edge of 'edit' button (just before 'copy')
+  assume-console [
+    left-click 3, 65
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # it pops back into editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊add 2, 2                                         .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊0add 2, 2                                        .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+after <global-touch> [
+  # support 'edit' button
+  {
+    edit?:bool <- should-attempt-edit? click-row, click-column, env
+    break-unless edit?
+    edit?, env <- try-edit-sandbox click-row, env
+    break-unless edit?
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+# some preconditions for attempting to edit a sandbox
+def should-attempt-edit? click-row:num, click-column:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  # are we below the sandbox editor?
+  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, click-column, env
+  return-unless click-sandbox-area?, false
+  # narrower, is the click in the columns spanning the 'edit' button?
+  first-sandbox:&:editor <- get *env, current-sandbox:offset
+  assert first-sandbox, [!!]
+  sandbox-left-margin:num <- get *first-sandbox, left:offset
+  sandbox-right-margin:num <- get *first-sandbox, right:offset
+  edit-button-left:num, edit-button-right:num, _ <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
+  edit-button-vertical-area?:bool <- within-range? click-column, edit-button-left, edit-button-right
+  return-unless edit-button-vertical-area?, false
+  # finally, is sandbox editor empty?
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  result <- empty-editor? current-sandbox
+]
+
+def try-edit-sandbox click-row:num, env:&:environment -> clicked-on-edit-button?:bool, env:&:environment [
+  local-scope
+  load-inputs
+  # identify the sandbox to edit, if the click was actually on the 'edit' button
+  sandbox:&:sandbox <- find-sandbox env, click-row
+  return-unless sandbox, false
+  clicked-on-edit-button? <- copy true
+  # 'edit' button = 'copy' button + 'delete' button
+  text:text <- get *sandbox, data:offset
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  current-sandbox <- insert-text current-sandbox, text
+  env <- delete-sandbox env, sandbox
+  # reset scroll
+  *env <- put *env, render-from:offset, -1
+  # position cursor in sandbox editor
+  *env <- put *env, sandbox-in-focus?:offset, true
+]
+
+scenario sandbox-with-print-can-be-edited [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 20/height
+  # left editor is empty
+  assume-resources [
+  ]
+  # right editor contains a print instruction
+  env:&:environment <- new-programming-environment resources, screen, [print screen, 4]
+  render-all screen, env, render
+  # run the sandbox
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊print screen, 4                                  .
+    .                                                  ┊screen:                                          .
+    .                                                  ┊  .4                             .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # edit the sandbox
+  assume-console [
+    left-click 3, 65
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊print screen, 4                                  .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario editing-sandbox-after-scrolling-resets-scroll [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press page-down
+    press page-down
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # edit the second sandbox
+  assume-console [
+    left-click 2, 55
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊add 2, 2                                         .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario editing-sandbox-updates-sandbox-count [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # edit the second sandbox, then resave
+  assume-console [
+    left-click 3, 60
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # no change in contents
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # now try to scroll past end
+  assume-console [
+    press page-down
+    press page-down
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # screen should show just final sandbox with the right index (1)
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
diff --git a/archive/2.vm/edit/009-sandbox-test.mu b/archive/2.vm/edit/009-sandbox-test.mu
new file mode 100644
index 00000000..52c1e909
--- /dev/null
+++ b/archive/2.vm/edit/009-sandbox-test.mu
@@ -0,0 +1,231 @@
+## clicking on sandbox results to 'fix' them and turn sandboxes into tests
+
+scenario sandbox-click-on-result-toggles-color-to-green [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 20/height
+  # basic recipe
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  reply 4|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  reply 4                                         ┊─────────────────────────────────────────────────.
+    .]                                                 ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊foo                                              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # click on the '4' in the result
+  $clear-trace
+  assume-console [
+    left-click 5, 51
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # color toggles to green
+  screen-should-contain-in-color 2/green, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   4                                                .
+    .                                                                                                    .
+    .                                                                                                    .
+  ]
+  # don't render entire sandbox side
+  check-trace-count-for-label-lesser-than 250, [print-character]  # say 5 sandbox lines
+  # cursor should remain unmoved
+  run [
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .␣ecipe foo [                                      ┊                                                 .
+    .  reply 4                                         ┊─────────────────────────────────────────────────.
+    .]                                                 ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊foo                                              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # now change the result
+  # then rerun
+  assume-console [
+    left-click 2, 11  # cursor to end of line
+    press backspace
+    type [3]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # result turns red
+  screen-should-contain-in-color 1/red, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   3                                                .
+    .                                                                                                    .
+    .                                                                                                    .
+  ]
+]
+
+# this requires tracking a couple more things
+container sandbox [
+  response-starting-row-on-screen:num
+  expected-response:text
+]
+
+# include expected response when saving or restoring a sandbox
+before <end-save-sandbox> [
+  {
+    expected-response:text <- get *sandbox, expected-response:offset
+    break-unless expected-response
+    filename <- append filename, [.out]
+    resources <- dump resources, filename, expected-response
+  }
+]
+
+before <end-restore-sandbox> [
+  {
+    filename <- append filename, [.out]
+    contents <- slurp resources, filename
+    break-unless contents
+    *curr <- put *curr, expected-response:offset, contents
+  }
+]
+
+# clicks on sandbox responses save it as 'expected'
+after <global-touch> [
+  # check if it's inside the output of any sandbox
+  {
+    sandbox-left-margin:num <- get *current-sandbox, left:offset
+    click-column:num <- get t, column:offset
+    on-sandbox-side?:bool <- greater-or-equal click-column, sandbox-left-margin
+    break-unless on-sandbox-side?
+    first-sandbox:&:sandbox <- get *env, sandbox:offset
+    break-unless first-sandbox
+    first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
+    click-row:num <- get t, row:offset
+    below-sandbox-editor?:bool <- greater-or-equal click-row, first-sandbox-begins
+    break-unless below-sandbox-editor?
+    # identify the sandbox whose output is being clicked on
+    sandbox:&:sandbox, sandbox-index:num <- find-click-in-sandbox-output env, click-row
+    break-unless sandbox
+    # update it
+    sandbox <- toggle-expected-response sandbox
+    # minimal update to disk
+    save-sandbox resources, sandbox, sandbox-index
+    # minimal update to screen
+    sandbox-right-margin:num <- get *current-sandbox, right:offset
+    row:num <- render-sandbox-response screen, sandbox, sandbox-left-margin, sandbox-right-margin
+    {
+      height:num <- screen-height screen
+      at-bottom?:bool <- greater-or-equal row, height
+      break-if at-bottom?
+      draw-horizontal screen, row, sandbox-left-margin, sandbox-right-margin
+    }
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+def find-click-in-sandbox-output env:&:environment, click-row:num -> sandbox:&:sandbox, sandbox-index:num [
+  local-scope
+  load-inputs
+  # assert click-row >= sandbox.starting-row-on-screen
+  sandbox:&:sandbox <- get *env, sandbox:offset
+  start:num <- get *sandbox, starting-row-on-screen:offset
+  clicked-on-sandboxes?:bool <- greater-or-equal click-row, start
+  assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor]
+  # while click-row < sandbox.next-sandbox.starting-row-on-screen
+  sandbox-index <- copy 0
+  {
+    next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
+    break-unless next-sandbox
+    next-start:num <- get *next-sandbox, starting-row-on-screen:offset
+    found?:bool <- lesser-than click-row, next-start
+    break-if found?
+    sandbox <- copy next-sandbox
+    sandbox-index <- add sandbox-index, 1
+    loop
+  }
+  # return sandbox if click is in its output region
+  response-starting-row:num <- get *sandbox, response-starting-row-on-screen:offset
+  return-unless response-starting-row, null/no-click-in-sandbox-output, 0/sandbox-index
+  click-in-response?:bool <- greater-or-equal click-row, response-starting-row
+  return-unless click-in-response?, null/no-click-in-sandbox-output, 0/sandbox-index
+  return sandbox, sandbox-index
+]
+
+def toggle-expected-response sandbox:&:sandbox -> sandbox:&:sandbox [
+  local-scope
+  load-inputs
+  expected-response:text <- get *sandbox, expected-response:offset
+  {
+    # if expected-response is set, reset
+    break-unless expected-response
+    *sandbox <- put *sandbox, expected-response:offset, null
+  }
+  {
+    # if not, set expected response to the current response
+    break-if expected-response
+    response:text <- get *sandbox, response:offset
+    *sandbox <- put *sandbox, expected-response:offset, response
+  }
+]
+
+# when rendering a sandbox, color it in red/green if expected response exists
+after <render-sandbox-response> [
+  {
+    break-unless sandbox-response
+    *sandbox <- put *sandbox, response-starting-row-on-screen:offset, row
+    row <- render-sandbox-response screen, sandbox, left, right
+    jump +render-sandbox-end
+  }
+]
+
+def render-sandbox-response screen:&:screen, sandbox:&:sandbox, left:num, right:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  sandbox-response:text <- get *sandbox, response:offset
+  expected-response:text <- get *sandbox, expected-response:offset
+  row:num <- get *sandbox response-starting-row-on-screen:offset
+  {
+    break-if expected-response
+    row <- render-text screen, sandbox-response, left, right, 245/grey, row
+    return
+  }
+  response-is-expected?:bool <- equal expected-response, sandbox-response
+  {
+    break-if response-is-expected?
+    row <- render-text screen, sandbox-response, left, right, 1/red, row
+  }
+  {
+    break-unless response-is-expected?:bool
+    row <- render-text screen, sandbox-response, left, right, 2/green, row
+  }
+]
+
+before <end-render-sandbox-reset-hidden> [
+  *sandbox <- put *sandbox, response-starting-row-on-screen:offset, 0
+]
diff --git a/archive/2.vm/edit/010-sandbox-trace.mu b/archive/2.vm/edit/010-sandbox-trace.mu
new file mode 100644
index 00000000..23b88833
--- /dev/null
+++ b/archive/2.vm/edit/010-sandbox-trace.mu
@@ -0,0 +1,253 @@
+## clicking on the code typed into a sandbox toggles its trace
+
+scenario sandbox-click-on-code-toggles-app-trace [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # basic recipe
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  stash [abc]|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  stash [abc]                                     ┊─────────────────────────────────────────────────.
+    .]                                                 ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊foo                                              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # click on the code in the sandbox
+  assume-console [
+    left-click 4, 51
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # trace now printed and cursor shouldn't have budged
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .␣ecipe foo [                                      ┊                                                 .
+    .  stash [abc]                                     ┊─────────────────────────────────────────────────.
+    .]                                                 ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊foo                                              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊abc                                              .
+  ]
+  screen-should-contain-in-color 245/grey, [
+    .                                                                                                    .
+    .                                                  ┊                                                 .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊abc                                              .
+  ]
+  # click again on the same region
+  assume-console [
+    left-click 4, 55
+  ]
+  run [
+    event-loop screen, console, env, resources
+    print screen, cursor
+  ]
+  # trace hidden again
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .␣ecipe foo [                                      ┊                                                 .
+    .  stash [abc]                                     ┊─────────────────────────────────────────────────.
+    .]                                                 ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊foo                                              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario sandbox-shows-app-trace-and-result [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # basic recipe
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  stash [abc]|
+      |  reply 4|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  stash [abc]                                     ┊─────────────────────────────────────────────────.
+    .  reply 4                                         ┊0   edit       copy       to recipe    delete    .
+    .]                                                 ┊foo                                              .
+    .                                                  ┊4                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # click on the code in the sandbox
+  assume-console [
+    left-click 4, 51
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # trace now printed above result
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  stash [abc]                                     ┊─────────────────────────────────────────────────.
+    .  reply 4                                         ┊0   edit       copy       to recipe    delete    .
+    .]                                                 ┊foo                                              .
+    .                                                  ┊abc                                              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊8 instructions run                               .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario clicking-on-app-trace-does-nothing [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [stash 123456789]
+  render-all screen, env, render
+  # create and expand the trace
+  assume-console [
+    press F4
+    left-click 4, 51
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊stash 123456789                                  .
+    .                                                  ┊123456789                                        .
+  ]
+  # click on the stash under the edit-button region (or any of the other buttons, really)
+  assume-console [
+    left-click 5, 57
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # no change; doesn't die
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊stash 123456789                                  .
+    .                                                  ┊123456789                                        .
+  ]
+]
+
+container sandbox [
+  trace:text
+  display-trace?:bool
+]
+
+# replaced in a later layer
+def! update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  response:text, _, fake-screen:&:screen, trace:text <- run-sandboxed data
+  *sandbox <- put *sandbox, response:offset, response
+  *sandbox <- put *sandbox, screen:offset, fake-screen
+  *sandbox <- put *sandbox, trace:offset, trace
+]
+
+# clicks on sandbox code toggle its display-trace? flag
+after <global-touch> [
+  # check if it's inside the code of any sandbox
+  {
+    sandbox-left-margin:num <- get *current-sandbox, left:offset
+    click-column:num <- get t, column:offset
+    on-sandbox-side?:bool <- greater-or-equal click-column, sandbox-left-margin
+    break-unless on-sandbox-side?
+    first-sandbox:&:sandbox <- get *env, sandbox:offset
+    break-unless first-sandbox
+    first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
+    click-row:num <- get t, row:offset
+    below-sandbox-editor?:bool <- greater-or-equal click-row, first-sandbox-begins
+    break-unless below-sandbox-editor?
+    # identify the sandbox whose code is being clicked on
+    sandbox:&:sandbox <- find-click-in-sandbox-code env, click-row
+    break-unless sandbox
+    # toggle its display-trace? property
+    x:bool <- get *sandbox, display-trace?:offset
+    x <- not x
+    *sandbox <- put *sandbox, display-trace?:offset, x
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+def find-click-in-sandbox-code env:&:environment, click-row:num -> sandbox:&:sandbox [
+  local-scope
+  load-inputs
+  # assert click-row >= sandbox.starting-row-on-screen
+  sandbox <- get *env, sandbox:offset
+  start:num <- get *sandbox, starting-row-on-screen:offset
+  clicked-on-sandboxes?:bool <- greater-or-equal click-row, start
+  assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor]
+  # while click-row < sandbox.next-sandbox.starting-row-on-screen
+  {
+    next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
+    break-unless next-sandbox
+    next-start:num <- get *next-sandbox, starting-row-on-screen:offset
+    found?:bool <- lesser-than click-row, next-start
+    break-if found?
+    sandbox <- copy next-sandbox
+    loop
+  }
+  # return sandbox if click is in its code region
+  code-ending-row:num <- get *sandbox, code-ending-row-on-screen:offset
+  click-above-response?:bool <- lesser-than click-row, code-ending-row
+  start:num <- get *sandbox, starting-row-on-screen:offset
+  click-below-menu?:bool <- greater-than click-row, start
+  click-on-sandbox-code?:bool <- and click-above-response?, click-below-menu?
+  {
+    break-if click-on-sandbox-code?
+    return null/no-click-in-sandbox-output
+  }
+  return sandbox
+]
+
+# when rendering a sandbox, dump its trace before response/warning if display-trace? property is set
+after <render-sandbox-results> [
+  {
+    display-trace?:bool <- get *sandbox, display-trace?:offset
+    break-unless display-trace?
+    sandbox-trace:text <- get *sandbox, trace:offset
+    break-unless sandbox-trace  # nothing to print; move on
+    row, screen <- render-text screen, sandbox-trace, left, right, 245/grey, row
+  }
+  <render-sandbox-trace-done>
+]
diff --git a/archive/2.vm/edit/011-errors.mu b/archive/2.vm/edit/011-errors.mu
new file mode 100644
index 00000000..47258815
--- /dev/null
+++ b/archive/2.vm/edit/011-errors.mu
@@ -0,0 +1,886 @@
+## handling malformed programs
+
+container environment [
+  recipe-errors:text
+]
+
+# copy code from recipe editor, persist to disk, load, save any errors
+def! update-recipes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, resources:&:resources, screen:&:screen [
+  local-scope
+  load-inputs
+  recipes:&:editor <- get *env, recipes:offset
+  in:text <- editor-contents recipes
+  resources <- dump resources, [lesson/recipes.mu], in
+  recipe-errors:text <- reload in
+  *env <- put *env, recipe-errors:offset, recipe-errors
+  # if recipe editor has errors, stop
+  {
+    break-unless recipe-errors
+    update-status screen, [errors found     ], 1/red
+    errors-found? <- copy true
+    return
+  }
+  errors-found? <- copy false
+]
+
+after <begin-run-sandboxes-on-F4> [
+  old-recipe-errors:text <- get *env, recipe-errors:offset
+]
+before <end-run-sandboxes-on-F4> [
+  # if there were recipe errors before, check if we can clear them
+  {
+    break-unless old-recipe-errors
+    screen <- render-recipes screen, env, render
+  }
+  render-recipe-errors env, screen
+]
+
+before <end-render-recipe-components> [
+  row <- render-recipe-errors env, screen
+]
+
+def render-recipe-errors env:&:environment, screen:&:screen -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  recipe-errors:text <- get *env, recipe-errors:offset
+  recipes:&:editor <- get *env, recipes:offset
+  row:num <- get *recipes, bottom:offset
+  row <- add row, 1
+  return-unless recipe-errors
+  left:num <- get *recipes, left:offset
+  right:num <- get *recipes, right:offset
+  row, screen <- render-text screen, recipe-errors, left, right, 1/red, row
+  # draw dotted line after recipes
+  draw-horizontal screen, row, left, right, 9480/horizontal-dotted
+  row <- add row, 1
+  clear-screen-from screen, row, left, left, right
+]
+
+container environment [
+  error-index:num  # index of first sandbox with an error (or -1 if none)
+]
+
+after <programming-environment-initialization> [
+  *result <- put *result, error-index:offset, -1
+]
+
+after <begin-run-sandboxes> [
+  *env <- put *env, error-index:offset, -1
+]
+
+before <end-run-sandboxes> [
+  {
+    error-index:num <- get *env, error-index:offset
+    sandboxes-completed-successfully?:bool <- equal error-index, -1
+    break-if sandboxes-completed-successfully?
+    errors-found? <- copy true
+  }
+]
+
+before <end-run-sandboxes-on-F4> [
+  {
+    break-unless error?
+    recipe-errors:text <- get *env, recipe-errors:offset
+    break-if recipe-errors
+    error-index:num <- get *env, error-index:offset
+    sandboxes-completed-successfully?:bool <- equal error-index, -1
+    break-if sandboxes-completed-successfully?
+    error-index-text:text <- to-text error-index
+    status:text <- interpolate [errors found (_)    ], error-index-text
+    update-status screen, status, 1/red
+  }
+]
+
+container sandbox [
+  errors:text
+]
+
+def! update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  response:text, errors:text, fake-screen:&:screen, trace:text, completed?:bool <- run-sandboxed data
+  *sandbox <- put *sandbox, response:offset, response
+  *sandbox <- put *sandbox, errors:offset, errors
+  *sandbox <- put *sandbox, screen:offset, fake-screen
+  *sandbox <- put *sandbox, trace:offset, trace
+  {
+    break-if errors
+    break-if completed?
+    errors <- new [took too long!
+]
+    *sandbox <- put *sandbox, errors:offset, errors
+  }
+  {
+    break-unless errors
+    error-index:num <- get *env, error-index:offset
+    error-not-set?:bool <- equal error-index, -1
+    break-unless error-not-set?
+    *env <- put *env, error-index:offset, idx
+  }
+]
+
+# make sure we render any trace
+after <render-sandbox-trace-done> [
+  {
+    sandbox-errors:text <- get *sandbox, errors:offset
+    break-unless sandbox-errors
+    *sandbox <- put *sandbox, response-starting-row-on-screen:offset, 0  # no response
+    row, screen <- render-text screen, sandbox-errors, left, right, 1/red, row
+    # don't try to print anything more for this sandbox
+    jump +render-sandbox-end
+  }
+]
+
+scenario run-shows-errors-in-get [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  get 123:num, foo:offset|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .recipe foo [                                      ┊foo                                              .
+    .  get 123:num, foo:offset                         ┊─────────────────────────────────────────────────.
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo [                                      ┊foo                                              .
+    .  get 123:num, foo:offset                         ┊─────────────────────────────────────────────────.
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+    .foo: unknown element 'foo' in container 'number'  ┊                                                 .
+    .foo: first ingredient of 'get' should be a contai↩┊                                                 .
+    .ner, but got '123:num'                            ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  screen-should-contain-in-color 1/red, [
+    .  errors found                                                                                      .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .foo: unknown element 'foo' in container 'number'                                                    .
+    .foo: first ingredient of 'get' should be a contai                                                   .
+    .ner, but got '123:num'                                                                              .
+    .                                                                                                    .
+  ]
+]
+
+scenario run-shows-errors-without-final-newline-in-recipe-side [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen
+  render-all screen, env, render
+  assume-console [
+    type [recipe foo x [
+]]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo x [                                    ┊                                                 .
+    .]                                                 ┊─────────────────────────────────────────────────.
+    .foo: ingredient 'x' has no type                   ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-updates-status-with-first-erroneous-sandbox [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    left-click 3, 80
+    # create invalid sandbox 1
+    type [get foo, x:offset]
+    press F4
+    # create invalid sandbox 0
+    type [get foo, x:offset]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # status line shows that error is in first sandbox
+  screen-should-contain [
+    .  errors found (0)                                                               run (F4)           .
+  ]
+]
+
+scenario run-updates-status-with-first-erroneous-sandbox-2 [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    left-click 3, 80
+    # create invalid sandbox 2
+    type [get foo, x:offset]
+    press F4
+    # create invalid sandbox 1
+    type [get foo, x:offset]
+    press F4
+    # create valid sandbox 0
+    type [add 2, 2]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # status line shows that error is in second sandbox
+  screen-should-contain [
+    .  errors found (1)                                                               run (F4)           .
+  ]
+]
+
+scenario run-hides-errors-from-past-sandboxes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [get foo, x:offset]  # invalid
+  render-all screen, env, render
+  assume-console [
+    press F4  # generate error
+  ]
+  event-loop screen, console, env, resources
+  assume-console [
+    left-click 3, 58
+    press ctrl-k
+    type [add 2, 2]  # valid code
+    press F4  # update sandbox
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # error should disappear
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-updates-errors-for-shape-shifting-recipes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  # define a shape-shifting recipe with an error
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo x:_elem -> z:_elem [|
+      |  local-scope|
+      |  load-ingredients|
+      |  y:&:num <- copy null|
+      |  z <- add x, y|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo 2]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .  errors found (0)                                                               run (F4)           .
+    .recipe foo x:_elem -> z:_elem [                   ┊                                                 .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  load-ingredients                                ┊0   edit       copy       to recipe    delete    .
+    .  y:&:num <- copy null                            ┊foo 2                                            .
+    .  z <- add x, y                                   ┊foo_2: 'add' requires number ingredients, but go↩.
+    .]                                                 ┊t 'y'                                            .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # now rerun everything
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # error should remain unchanged
+  screen-should-contain [
+    .  errors found (0)                                                               run (F4)           .
+    .recipe foo x:_elem -> z:_elem [                   ┊                                                 .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  load-ingredients                                ┊0   edit       copy       to recipe    delete    .
+    .  y:&:num <- copy null                            ┊foo 2                                            .
+    .  z <- add x, y                                   ┊foo_3: 'add' requires number ingredients, but go↩.
+    .]                                                 ┊t 'y'                                            .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-avoids-spurious-errors-on-reloading-shape-shifting-recipes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  # overload a well-known shape-shifting recipe
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe length l:&:list:_elem -> n:num [|
+      |]|
+    ]
+  ]
+  # call code that uses other variants of it, but not it itself
+  test-sandbox:text <- new [x:&:list:num <- copy null
+to-text x]
+  env:&:environment <- new-programming-environment resources, screen, test-sandbox
+  render-all screen, env, render
+  # run it once
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  # no errors anywhere on screen (can't check anything else, since to-text will return an address)
+  screen-should-contain-in-color 1/red, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                <-                                  .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+  ]
+  # rerun everything
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # still no errors
+  screen-should-contain-in-color 1/red, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                <-                                  .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+  ]
+]
+
+scenario run-shows-missing-type-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  x <- copy 0|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo [                                      ┊foo                                              .
+    .  x <- copy 0                                     ┊─────────────────────────────────────────────────.
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+    .foo: missing type for 'x' in 'x <- copy 0'        ┊                                                 .
+    .foo: can't copy '0' to 'x'; types don't match     ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-shows-unbalanced-bracket-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  # recipe is incomplete (unbalanced '[')
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo \\\[|
+      |  x <- copy 0|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo \\[                                      ┊foo                                              .
+    .  x <- copy 0                                     ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .9: unbalanced '\\[' for recipe                      ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-shows-get-on-non-container-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  x:&:point <- new point:type|
+      |  get x:&:point, 1:offset|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo [                                      ┊foo                                              .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  x:&:point <- new point:type                     ┊                                                 .
+    .  get x:&:point, 1:offset                         ┊                                                 .
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+    .foo: first ingredient of 'get' should be a contai↩┊                                                 .
+    .ner, but got 'x:&:point'                          ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-shows-non-literal-get-argument-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  x:num <- copy 0|
+      |  y:&:point <- new point:type|
+      |  get *y:&:point, x:num|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo [                                      ┊foo                                              .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  x:num <- copy 0                                 ┊                                                 .
+    .  y:&:point <- new point:type                     ┊                                                 .
+    .  get *y:&:point, x:num                           ┊                                                 .
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+    .foo: second ingredient of 'get' should have type ↩┊                                                 .
+    .'offset', but got 'x:num'                         ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-shows-errors-every-time [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  # try to run a file with an error
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  x:num <- copy y:num|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo [                                      ┊foo                                              .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  x:num <- copy y:num                             ┊                                                 .
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+    .foo: tried to read ingredient 'y' in 'x:num <- co↩┊                                                 .
+    .py y:num' but it hasn't been written to yet       ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # rerun the file, check for the same error
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo [                                      ┊foo                                              .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  x:num <- copy y:num                             ┊                                                 .
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+    .foo: tried to read ingredient 'y' in 'x:num <- co↩┊                                                 .
+    .py y:num' but it hasn't been written to yet       ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-hides-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  # try to run a file with an error
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  x:num <- copy y:num|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .recipe foo [                                      ┊foo                                              .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  x:num <- copy y:num                             ┊                                                 .
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+    .foo: tried to read ingredient 'y' in 'x:num <- co↩┊                                                 .
+    .py y:num' but it hasn't been written to yet       ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # fix the error, hit F4
+  assume-console [
+    left-click 3, 16
+    press ctrl-k
+    type [0]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  # no error anymore
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  x:num <- copy 0                                 ┊0   edit       copy       to recipe    delete    .
+    .]                                                 ┊foo                                              .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario scrolling-recipe-side-reveals-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 5/height
+  # recipe overflows recipe side
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  a:num <- copy 0|  # padding to overflow recipe side
+      |  b:num <- copy 0|  # padding to overflow recipe side
+      |  get 123:num, foo:offset|  # line containing error
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  # hit F4, generating errors, then scroll down
+  assume-console [
+    press F4
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # errors should be displayed
+  screen-should-contain [
+    .  errors found                                                                   run (F4)           .
+    .  get 123:num, foo:offset                         ┊foo                                              .
+    .\\]                                                 ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .foo: unknown element 'foo' in container 'number'  ┊                                                 .
+  ]
+]
+
+scenario run-instruction-and-print-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  # sandbox editor contains an illegal instruction
+  env:&:environment <- new-programming-environment resources, screen, [get 1234:num, foo:offset]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints error message in red
+  screen-should-contain [
+    .  errors found (0)                                                               run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊get 1234:num, foo:offset                         .
+    .                                                  ┊unknown element 'foo' in container 'number'      .
+    .                                                  ┊first ingredient of 'get' should be a container,↩.
+    .                                                  ┊ but got '1234:num'                              .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  screen-should-contain-in-color 7/white, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   get 1234:num, foo:offset                         .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+  ]
+  screen-should-contain-in-color 1/red, [
+    .  errors found (0)                                                                                  .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   unknown element 'foo' in container 'number'      .
+    .                                                   first ingredient of 'get' should be a container, .
+    .                                                    but got '1234:num'                              .
+    .                                                                                                    .
+  ]
+  screen-should-contain-in-color 245/grey, [
+    .                                                                                                    .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                ↩.
+    .                                                  ┊                                                 .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-instruction-and-print-errors-only-once [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  # sandbox editor contains an illegal instruction
+  env:&:environment <- new-programming-environment resources, screen, [get 1234:num, foo:offset]
+  render-all screen, env, render
+  # run the code in the editors multiple times
+  assume-console [
+    press F4
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints error message just once
+  screen-should-contain [
+    .  errors found (0)                                                               run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊get 1234:num, foo:offset                         .
+    .                                                  ┊unknown element 'foo' in container 'number'      .
+    .                                                  ┊first ingredient of 'get' should be a container,↩.
+    .                                                  ┊ but got '1234:num'                              .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario sandbox-can-handle-infinite-loop [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 20/height
+  # sandbox editor will trigger an infinite loop
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  {|
+      |    loop|
+      |  }|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  # run the sandbox
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found (0)                                                               run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  {                                               ┊─────────────────────────────────────────────────.
+    .    loop                                          ┊0   edit       copy       to recipe    delete    .
+    .  }                                               ┊foo                                              .
+    .]                                                 ┊took too long!                                   .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario sandbox-with-errors-shows-trace [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # generate a stash and a error
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  a:num <- next-ingredient|
+      |  b:num <- next-ingredient|
+      |  stash [dividing by], b|
+      |  _, c:num <- divide-with-remainder a, b|
+      |  reply b|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo 4, 0]
+  render-all screen, env, render
+  # run
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  # screen prints error message
+  screen-should-contain [
+    .  errors found (0)                                                               run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  a:num <- next-ingredient                        ┊0   edit       copy       to recipe    delete    .
+    .  b:num <- next-ingredient                        ┊foo 4, 0                                         .
+    .  stash [dividing by], b                          ┊foo: divide by zero in '_, c:num <- divide-with-↩.
+    .  _, c:num <- divide-with-remainder a, b          ┊remainder a, b'                                  .
+    .  reply b                                         ┊─────────────────────────────────────────────────.
+    .]                                                 ┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # click on the call in the sandbox
+  assume-console [
+    left-click 4, 55
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # screen should expand trace
+  screen-should-contain [
+    .  errors found (0)                                                               run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  local-scope                                     ┊─────────────────────────────────────────────────.
+    .  a:num <- next-ingredient                        ┊0   edit       copy       to recipe    delete    .
+    .  b:num <- next-ingredient                        ┊foo 4, 0                                         .
+    .  stash [dividing by], b                          ┊dividing by 0                                    .
+    .  _, c:num <- divide-with-remainder a, b          ┊14 instructions run                              .
+    .  reply b                                         ┊foo: divide by zero in '_, c:num <- divide-with-↩.
+    .]                                                 ┊remainder a, b'                                  .
+    .                                                  ┊─────────────────────────────────────────────────.
+  ]
+]
diff --git a/archive/2.vm/edit/012-editor-undo.mu b/archive/2.vm/edit/012-editor-undo.mu
new file mode 100644
index 00000000..871f6c74
--- /dev/null
+++ b/archive/2.vm/edit/012-editor-undo.mu
@@ -0,0 +1,2111 @@
+## undo/redo
+
+# for every undoable event, create a type of *operation* that contains all the
+# information needed to reverse it
+exclusive-container operation [
+  typing:insert-operation
+  move:move-operation
+  delete:delete-operation
+]
+
+container insert-operation [
+  before-row:num
+  before-column:num
+  before-top-of-screen:&:duplex-list:char
+  after-row:num
+  after-column:num
+  after-top-of-screen:&:duplex-list:char
+  # inserted text is from 'insert-from' until 'insert-until'; list doesn't have to terminate
+  insert-from:&:duplex-list:char
+  insert-until:&:duplex-list:char
+  tag:num  # event causing this operation; might be used to coalesce runs of similar events
+    # 0: no coalesce (enter+indent)
+    # 1: regular alphanumeric characters
+]
+
+container move-operation [
+  before-row:num
+  before-column:num
+  before-top-of-screen:&:duplex-list:char
+  after-row:num
+  after-column:num
+  after-top-of-screen:&:duplex-list:char
+  tag:num  # event causing this operation; might be used to coalesce runs of similar events
+    # 0: no coalesce (touch events, etc)
+    # 1: left arrow
+    # 2: right arrow
+    # 3: up arrow
+    # 4: down arrow
+    # 5: line up
+    # 6: line down
+]
+
+container delete-operation [
+  before-row:num
+  before-column:num
+  before-top-of-screen:&:duplex-list:char
+  after-row:num
+  after-column:num
+  after-top-of-screen:&:duplex-list:char
+  deleted-text:&:duplex-list:char
+  delete-from:&:duplex-list:char
+  delete-until:&:duplex-list:char
+  tag:num  # event causing this operation; might be used to coalesce runs of similar events
+    # 0: no coalesce (ctrl-k, ctrl-u)
+    # 1: backspace
+    # 2: delete
+]
+
+# every editor accumulates a list of operations to undo/redo
+container editor [
+  undo:&:list:&:operation
+  redo:&:list:&:operation
+]
+
+# ctrl-z - undo operation
+after <handle-special-character> [
+  {
+    undo?:bool <- equal c, 26/ctrl-z
+    break-unless undo?
+    undo:&:list:&:operation <- get *editor, undo:offset
+    break-unless undo
+    op:&:operation <- first undo
+    undo <- rest undo
+    *editor <- put *editor, undo:offset, undo
+    redo:&:list:&:operation <- get *editor, redo:offset
+    redo <- push op, redo
+    *editor <- put *editor, redo:offset, redo
+    <handle-undo>
+    return true/go-render
+  }
+]
+
+# ctrl-y - redo operation
+after <handle-special-character> [
+  {
+    redo?:bool <- equal c, 25/ctrl-y
+    break-unless redo?
+    redo:&:list:&:operation <- get *editor, redo:offset
+    break-unless redo
+    op:&:operation <- first redo
+    redo <- rest redo
+    *editor <- put *editor, redo:offset, redo
+    undo:&:list:&:operation <- get *editor, undo:offset
+    undo <- push op, undo
+    *editor <- put *editor, undo:offset, undo
+    <handle-redo>
+    return true/go-render
+  }
+]
+
+# undo typing
+
+scenario editor-can-undo-typing [
+  local-scope
+  # create an editor and type a character
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [0]
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # character should be gone
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .1         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+# save operation to undo
+after <begin-insert-character> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset
+]
+before <end-insert-character> [
+  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  undo:&:list:&:operation <- get *editor, undo:offset
+  {
+    # if previous operation was an insert, coalesce this operation with it
+    break-unless undo
+    op:&:operation <- first undo
+    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
+    break-unless is-insert?
+    previous-coalesce-tag:num <- get typing, tag:offset
+    break-unless previous-coalesce-tag
+    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    insert-until:&:duplex-list:char <- next before-cursor
+    typing <- put typing, insert-until:offset, insert-until
+    typing <- put typing, after-row:offset, cursor-row
+    typing <- put typing, after-column:offset, cursor-column
+    typing <- put typing, after-top-of-screen:offset, top-after
+    *op <- merge 0/insert-operation, typing
+    break +done-adding-insert-operation
+  }
+  # if not, create a new operation
+  insert-from:&:duplex-list:char <- next cursor-before
+  insert-to:&:duplex-list:char <- next insert-from
+  op:&:operation <- new operation:type
+  *op <- merge 0/insert-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, insert-from, insert-to, 1/coalesce
+  editor <- add-operation editor, op
+  +done-adding-insert-operation
+]
+
+# enter operations never coalesce with typing before or after
+after <begin-insert-enter> [
+  cursor-row-before:num <- copy cursor-row
+  cursor-column-before:num <- copy cursor-column
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset
+]
+before <end-insert-enter> [
+  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-row:offset
+  # never coalesce
+  insert-from:&:duplex-list:char <- next cursor-before
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  insert-to:&:duplex-list:char <- next before-cursor
+  op:&:operation <- new operation:type
+  *op <- merge 0/insert-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, insert-from, insert-to, 0/never-coalesce
+  editor <- add-operation editor, op
+]
+
+# Everytime you add a new operation to the undo stack, be sure to clear the
+# redo stack, because it's now obsolete.
+# Beware: since we're counting cursor moves as operations, this means just
+# moving the cursor can lose work on the undo stack.
+def add-operation editor:&:editor, op:&:operation -> editor:&:editor [
+  local-scope
+  load-inputs
+  undo:&:list:&:operation <- get *editor, undo:offset
+  undo <- push op undo
+  *editor <- put *editor, undo:offset, undo
+  redo:&:list:&:operation <- get *editor, redo:offset
+  redo <- copy null
+  *editor <- put *editor, redo:offset, redo
+]
+
+after <handle-undo> [
+  {
+    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
+    break-unless is-insert?
+    start:&:duplex-list:char <- get typing, insert-from:offset
+    end:&:duplex-list:char <- get typing, insert-until:offset
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    before-cursor:&:duplex-list:char <- prev start
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    remove-between before-cursor, end
+    cursor-row <- get typing, before-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get typing, before-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get typing, before-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+scenario editor-can-undo-typing-multiple [
+  local-scope
+  # create an editor and type multiple characters
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [012]
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # all characters must be gone
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-can-undo-typing-multiple-2 [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [a], 0/left, 10/right
+  editor-render screen, e
+  # type some characters
+  assume-console [
+    type [012]
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .012a      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # back to original text
+  screen-should-contain [
+    .          .
+    .a         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [3]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .3a        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-can-undo-typing-enter [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [  abc], 0/left, 10/right
+  editor-render screen, e
+  # new line
+  assume-console [
+    left-click 1, 8
+    press enter
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .  abc     .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # line is indented
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 2
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 5
+  ]
+  # back to original text
+  screen-should-contain [
+    .          .
+    .  abc     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be at end of line
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .  abc1    .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+# redo typing
+
+scenario editor-redo-typing [
+  local-scope
+  # create an editor, type something, undo
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [a], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [012]
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # all characters must be back
+  screen-should-contain [
+    .          .
+    .012a      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [3]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0123a     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <handle-redo> [
+  {
+    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
+    break-unless is-insert?
+    before-cursor <- get *editor, before-cursor:offset
+    insert-from:&:duplex-list:char <- get typing, insert-from:offset  # ignore insert-to because it's already been spliced away
+    # assert insert-to matches next(before-cursor)
+    splice before-cursor, insert-from
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    cursor-row <- get typing, after-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get typing, after-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get typing, after-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+scenario editor-redo-typing-empty [
+  local-scope
+  # create an editor, type something, undo
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [012]
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # all characters must be back
+  screen-should-contain [
+    .          .
+    .012       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [3]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0123      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-work-clears-redo-stack [
+  local-scope
+  # create an editor with some text, do some work, undo
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [1]
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  # do some more work
+  assume-console [
+    type [0]
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .0abc      .
+    .def       .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # nothing should happen
+  screen-should-contain [
+    .          .
+    .0abc      .
+    .def       .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-redo-typing-and-enter-and-tab [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  # insert some text and tabs, hit enter, some more text and tabs
+  assume-console [
+    press tab
+    type [ab]
+    press tab
+    type [cd]
+    press enter
+    press tab
+    type [efg]
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .    efg   .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 7
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # typing in second line deleted, but not indent
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # indent and newline deleted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 8
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # empty screen
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 8
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo again
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # newline and indent inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo again
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # indent and newline deleted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 7
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .    efg   .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+# undo cursor movement and scroll
+
+scenario editor-can-undo-touch [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 3, 1
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # click undone
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .1abc      .
+    .def       .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+after <begin-move-cursor> [
+  cursor-row-before:num <- get *editor, cursor-row:offset
+  cursor-column-before:num <- get *editor, cursor-column:offset
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-move-cursor> [
+  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  {
+    break-unless undo-coalesce-tag
+    # if previous operation was also a move, and also had the same coalesce
+    # tag, coalesce with it
+    undo:&:list:&:operation <- get *editor, undo:offset
+    break-unless undo
+    op:&:operation <- first undo
+    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
+    break-unless is-move?
+    previous-coalesce-tag:num <- get move, tag:offset
+    coalesce?:bool <- equal undo-coalesce-tag, previous-coalesce-tag
+    break-unless coalesce?
+    move <- put move, after-row:offset, cursor-row
+    move <- put move, after-column:offset, cursor-column
+    move <- put move, after-top-of-screen:offset, top-after
+    *op <- merge 1/move-operation, move
+    break +done-adding-move-operation
+  }
+  op:&:operation <- new operation:type
+  *op <- merge 1/move-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, undo-coalesce-tag
+  editor <- add-operation editor, op
+  +done-adding-move-operation
+]
+
+after <handle-undo> [
+  {
+    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
+    break-unless is-move?
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    cursor-row <- get move, before-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get move, before-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get move, before-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+scenario editor-can-undo-scroll [
+  local-scope
+  # screen has 1 line for menu + 3 lines
+  assume-screen 5/width, 4/height
+  # editor contains a wrapped line
+  contents:text <- new [a
+b
+cdefgh]
+  e:&:editor <- new-editor contents, 0/left, 5/right
+  # position cursor at end of screen and try to move right
+  assume-console [
+    left-click 3, 3
+    press right-arrow
+  ]
+  editor-event-loop screen, console, e
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  # screen scrolls
+  screen-should-contain [
+    .     .
+    .b    .
+    .cdef↩.
+    .gh   .
+  ]
+  memory-should-contain [
+    3 <- 3
+    4 <- 0
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moved back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 3
+    4 <- 3
+  ]
+  # scroll undone
+  screen-should-contain [
+    .     .
+    .a    .
+    .b    .
+    .cdef↩.
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .     .
+    .b    .
+    .cde1↩.
+    .fgh  .
+  ]
+]
+
+scenario editor-can-undo-left-arrow [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 3, 1
+    press left-arrow
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 3
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .g1hi      .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-up-arrow [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 3, 1
+    press up-arrow
+  ]
+  editor-event-loop screen, console, e
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 3
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .g1hi      .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-down-arrow [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 2, 1
+    press down-arrow
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-ctrl-f [
+  local-scope
+  # create an editor with multiple pages of text
+  assume-screen 10/width, 5/height
+  contents:text <- new [a
+b
+c
+d
+e
+f]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # scroll the page
+  assume-console [
+    press ctrl-f
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen should again show page 1
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+    .d         .
+  ]
+]
+
+scenario editor-can-undo-page-down [
+  local-scope
+  # create an editor with multiple pages of text
+  assume-screen 10/width, 5/height
+  contents:text <- new [a
+b
+c
+d
+e
+f]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # scroll the page
+  assume-console [
+    press page-down
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen should again show page 1
+  screen-should-contain [
+    .          .
+    .a         .
+    .b         .
+    .c         .
+    .d         .
+  ]
+]
+
+scenario editor-can-undo-ctrl-b [
+  local-scope
+  # create an editor with multiple pages of text
+  assume-screen 10/width, 5/height
+  contents:text <- new [a
+b
+c
+d
+e
+f]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # scroll the page down and up
+  assume-console [
+    press page-down
+    press ctrl-b
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen should again show page 2
+  screen-should-contain [
+    .          .
+    .d         .
+    .e         .
+    .f         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-page-up [
+  local-scope
+  # create an editor with multiple pages of text
+  assume-screen 10/width, 5/height
+  contents:text <- new [a
+b
+c
+d
+e
+f]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # scroll the page down and up
+  assume-console [
+    press page-down
+    press page-up
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen should again show page 2
+  screen-should-contain [
+    .          .
+    .d         .
+    .e         .
+    .f         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-ctrl-a [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor, then to start of line
+  assume-console [
+    left-click 2, 1
+    press ctrl-a
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-home [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor, then to start of line
+  assume-console [
+    left-click 2, 1
+    press home
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-ctrl-e [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor, then to start of line
+  assume-console [
+    left-click 2, 1
+    press ctrl-e
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-end [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor, then to start of line
+  assume-console [
+    left-click 2, 1
+    press end
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-multiple-arrows-in-the-same-direction [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 2, 1
+    press right-arrow
+    press right-arrow
+    press up-arrow
+  ]
+  editor-event-loop screen, console, e
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # up-arrow is undone
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 3
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # both right-arrows are undone
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+]
+
+# redo cursor movement and scroll
+
+scenario editor-redo-touch [
+  local-scope
+  # create an editor with some text, click on a character, undo
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    left-click 3, 1
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves to left-click
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 3
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .g1hi      .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+after <handle-redo> [
+  {
+    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
+    break-unless is-move?
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    cursor-row <- get move, after-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get move, after-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get move, after-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+scenario editor-separates-undo-insert-from-undo-cursor-move [
+  local-scope
+  # create an editor, type some text, move the cursor, type some more text
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [abc]
+    left-click 1, 1
+    type [d]
+  ]
+  editor-event-loop screen, console, e
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  screen-should-contain [
+    .          .
+    .adbc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # last letter typed is deleted
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # no change to screen; cursor moves
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # screen empty
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # first insert
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  # redo again
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor moves
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # redo again
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # second insert
+  screen-should-contain [
+    .          .
+    .adbc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+]
+
+# undo backspace
+
+scenario editor-can-undo-and-redo-backspace [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit backspace
+  assume-console [
+    type [abc]
+    press backspace
+    press backspace
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  screen-should-contain [
+    .          .
+    .a         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+# save operation to undo
+after <begin-backspace-character> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-backspace-character> [
+  {
+    break-unless backspaced-cell  # backspace failed; don't add an undo operation
+    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-row:offset
+    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    undo:&:list:&:operation <- get *editor, undo:offset
+    {
+      # if previous operation was an insert, coalesce this operation with it
+      break-unless undo
+      op:&:operation <- first undo
+      deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
+      break-unless is-delete?
+      previous-coalesce-tag:num <- get deletion, tag:offset
+      coalesce?:bool <- equal previous-coalesce-tag, 1/coalesce-backspace
+      break-unless coalesce?
+      deletion <- put deletion, delete-from:offset, before-cursor
+      backspaced-so-far:&:duplex-list:char <- get deletion, deleted-text:offset
+      splice backspaced-cell, backspaced-so-far
+      deletion <- put deletion, deleted-text:offset, backspaced-cell
+      deletion <- put deletion, after-row:offset, cursor-row
+      deletion <- put deletion, after-column:offset, cursor-column
+      deletion <- put deletion, after-top-of-screen:offset, top-after
+      *op <- merge 2/delete-operation, deletion
+      break +done-adding-backspace-operation
+    }
+    # if not, create a new operation
+    op:&:operation <- new operation:type
+    deleted-until:&:duplex-list:char <- next before-cursor
+    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, backspaced-cell/deleted, before-cursor/delete-from, deleted-until, 1/coalesce-backspace
+    editor <- add-operation editor, op
+    +done-adding-backspace-operation
+  }
+]
+
+after <handle-undo> [
+  {
+    deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
+    break-unless is-delete?
+    anchor:&:duplex-list:char <- get deletion, delete-from:offset
+    break-unless anchor
+    deleted:&:duplex-list:char <- get deletion, deleted-text:offset
+    old-cursor:&:duplex-list:char <- last deleted
+    splice anchor, deleted
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    before-cursor <- copy old-cursor
+    cursor-row <- get deletion, before-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get deletion, before-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get deletion, before-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+after <handle-redo> [
+  {
+    deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
+    break-unless is-delete?
+    start:&:duplex-list:char <- get deletion, delete-from:offset
+    end:&:duplex-list:char <- get deletion, delete-until:offset
+    data:&:duplex-list:char <- get *editor, data:offset
+    remove-between start, end
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    cursor-row <- get deletion, after-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get deletion, after-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get deletion, before-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+# undo delete
+
+scenario editor-can-undo-and-redo-delete [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit delete and backspace a few times
+  assume-console [
+    type [abcdef]
+    left-click 1, 2
+    press delete
+    press backspace
+    press delete
+    press delete
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .af        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # undo deletes
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  screen-should-contain [
+    .          .
+    .adef      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo backspace
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .abdef     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo first delete
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .abcdef    .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo first delete
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .abdef     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo backspace
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  screen-should-contain [
+    .          .
+    .adef      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo deletes
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  screen-should-contain [
+    .          .
+    .af        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <begin-delete-character> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-delete-character> [
+  {
+    break-unless deleted-cell  # delete failed; don't add an undo operation
+    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-column:offset
+    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    undo:&:list:&:operation <- get *editor, undo:offset
+    {
+      # if previous operation was an insert, coalesce this operation with it
+      break-unless undo
+      op:&:operation <- first undo
+      deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
+      break-unless is-delete?
+      previous-coalesce-tag:num <- get deletion, tag:offset
+      coalesce?:bool <- equal previous-coalesce-tag, 2/coalesce-delete
+      break-unless coalesce?
+      delete-until:&:duplex-list:char <- next before-cursor
+      deletion <- put deletion, delete-until:offset, delete-until
+      deleted-so-far:&:duplex-list:char <- get deletion, deleted-text:offset
+      deleted-so-far <- append deleted-so-far, deleted-cell
+      deletion <- put deletion, deleted-text:offset, deleted-so-far
+      deletion <- put deletion, after-row:offset, cursor-row
+      deletion <- put deletion, after-column:offset, cursor-column
+      deletion <- put deletion, after-top-of-screen:offset, top-after
+      *op <- merge 2/delete-operation, deletion
+      break +done-adding-delete-operation
+    }
+    # if not, create a new operation
+    op:&:operation <- new operation:type
+    deleted-until:&:duplex-list:char <- next before-cursor
+    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cell/deleted, before-cursor/delete-from, deleted-until, 2/coalesce-delete
+    editor <- add-operation editor, op
+    +done-adding-delete-operation
+  }
+]
+
+# undo ctrl-k
+
+scenario editor-can-undo-and-redo-ctrl-k [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit delete and backspace a few times
+  assume-console [
+    left-click 1, 1
+    press ctrl-k
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  screen-should-contain [
+    .          .
+    .a         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .a1        .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <begin-delete-to-end-of-line> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-delete-to-end-of-line> [
+  {
+    break-unless deleted-cells  # delete failed; don't add an undo operation
+    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-column:offset
+    deleted-until:&:duplex-list:char <- next before-cursor
+    op:&:operation <- new operation:type
+    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cells/deleted, before-cursor/delete-from, deleted-until, 0/never-coalesce
+    editor <- add-operation editor, op
+    +done-adding-delete-operation
+  }
+]
+
+# undo ctrl-u
+
+scenario editor-can-undo-and-redo-ctrl-u [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit delete and backspace a few times
+  assume-console [
+    left-click 1, 2
+    press ctrl-u
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .c         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  screen-should-contain [
+    .          .
+    .c         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .1c        .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <begin-delete-to-start-of-line> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-delete-to-start-of-line> [
+  {
+    break-unless deleted-cells  # delete failed; don't add an undo operation
+    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+    op:&:operation <- new operation:type
+    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    deleted-until:&:duplex-list:char <- next before-cursor
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-column:offset
+    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cells/deleted, before-cursor/delete-from, deleted-until, 0/never-coalesce
+    editor <- add-operation editor, op
+    +done-adding-delete-operation
+  }
+]
+
+scenario editor-can-undo-and-redo-ctrl-u-2 [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit delete and backspace a few times
+  assume-console [
+    type [abc]
+    press ctrl-u
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
diff --git a/archive/2.vm/edit/Readme.md b/archive/2.vm/edit/Readme.md
new file mode 100644
index 00000000..698e534b
--- /dev/null
+++ b/archive/2.vm/edit/Readme.md
@@ -0,0 +1,49 @@
+Environment for learning programming using Mu: http://akkartik.name/post/mu
+
+Run it from the `mu` directory:
+
+  ```shell
+  $ ./mu edit
+  ```
+
+This will load all the `.mu` files in this directory and then run the editor.
+Press ctrl-c to quit. Press F4 to save your work (if a lesson/ directory
+exists) and to run the contents of the sandbox editor on the right.
+
+You can also run the tests for the environment:
+
+  ```shell
+  $ ./mu test edit
+  ```
+
+You can also load the files more explicitly by enumerating them all (in order):
+
+  ```shell
+  $  ./mu edit/*.mu
+  ```
+
+This is handy if you want to play with simpler versions of the editor that are
+easier to understand. Stop loading at any layer to run with a subset of
+features:
+
+  ```shell
+  $ ./mu edit/001*.mu edit/002*.mu  # run a simple editor rather than the full environment
+  ```
+
+---
+
+Appendix: keyboard shortcuts
+
+  _moving and scrolling_
+  - `ctrl-a` or `home`: move cursor to start of line
+  - `ctrl-e` or `end`: move cursor to end of line
+  - `ctrl-f` or `page-down`: scroll down by one page
+  - `ctrl-b` or `page-up`: scroll up by one page
+  - `ctrl-x`: scroll down by one line
+  - `ctrl-s`: scroll up by one line
+  - `ctrl-t`: scroll until current line is at top of screen
+
+  _modifying text_
+  - `ctrl-k`: delete text from cursor to end of line
+  - `ctrl-u`: delete text from start of line until just before cursor
+  - `ctrl-/`: comment/uncomment current line (using a special leader to ignore real comments https://www.reddit.com/r/vim/comments/4ootmz/_/d4ehmql)
diff --git a/archive/2.vm/example1.mu b/archive/2.vm/example1.mu
new file mode 100644
index 00000000..83995a1e
--- /dev/null
+++ b/archive/2.vm/example1.mu
@@ -0,0 +1,7 @@
+def main [
+  local-scope
+  a:num <- add 2, 2
+  a <- multiply a, 3
+  # uncomment the next line to see the output
+# $print a
+]
diff --git a/archive/2.vm/exception1.mu b/archive/2.vm/exception1.mu
new file mode 100644
index 00000000..df4754e5
--- /dev/null
+++ b/archive/2.vm/exception1.mu
@@ -0,0 +1,61 @@
+# Example program showing exceptions built out of delimited continuations.
+
+# Since Mu is statically typed, we can't build an all-purpose higher-order
+# function called 'try'; it wouldn't know how many arguments the function
+# passed to it needs to take, what their types are, etc. Instead, until Mu
+# gets macros we'll directly use the continuation primitives.
+
+def main [
+  local-scope
+  foo false/no-exception
+  foo true/raise-exception
+]
+
+# example showing exception handling
+def foo raise-exception?:bool [
+  local-scope
+  load-inputs
+  # To run an instruction of the form:
+  #   try f ...
+  # write this:
+  #   call-with-continuation-mark 999/exception-tag, f, ...
+  # By convention we reserve tag 999 for exceptions.
+  #
+  # 'f' above may terminate at either a regular 'return' or a 'return-with-continuation-mark'.
+  # We never re-call the continuation returned in the latter case;
+  # its existence merely signals that an exception was raised.
+  # So just treat it as a boolean.
+  # The other inputs and outputs to 'call-with-continuation-mark' depend on
+  # the function it is called with.
+  exception-raised?:bool, err:text, result:num <- call-with-continuation-mark 999/exception-tag, f, raise-exception?
+  {
+    break-if exception-raised?
+    $print [normal exit; result ] result 10/newline
+  }
+  {
+    break-unless exception-raised?
+    $print [error caught: ] err 10/newline
+  }
+]
+
+# A callee function that can raise an exception has some weird constraints at
+# the moment.
+#
+# The caller's 'call-with-continuation-mark' instruction may return with
+# either a regular 'return' or a 'return-continuation-until-mark'.
+# To handle both cases, regular 'return' instructions in the callee must
+# prepend an extra 0 result, in place of the continuation that may have been
+# returned.
+# This change to number of outputs violates our type system, so the call has
+# to be dynamically typed. The callee can't have a header.
+def f [
+  local-scope
+  raise-exception?:bool <- next-input
+  {
+    break-unless raise-exception?
+    # throw/raise: 2 results + implicit continuation (ignoring the continuation tag)
+    return-continuation-until-mark 999/exception-tag, [error will robinson!], 0/unused
+  }
+  # normal return: 3 results including 0 continuation placeholder at start
+  return 0/continuation-placeholder, null/no-error, 34/regular-result
+]
diff --git a/archive/2.vm/exception2.mu b/archive/2.vm/exception2.mu
new file mode 100644
index 00000000..f07c135b
--- /dev/null
+++ b/archive/2.vm/exception2.mu
@@ -0,0 +1,62 @@
+# Example program showing exceptions built out of delimited continuations.
+# Slightly less klunky than exception1.mu.
+
+# Since Mu is statically typed, we can't build an all-purpose higher-order
+# function called 'try'; it wouldn't know how many arguments the function
+# passed to it needs to take, what their types are, etc. Instead, until Mu
+# gets macros we'll directly use the continuation primitives.
+
+exclusive-container error-or:_elem [
+  error:text
+  value:_elem
+]
+
+def main [
+  local-scope
+  foo false/no-exception
+  foo true/raise-exception
+]
+
+# example showing exception handling
+def foo raise-exception?:bool [
+  local-scope
+  load-inputs
+  # To run an instruction of the form:
+  #   try f ...
+  # write this:
+  #   call-with-continuation-mark 999/exception-tag, f, ...
+  # By convention we reserve tag 999 for exceptions.
+  #
+  # The other inputs and outputs to 'call-with-continuation-mark' depend on
+  # the function it is called with.
+  _, result:error-or:num <- call-with-continuation-mark 999/exception-tag, f, raise-exception?
+  {
+    val:num, normal-exit?:bool <- maybe-convert result, value:variant
+    break-unless normal-exit?
+    $print [normal exit; result ] val 10/newline
+  }
+  {
+    err:text, error-exit?:bool <- maybe-convert result, error:variant
+    break-unless error-exit?
+    $print [error caught: ] err 10/newline
+  }
+]
+
+# Callee function that we catch exceptions in must always return using a
+# continuation.
+def f raise-exception?:bool -> result:error-or:num [
+  local-scope
+  load-inputs
+  {
+    break-unless raise-exception?
+    # throw/raise
+    result <- merge 0/error, [error will robinson!]
+    return-continuation-until-mark 999/exception-tag, result
+  }
+  # 'normal' return; still uses the continuation mark
+  result <- merge 1/value, 34
+  return-continuation-until-mark 999/exception-tag, result
+  # dead code just to avoid errors
+  result <- merge 1/value, 0
+  return result
+]
diff --git a/archive/2.vm/exuberant_ctags_rc b/archive/2.vm/exuberant_ctags_rc
new file mode 100644
index 00000000..46823a2e
--- /dev/null
+++ b/archive/2.vm/exuberant_ctags_rc
@@ -0,0 +1,11 @@
+--langdef=mu
+--langmap=mu:.mu
+--regex-mu=/^def[ \t]+([^ \t]*)/\1/d,definition/
+--regex-mu=/^def![ \t]+([^ \t]*)/\1/d,definition/
+--regex-mu=/^recipe[ \t]+([^ \t]*)/\1/d,definition/
+--regex-mu=/^recipe![ \t]+([^ \t]*)/\1/d,definition/
+--regex-mu=/^type[ \t]+([^ \t]*)/\1/t,typeref/
+--regex-mu=/^container[ \t]+([^ \t:]*)/\1/s,struct/
+--regex-mu=/^exclusive-container[ \t]+([^ \t:]*)/\1/u,union/
+--regex-mu=/$x/x/x/e/ --------- next option is for way-points in Mu
+--regex-mu=/^[ \t]*(<[^ \t]*>)/\1/d,definition/
diff --git a/archive/2.vm/factorial.mu b/archive/2.vm/factorial.mu
new file mode 100644
index 00000000..cf2284b2
--- /dev/null
+++ b/archive/2.vm/factorial.mu
@@ -0,0 +1,33 @@
+# example program: compute the factorial of 5
+
+def main [
+  local-scope
+  x:num <- factorial 5
+  $print [result: ], x, [ 
+]
+]
+
+def factorial n:num -> result:num [
+  local-scope
+  load-inputs
+  {
+    # if n=0 return 1
+    zero?:bool <- equal n, 0
+    break-unless zero?
+    return 1
+  }
+  # return n * factorial(n-1)
+  x:num <- subtract n, 1
+  subresult:num <- factorial x
+  result <- multiply subresult, n
+]
+
+# unit test
+scenario factorial-test [
+  run [
+    1:num <- factorial 5
+  ]
+  memory-should-contain [
+    1 <- 120
+  ]
+]
diff --git a/archive/2.vm/filesystem.mu b/archive/2.vm/filesystem.mu
new file mode 100644
index 00000000..6ea8e08c
--- /dev/null
+++ b/archive/2.vm/filesystem.mu
@@ -0,0 +1,20 @@
+# example program: copy one file into another, character by character
+# BEWARE: this will modify your file system
+# before running it, put some text into /tmp/mu-x
+# after running it, check /tmp/mu-y
+
+def main [
+  local-scope
+  source-file:&:source:char <- start-reading null/real-filesystem, [/tmp/mu-x]
+  sink-file:&:sink:char, write-routine:num <- start-writing null/real-filesystem, [/tmp/mu-y]
+  {
+    c:char, done?:bool, source-file <- read source-file
+    break-if done?
+    sink-file <- write sink-file, c
+    loop
+  }
+  close sink-file
+  # make sure to wait for the file to be actually written to disk
+  # (Mu practices structured concurrency: http://250bpm.com/blog:71)
+  wait-for-routine write-routine
+]
diff --git a/archive/2.vm/fork.mu b/archive/2.vm/fork.mu
new file mode 100644
index 00000000..af414cf2
--- /dev/null
+++ b/archive/2.vm/fork.mu
@@ -0,0 +1,16 @@
+# example program: running multiple routines
+
+def main [
+  start-running thread2
+  {
+    $print 34
+    loop
+  }
+]
+
+def thread2 [
+  {
+    $print 35
+    loop
+  }
+]
diff --git a/archive/2.vm/git_log_filtered b/archive/2.vm/git_log_filtered
new file mode 100755
index 00000000..7ab6d43e
--- /dev/null
+++ b/archive/2.vm/git_log_filtered
@@ -0,0 +1,9 @@
+#!/bin/sh
+# Show the log while skipping unimportant directories.
+#
+# I usually run this through an alias local to this repo:
+#   $ git config alias.ll '!./git_log_filtered'
+#   $ git ll --stat
+set -e
+
+git log $* -- . ":(exclude)html"
diff --git a/archive/2.vm/http-client.mu b/archive/2.vm/http-client.mu
new file mode 100644
index 00000000..8f04c2bc
--- /dev/null
+++ b/archive/2.vm/http-client.mu
@@ -0,0 +1,29 @@
+# example program: reading a URL over HTTP
+
+def main [
+  local-scope
+  $print [aaa] 10/newline
+  google:&:source:char <- start-reading-from-network null/real-resources, [google.com/]
+  $print [bbb] 10/newline
+  n:num <- copy 0
+  buf:&:buffer:char <- new-buffer 30
+  {
+    c:char, done?:bool <- read google
+    break-if done?
+    n <- add n, 1
+    buf <- append buf, c
+    {
+      _, a:num <- divide-with-remainder n, 100
+      break-if a
+      $print n 10/newline
+    }
+    loop
+  }
+  result:text <- buffer-to-array buf
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  len:num <- length *result
+  print null/real-screen, result
+  wait-for-some-interaction
+  close-console
+]
diff --git a/archive/2.vm/http-server.mu b/archive/2.vm/http-server.mu
new file mode 100644
index 00000000..6bd172b3
--- /dev/null
+++ b/archive/2.vm/http-server.mu
@@ -0,0 +1,28 @@
+# example program: a single-request HTTP server
+#   listen for connections from clients on a server socket
+#   when a connection occurs, transfer it to a session socket
+#   read from it using channels
+#   write to it directly
+#
+# After running it, navigate to localhost:8080. Your browser should display
+# "SUCCESS!" and the server will terminate after one connection.
+
+def main [
+  local-scope
+  socket:num <- $open-server-socket 8080/port
+  $print [Mu socket creation returned ], socket, 10/newline
+  return-unless socket
+  session:num <- $accept socket
+  contents:&:source:char, sink:&:sink:char <- new-channel 30
+  sink <- start-running receive-from-socket session, sink
+  query:text <- drain contents
+  $print [Done reading from socket.], 10/newline
+  write-to-socket session, [HTTP/1.0 200 OK
+Content-type: text/plain
+
+SUCCESS!
+]
+  $print 10/newline, [Wrote to and closing socket...], 10/newline
+  session <- $close-socket session
+  socket <- $close-socket socket
+]
diff --git a/archive/2.vm/immutable-error.mu b/archive/2.vm/immutable-error.mu
new file mode 100644
index 00000000..25554b2f
--- /dev/null
+++ b/archive/2.vm/immutable-error.mu
@@ -0,0 +1,13 @@
+# compare mutable.mu
+
+def main [
+  local-scope
+  x:&:num <- new number:type
+  foo x
+]
+
+def foo x:&:num [
+  local-scope
+  load-inputs
+  *x <- copy 34  # will cause an error because x is immutable in this function
+]
diff --git a/archive/2.vm/lambda-to-mu.mu b/archive/2.vm/lambda-to-mu.mu
new file mode 100644
index 00000000..a171b4ca
--- /dev/null
+++ b/archive/2.vm/lambda-to-mu.mu
@@ -0,0 +1,590 @@
+## experimental compiler to translate programs written in a generic
+## expression-oriented language called 'lambda' into Mu
+
+# incomplete; code generator not done
+# potential enhancements:
+#   symbol table
+#   poor man's macros
+#     substitute one instruction with multiple, parameterized by inputs and products
+
+scenario convert-lambda [
+  run [
+    local-scope
+    1:text/raw <- lambda-to-mu [(add a (multiply b c))]
+    2:@:char/raw <- copy *1:text/raw
+  ]
+  memory-should-contain [
+    2:array:character <- [t1 <- multiply b c
+result <- add a t1]
+  ]
+]
+
+def lambda-to-mu in:text -> out:text [
+  local-scope
+  load-inputs
+  out <- copy null
+  cells:&:cell <- parse in
+  out <- to-mu cells
+]
+
+# 'parse' will turn lambda expressions into trees made of cells
+exclusive-container cell [
+  atom:text
+  pair:pair
+]
+
+# printed below as < first | rest >
+container pair [
+  first:&:cell
+  rest:&:cell
+]
+
+def new-atom name:text -> result:&:cell [
+  local-scope
+  load-inputs
+  result <- new cell:type
+  *result <- merge 0/tag:atom, name
+]
+
+def new-pair a:&:cell, b:&:cell -> result:&:cell [
+  local-scope
+  load-inputs
+  result <- new cell:type
+  *result <- merge 1/tag:pair, a/first, b/rest
+]
+
+def is-atom? x:&:cell -> result:bool [
+  local-scope
+  load-inputs
+  return-unless x, false
+  _, result <- maybe-convert *x, atom:variant
+]
+
+def is-pair? x:&:cell -> result:bool [
+  local-scope
+  load-inputs
+  return-unless x, false
+  _, result <- maybe-convert *x, pair:variant
+]
+
+scenario atom-is-not-pair [
+  local-scope
+  s:text <- new [a]
+  x:&:cell <- new-atom s
+  10:bool/raw <- is-atom? x
+  11:bool/raw <- is-pair? x
+  memory-should-contain [
+    10 <- 1
+    11 <- 0
+  ]
+]
+
+scenario pair-is-not-atom [
+  local-scope
+  # construct (a . nil)
+  s:text <- new [a]
+  x:&:cell <- new-atom s
+  y:&:cell <- new-pair x, null
+  10:bool/raw <- is-atom? y
+  11:bool/raw <- is-pair? y
+  memory-should-contain [
+    10 <- 0
+    11 <- 1
+  ]
+]
+
+def atom-match? x:&:cell, pat:text -> result:bool [
+  local-scope
+  load-inputs
+  s:text, is-atom?:bool <- maybe-convert *x, atom:variant
+  return-unless is-atom?, false
+  result <- equal pat, s
+]
+
+scenario atom-match [
+  local-scope
+  x:&:cell <- new-atom [abc]
+  10:bool/raw <- atom-match? x, [abc]
+  memory-should-contain [
+    10 <- 1
+  ]
+]
+
+def first x:&:cell -> result:&:cell [
+  local-scope
+  load-inputs
+  pair:pair, pair?:bool <- maybe-convert *x, pair:variant
+  return-unless pair?, null
+  result <- get pair, first:offset
+]
+
+def rest x:&:cell -> result:&:cell [
+  local-scope
+  load-inputs
+  pair:pair, pair?:bool <- maybe-convert *x, pair:variant
+  return-unless pair?, null
+  result <- get pair, rest:offset
+]
+
+def set-first base:&:cell, new-first:&:cell -> base:&:cell [
+  local-scope
+  load-inputs
+  pair:pair, is-pair?:bool <- maybe-convert *base, pair:variant
+  return-unless is-pair?
+  pair <- put pair, first:offset, new-first
+  *base <- merge 1/pair, pair
+]
+
+def set-rest base:&:cell, new-rest:&:cell -> base:&:cell [
+  local-scope
+  load-inputs
+  pair:pair, is-pair?:bool <- maybe-convert *base, pair:variant
+  return-unless is-pair?
+  pair <- put pair, rest:offset, new-rest
+  *base <- merge 1/pair, pair
+]
+
+scenario cell-operations-on-atom [
+  local-scope
+  s:text <- new [a]
+  x:&:cell <- new-atom s
+  10:&:cell/raw <- first x
+  11:&:cell/raw <- rest x
+  memory-should-contain [
+    10 <- 0  # first is nil
+    11 <- 0  # rest is nil
+  ]
+]
+
+scenario cell-operations-on-pair [
+  local-scope
+  # construct (a . nil)
+  s:text <- new [a]
+  x:&:cell <- new-atom s
+  y:&:cell <- new-pair x, null
+  x2:&:cell <- first y
+  10:bool/raw <- equal x, x2
+  11:&:cell/raw <- rest y
+  memory-should-contain [
+    10 <- 1  # first is correct
+    11 <- 0  # rest is nil
+  ]
+]
+
+## convert lambda text to a tree of cells
+
+def parse in:text -> out:&:cell [
+  local-scope
+  load-inputs
+  s:&:stream:char <- new-stream in
+  out, s <- parse s
+  trace 2, [app/parse], out
+]
+
+def parse in:&:stream:char -> out:&:cell, in:&:stream:char [
+  local-scope
+  load-inputs
+  # skip whitespace
+  in <- skip-whitespace in
+  c:char, eof?:bool <- peek in
+  return-if eof?, null
+  pair?:bool <- equal c, 40/open-paren
+  {
+    break-if pair?
+    # atom
+    buf:&:buffer:char <- new-buffer 30
+    {
+      done?:bool <- end-of-stream? in
+      break-if done?
+      # stop before close paren or space
+      c:char <- peek in
+      done? <- equal c, 41/close-paren
+      break-if done?
+      done? <- space? c
+      break-if done?
+      c <- read in
+      buf <- append buf, c
+      loop
+    }
+    s:text <- buffer-to-array buf
+    out <- new-atom s
+  }
+  {
+    break-unless pair?
+    # pair
+    read in  # skip the open-paren
+    out <- new cell:type  # start out with nil
+    # read in first element of pair
+    {
+      end?:bool <- end-of-stream? in
+      not-end?:bool <- not end?
+      assert not-end?, [unbalanced '(' in expression]
+      c <- peek in
+      close-paren?:bool <- equal c, 41/close-paren
+      break-if close-paren?
+      first:&:cell, in <- parse in
+      *out <- merge 1/pair, first, null
+    }
+    # read in any remaining elements
+    curr:&:cell <- copy out
+    {
+      in <- skip-whitespace in
+      end?:bool <- end-of-stream? in
+      not-end?:bool <- not end?
+      assert not-end?, [unbalanced '(' in expression]
+      # termination check: ')'
+      c <- peek in
+      {
+        close-paren?:bool <- equal c, 41/close-paren
+        break-unless close-paren?
+        read in  # skip ')'
+        break +end-pair
+      }
+      # still here? read next element of pair
+      next:&:cell, in <- parse in
+      is-dot?:bool <- atom-match? next, [.]
+      {
+        break-if is-dot?
+        next-curr:&:cell <- new-pair next, null
+        curr <- set-rest curr, next-curr
+        curr <- rest curr
+      }
+      {
+        break-unless is-dot?
+        # deal with dotted pair
+        in <- skip-whitespace in
+        c <- peek in
+        not-close-paren?:bool <- not-equal c, 41/close-paren
+        assert not-close-paren?, [')' cannot immediately follow '.']
+        final:&:cell <- parse in
+        curr <- set-rest curr, final
+        # we're not gonna update curr, so better make sure the next iteration
+        # is going to end the pair
+        in <- skip-whitespace in
+        c <- peek in
+        close-paren?:bool <- equal c, 41/close-paren
+        assert close-paren?, ['.' must be followed by exactly one expression before ')']
+      }
+      loop
+    }
+    +end-pair
+  }
+]
+
+def skip-whitespace in:&:stream:char -> in:&:stream:char [
+  local-scope
+  load-inputs
+  {
+    done?:bool <- end-of-stream? in
+    return-if done?, null
+    c:char <- peek in
+    space?:bool <- space? c
+    break-unless space?
+    read in  # skip
+    loop
+  }
+]
+
+def to-text x:&:cell -> out:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 30
+  buf <- to-buffer x, buf
+  out <- buffer-to-array buf
+]
+
+def to-buffer x:&:cell, buf:&:buffer:char -> buf:&:buffer:char [
+  local-scope
+  load-inputs
+  # base case: empty cell
+  {
+    break-if x
+    buf <- append buf, [<>]
+    return
+  }
+  # base case: atom
+  {
+    s:text, atom?:bool <- maybe-convert *x, atom:variant
+    break-unless atom?
+    buf <- append buf, s
+    return
+  }
+  # recursive case: pair
+  buf <- append buf, [< ]
+  first:&:cell <- first x
+  buf <- to-buffer first, buf
+  buf <- append buf, [ | ]
+  rest:&:cell <- rest x
+  buf <- to-buffer rest, buf
+  buf <- append buf, [ >]
+]
+
+scenario parse-single-letter-atom [
+  local-scope
+  s:text <- new [a]
+  x:&:cell <- parse s
+  s2:text, 10:bool/raw <- maybe-convert *x, atom:variant
+  11:@:char/raw <- copy *s2
+  memory-should-contain [
+    10 <- 1  # parse result is an atom
+    11:array:character <- [a]
+  ]
+]
+
+scenario parse-atom [
+  local-scope
+  s:text <- new [abc]
+  x:&:cell <- parse s
+  s2:text, 10:bool/raw <- maybe-convert *x, atom:variant
+  11:@:char/raw <- copy *s2
+  memory-should-contain [
+    10 <- 1  # parse result is an atom
+    11:array:character <- [abc]
+  ]
+]
+
+scenario parse-list-of-two-atoms [
+  local-scope
+  s:text <- new [(abc def)]
+  x:&:cell <- parse s
+  trace-should-contain [
+    app/parse: < abc | < def | <> > >
+  ]
+  10:bool/raw <- is-pair? x
+  x1:&:cell <- first x
+  x2:&:cell <- rest x
+  s1:text, 11:bool/raw <- maybe-convert *x1, atom:variant
+  12:bool/raw <- is-pair? x2
+  x3:&:cell <- first x2
+  s2:text, 13:bool/raw <- maybe-convert *x3, atom:variant
+  14:&:cell/raw <- rest x2
+  20:@:char/raw <- copy *s1
+  30:@:char/raw <- copy *s2
+  memory-should-contain [
+    10 <- 1  # parse result is a pair
+    11 <- 1  # result.first is an atom
+    12 <- 1  # result.rest is a pair
+    13 <- 1  # result.rest.first is an atom
+    14 <- 0  # result.rest.rest is nil
+    20:array:character <- [abc]  # result.first
+    30:array:character <- [def]  # result.rest.first
+  ]
+]
+
+scenario parse-list-with-extra-spaces [
+  local-scope
+  s:text <- new [ ( abc  def ) ]  # extra spaces
+  x:&:cell <- parse s
+  trace-should-contain [
+    app/parse: < abc | < def | <> > >
+  ]
+  10:bool/raw <- is-pair? x
+  x1:&:cell <- first x
+  x2:&:cell <- rest x
+  s1:text, 11:bool/raw <- maybe-convert *x1, atom:variant
+  12:bool/raw <- is-pair? x2
+  x3:&:cell <- first x2
+  s2:text, 13:bool/raw <- maybe-convert *x3, atom:variant
+  14:&:cell/raw <- rest x2
+  20:@:char/raw <- copy *s1
+  30:@:char/raw <- copy *s2
+  memory-should-contain [
+    10 <- 1  # parse result is a pair
+    11 <- 1  # result.first is an atom
+    12 <- 1  # result.rest is a pair
+    13 <- 1  # result.rest.first is an atom
+    14 <- 0  # result.rest.rest is nil
+    20:array:character <- [abc]  # result.first
+    30:array:character <- [def]  # result.rest.first
+  ]
+]
+
+scenario parse-list-of-more-than-two-atoms [
+  local-scope
+  s:text <- new [(abc def ghi)]
+  x:&:cell <- parse s
+  trace-should-contain [
+    app/parse: < abc | < def | < ghi | <> > > >
+  ]
+  10:bool/raw <- is-pair? x
+  x1:&:cell <- first x
+  x2:&:cell <- rest x
+  s1:text, 11:bool/raw <- maybe-convert *x1, atom:variant
+  12:bool/raw <- is-pair? x2
+  x3:&:cell <- first x2
+  s2:text, 13:bool/raw <- maybe-convert *x3, atom:variant
+  x4:&:cell <- rest x2
+  14:bool/raw <- is-pair? x4
+  x5:&:cell <- first x4
+  s3:text, 15:bool/raw <- maybe-convert *x5, atom:variant
+  16:&:cell/raw <- rest x4
+  20:@:char/raw <- copy *s1
+  30:@:char/raw <- copy *s2
+  40:@:char/raw <- copy *s3
+  memory-should-contain [
+    10 <- 1  # parse result is a pair
+    11 <- 1  # result.first is an atom
+    12 <- 1  # result.rest is a pair
+    13 <- 1  # result.rest.first is an atom
+    14 <- 1  # result.rest.rest is a pair
+    15 <- 1  # result.rest.rest.first is an atom
+    16 <- 0  # result.rest.rest.rest is nil
+    20:array:character <- [abc]  # result.first
+    30:array:character <- [def]  # result.rest.first
+    40:array:character <- [ghi]  # result.rest.rest
+  ]
+]
+
+scenario parse-nested-list [
+  local-scope
+  s:text <- new [((abc))]
+  x:&:cell <- parse s
+  trace-should-contain [
+    app/parse: < < abc | <> > | <> >
+  ]
+  10:bool/raw <- is-pair? x
+  x1:&:cell <- first x
+  11:bool/raw <- is-pair? x
+  x2:&:cell <- first x1
+  s1:text, 12:bool/raw <- maybe-convert *x2, atom:variant
+  13:&:cell/raw <- rest x1
+  14:&:cell/raw <- rest x
+  20:@:char/raw <- copy *s1
+  memory-should-contain [
+    10 <- 1  # parse result is a pair
+    11 <- 1  # result.first is a pair
+    12 <- 1  # result.first.first is an atom
+    13 <- 0  # result.first.rest is nil
+    14 <- 0  # result.rest is nil
+    20:array:character <- [abc]  # result.first.first
+  ]
+]
+
+scenario parse-nested-list-2 [
+  local-scope
+  s:text <- new [((abc) def)]
+  x:&:cell <- parse s
+  trace-should-contain [
+    app/parse: < < abc | <> > | < def | <> > >
+  ]
+  10:bool/raw <- is-pair? x
+  x1:&:cell <- first x
+  11:bool/raw <- is-pair? x
+  x2:&:cell <- first x1
+  s1:text, 12:bool/raw <- maybe-convert *x2, atom:variant
+  13:&:cell/raw <- rest x1
+  x3:&:cell <- rest x
+  x4:&:cell <- first x3
+  s2:text, 14:bool/raw <- maybe-convert *x4, atom:variant
+  15:&:cell/raw <- rest x3
+  20:@:char/raw <- copy *s1
+  30:@:char/raw <- copy *s2
+  memory-should-contain [
+    10 <- 1  # parse result is a pair
+    11 <- 1  # result.first is a pair
+    12 <- 1  # result.first.first is an atom
+    13 <- 0  # result.first.rest is nil
+    14 <- 1  # result.rest.first is an atom
+    15 <- 0  # result.rest.rest is nil
+    20:array:character <- [abc]  # result.first.first
+    30:array:character <- [def]  # result.rest.first
+  ]
+]
+
+# todo: uncomment these tests after we figure out how to continue tests after
+# assertion failures
+#? scenario parse-error [
+#?   local-scope
+#?   s:text <- new [(]
+#? #?   hide-errors
+#?   x:&:cell <- parse s
+#? #?   show-errors
+#?   trace-should-contain [
+#?     error: unbalanced '(' in expression
+#?   ]
+#? ]
+#? 
+#? scenario parse-error-after-element [
+#?   local-scope
+#?   s:text <- new [(abc]
+#? #?   hide-errors
+#?   x:&:cell <- parse s
+#? #?   show-errors
+#?   trace-should-contain [
+#?     error: unbalanced '(' in expression
+#?   ]
+#? ]
+
+scenario parse-dotted-list-of-two-atoms [
+  local-scope
+  s:text <- new [(abc . def)]
+  x:&:cell <- parse s
+  trace-should-contain [
+    app/parse: < abc | def >
+  ]
+  10:bool/raw <- is-pair? x
+  x1:&:cell <- first x
+  x2:&:cell <- rest x
+  s1:text, 11:bool/raw <- maybe-convert *x1, atom:variant
+  s2:text, 12:bool/raw <- maybe-convert *x2, atom:variant
+  20:@:char/raw <- copy *s1
+  30:@:char/raw <- copy *s2
+  memory-should-contain [
+    # parses to < abc | def >
+    10 <- 1  # parse result is a pair
+    11 <- 1  # result.first is an atom
+    12 <- 1  # result.rest is an atom
+    20:array:character <- [abc]  # result.first
+    30:array:character <- [def]  # result.rest
+  ]
+]
+
+scenario parse-dotted-list-of-more-than-two-atoms [
+  local-scope
+  s:text <- new [(abc def . ghi)]
+  x:&:cell <- parse s
+  trace-should-contain [
+    app/parse: < abc | < def | ghi > >
+  ]
+  10:bool/raw <- is-pair? x
+  x1:&:cell <- first x
+  x2:&:cell <- rest x
+  s1:text, 11:bool/raw <- maybe-convert *x1, atom:variant
+  12:bool/raw <- is-pair? x2
+  x3:&:cell <- first x2
+  s2:text, 13:bool/raw <- maybe-convert *x3, atom:variant
+  x4:&:cell <- rest x2
+  s3:text, 14:bool/raw <- maybe-convert *x4, atom:variant
+  20:@:char/raw <- copy *s1
+  30:@:char/raw <- copy *s2
+  40:@:char/raw <- copy *s3
+  memory-should-contain [
+    10 <- 1  # parse result is a pair
+    11 <- 1  # result.first is an atom
+    12 <- 1  # result.rest is a pair
+    13 <- 1  # result.rest.first is an atom
+    14 <- 1  # result.rest.rest is an atom
+    20:array:character <- [abc]  # result.first
+    30:array:character <- [def]  # result.rest.first
+    40:array:character <- [ghi]  # result.rest.rest
+  ]
+]
+
+## convert tree of cells to Mu text
+
+def to-mu in:&:cell -> out:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 30
+  buf <- to-mu in, buf
+  out <- buffer-to-array buf
+]
+
+def to-mu in:&:cell, buf:&:buffer:char -> buf:&:buffer:char, result-name:text [
+  local-scope
+  load-inputs
+  # null cell? no change.
+  # pair with all atoms? gensym a new variable
+  # pair containing other pairs? recurse
+  result-name <- copy null
+]
diff --git a/archive/2.vm/mu b/archive/2.vm/mu
new file mode 100755
index 00000000..67068ebb
--- /dev/null
+++ b/archive/2.vm/mu
@@ -0,0 +1,11 @@
+#!/bin/sh
+# Run interpreter, first compiling if necessary.
+set -e
+
+./build3  &&  ./mu_bin "$@"
+
+# Scenarios considered:
+#   ./mu
+#   ./mu --help
+#   ./mu test
+#   ./mu test file1.mu
diff --git a/archive/2.vm/mu.vim b/archive/2.vm/mu.vim
new file mode 100644
index 00000000..dd0763a7
--- /dev/null
+++ b/archive/2.vm/mu.vim
@@ -0,0 +1,98 @@
+" Vim syntax file
+" Language:    mu
+" Maintainer:  Kartik Agaram <mu@akkartik.com>
+" URL:         http://github.com/akkartik/mu
+" License:     public domain
+"
+" Copy this into your ftplugin directory, and add the following to your vimrc
+" or to .vim/ftdetect/mu.vim:
+"   autocmd BufReadPost,BufNewFile *.mu set filetype=mu
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+" todo: why does this periodically lose syntax, like on file reload?
+"   $ vim x.mu
+"   :e
+"? if exists("b:syntax")
+"?   finish
+"? endif
+"? let b:syntax = "mu"
+
+setlocal iskeyword=@,48-57,?,!,_,$,-
+setlocal formatoptions-=t  " Mu programs have long lines
+setlocal formatoptions+=c  " but comments should still wrap
+
+syntax match muComment /#.*$/  | highlight link muComment Comment
+syntax match muSalientComment /##.*$/  | highlight link muSalientComment SalientComment
+syntax match muComment /;.*$/  | highlight link muComment Comment
+syntax match muSalientComment /;;.*$/  | highlight link muSalientComment SalientComment
+set comments+=n:#
+syntax match muCommentedCode "#? .*"  | highlight link muCommentedCode CommentedCode
+let b:cmt_head = "#? "
+
+syntax match muDelimiter "[{}]"  | highlight link muDelimiter Delimiter
+
+" Mu strings are inside [ ... ] and can span multiple lines
+" don't match '[' at end of line, that's usually code
+syntax match muLiteral %^[^ a-zA-Z0-9(){}\[\]#$_*@&,=-][^ ,]*\|[ ,]\@<=[^ a-zA-Z0-9(){}\[\]#$_*@&,=-][^ ,]*%
+syntax region muString start=+\[[^\]]+ end=+\]+
+syntax match muString "\[\]"
+highlight link muString String
+" Mu syntax for representing the screen in scenarios
+syntax region muScreen start=+ \.+ end=+\.$\|$+
+highlight link muScreen muString
+
+" Mu literals
+syntax match muLiteral %[^ ]\+:literal/[^ ,]*\|[^ ]\+:literal\>%
+syntax match muLiteral %\<[0-9-]\?[0-9]\+/[^ ,]*%
+syntax match muLiteral % [0-9-]\?[0-9]\+[, ]\@=\| [0-9-]\?[0-9]\+$%
+syntax match muLiteral "^\s\+[^ 0-9a-zA-Z{}$#\[\]][^ ]*\s*$"
+" labels
+syntax match muLiteral %[^ ]\+:label/[^ ,]*\|[^ ]\+:label\>%
+" other literal types
+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
+
+" sources of action at a distance
+syntax match muAssign "<-"
+syntax match muAssign "\<raw\>"
+highlight link muAssign SpecialChar
+syntax match muGlobal %[^ ]\+:global/\?[^ ,]*%  | highlight link muGlobal SpecialChar
+
+" common keywords
+" use regular expressions for common words that may come after '/'
+syntax keyword muKeyword default-space local-scope
+syntax keyword muKeyword next-input rewind-inputs load-inputs
+syntax keyword muKeyword next-ingredient rewind-ingredients load-ingredients
+syntax match muKeyword " input\>\| ingredient\>"
+highlight link muKeyword Constant
+
+syntax keyword muControl return return-if return-unless
+syntax keyword muControl reply reply-if reply-unless
+syntax keyword muControl output-if output-unless
+syntax match muControl "^return\>\| return\>\|^reply\>\| reply\>\|^output\|^ output\>"
+syntax keyword muControl jump-if jump-unless
+syntax keyword muControl break-if break-unless
+syntax keyword muControl loop-if loop-unless
+syntax match muControl "^jump\>\| jump\>\|^break\>\| break\>\|^loop\>\| loop\>"
+syntax keyword muControl start-running
+syntax keyword muControl call-with-continuation-mark return-continuation-until-mark
+highlight link muControl Identifier
+
+syntax match muRecipe "->"
+syntax match muRecipe "^recipe\>\|^def\>\|^before\>\|^after\>\| -> "
+syntax keyword muRecipe recipe! def! function fn
+highlight link muRecipe PreProc
+
+syntax match muScenario "^scenario\>"  | highlight link muScenario Statement
+syntax keyword muPendingScenario pending-scenario  | highlight link muPendingScenario SpecialChar
+syntax match muData "^type\>\|^container\>"
+syntax keyword muData exclusive-container
+highlight link muData Constant
+
+let &cpo = s:save_cpo
diff --git a/archive/2.vm/mutable.mu b/archive/2.vm/mutable.mu
new file mode 100644
index 00000000..1a1ec7f0
--- /dev/null
+++ b/archive/2.vm/mutable.mu
@@ -0,0 +1,13 @@
+# compare immutable-error.mu
+
+def main [
+  local-scope
+  x:&:num <- new number:type
+  foo x
+]
+
+def foo x:&:num -> x:&:num [
+  local-scope
+  load-inputs
+  *x <- copy 34
+]
diff --git a/archive/2.vm/new_lesson b/archive/2.vm/new_lesson
new file mode 100755
index 00000000..3642b823
--- /dev/null
+++ b/archive/2.vm/new_lesson
@@ -0,0 +1,15 @@
+#!/bin/sh
+# Run this before running './mu edit' or './mu sandbox' to make sure you don't
+# lose any work.
+#
+# You'll be editing code in lesson/recipes.mu, and any sandboxes you create
+# will be in lesson/0, lesson/1, etc., from top to bottom.
+
+set -e
+
+mkdir lesson
+cd lesson
+git init
+echo '**/.*.swp' > .gitignore
+git add .
+git commit -m 0
diff --git a/archive/2.vm/nqueens.mu b/archive/2.vm/nqueens.mu
new file mode 100644
index 00000000..9e9f4425
--- /dev/null
+++ b/archive/2.vm/nqueens.mu
@@ -0,0 +1,101 @@
+# http://rosettacode.org/wiki/N-queens_problem
+# port of the Arc solution at http://arclanguage.org/item?id=19743
+# run with tracing turned on:
+#   ./mu --trace nqueens.mu
+
+container square [
+  rank:num
+  file:num
+]
+
+def nqueens n:num, queens:&:list:square -> result:num, queens:&:list:square [
+  local-scope
+  load-inputs
+  # if 'queens' is already long enough, print it and return
+  added-so-far:num <- length queens
+  {
+    done?:bool <- greater-or-equal added-so-far, n
+    break-unless done?
+    stash queens
+    return 1
+  }
+  # still work to do
+  next-rank:num <- copy 0
+  {
+    break-unless queens
+    first:square <- first queens
+    existing-rank:num <- get first, rank:offset
+    next-rank <- add existing-rank, 1
+  }
+  result <- copy 0
+  next-file:num <- copy 0
+  {
+    done?:bool <- greater-or-equal next-file, n
+    break-if done?
+    curr:square <- merge next-rank, next-file
+    {
+      curr-conflicts?:bool <- conflict? curr, queens
+      break-if curr-conflicts?
+      new-queens:&:list:square <- push curr, queens
+      sub-result:num <- nqueens n, new-queens
+      result <- add result, sub-result
+    }
+    next-file <- add next-file, 1
+    loop
+  }
+]
+
+# check if putting a queen on 'curr' conflicts with any of the existing
+# queens
+# assumes that 'curr' is on a non-conflicting rank, and checks for conflict
+# only in files and diagonals
+def conflict? curr:square, queens:&:list:square -> result:bool [
+  local-scope
+  load-inputs
+  result <- conflicting-file? curr, queens
+  return-if result
+  result <- conflicting-diagonal? curr, queens
+]
+
+def conflicting-file? curr:square, queens:&:list:square -> result:bool [
+  local-scope
+  load-inputs
+  curr-file:num <- get curr, file:offset
+  {
+    break-unless queens
+    q:square <- first queens
+    qfile:num <- get q, file:offset
+    file-match?:bool <- equal curr-file, qfile
+    return-if file-match?, true/conflict-found
+    queens <- rest queens
+    loop
+  }
+  return false/no-conflict-found
+]
+
+def conflicting-diagonal? curr:square, queens:&:list:square -> result:bool [
+  local-scope
+  load-inputs
+  curr-rank:num <- get curr, rank:offset
+  curr-file:num <- get curr, file:offset
+  {
+    break-unless queens
+    q:square <- first queens
+    qrank:num <- get q, rank:offset
+    qfile:num <- get q, file:offset
+    rank-delta:num <- subtract qrank, curr-rank
+    file-delta:num <- subtract qfile, curr-file
+    rank-delta <- abs rank-delta
+    file-delta <- abs file-delta
+    diagonal-match?:bool <- equal rank-delta, file-delta
+    return-if diagonal-match?, true/conflict-found
+    queens <- rest queens
+    loop
+  }
+  return false/no-conflict-found
+]
+
+def main [
+  nqueens 4
+  $dump-trace [app]
+]
diff --git a/archive/2.vm/real-files.mu b/archive/2.vm/real-files.mu
new file mode 100644
index 00000000..7e50ac8e
--- /dev/null
+++ b/archive/2.vm/real-files.mu
@@ -0,0 +1,18 @@
+# example program: read a character from one file and write it to another
+# BEWARE: this will modify your file system
+# before running it, put a character into /tmp/mu-x
+# after running it, check /tmp/mu-y
+
+def main [
+  local-scope
+  f:num/file <- $open-file-for-reading [/tmp/mu-x]
+  $print [file to read from: ], f, 10/newline
+  c:char, eof?:bool <- $read-from-file f
+  $print [copying ], c, 10/newline
+  f <- $close-file f
+  $print [file after closing: ], f, 10/newline
+  f <- $open-file-for-writing [/tmp/mu-y]
+  $print [file to write to: ], f, 10/newline
+  $write-to-file f, c
+  f <- $close-file f
+]
diff --git a/archive/2.vm/relayout b/archive/2.vm/relayout
new file mode 100755
index 00000000..8ea48920
--- /dev/null
+++ b/archive/2.vm/relayout
@@ -0,0 +1,65 @@
+#!/bin/bash
+# Helper to change the numerical prefixes across the repo, say if you want to
+# create room between 023 and 024, and so on.
+#
+# Assumes there's only ever one file with any numeric prefix. If you move
+# 003trace.test.cc you might need to do some manual patch-up.
+
+set -e
+
+if [[ $# -eq 0 && `git diff HEAD |wc -l` -gt 0 ]]
+then
+  echo "Uncommitted changes"
+  exit
+fi
+
+if [[ $# -gt 0 ]] # dry run
+then
+  git() {
+    echo $*
+  }
+fi
+
+#
+
+index=0
+ls [0-9]* |grep -v "trace.test" |sort -n |
+  while read file
+  do
+    while [[ $file != `printf "%03d" $index`* ]]
+    do
+      echo
+      index=$(($index+1))
+    done
+    echo $file
+    index=$(($index+1))
+  done > .layout
+
+vim -c "set nu" .layout
+
+#
+
+root() {
+  echo $1 |sed 's/^[0-9]*//'
+}
+
+index=0
+cat .layout |
+  while read file
+  do
+    if [ ! -z $file ]
+    then
+      newfile=`printf "%03d" $index``root $file`
+      if [[ $newfile != $file ]]
+      then
+        echo git mv $file $newfile
+        git mv $file $newfile
+      fi
+    fi
+    index=$(($index+1))
+  done
+
+rm .layout
+
+# Scenarios considered:
+#   Don't redo the layout if Vim exits with error.
diff --git a/archive/2.vm/same-fringe.mu b/archive/2.vm/same-fringe.mu
new file mode 100644
index 00000000..b9235006
--- /dev/null
+++ b/archive/2.vm/same-fringe.mu
@@ -0,0 +1,89 @@
+# The 'same fringe' problem: http://wiki.c2.com/?SameFringeProblem
+# Example program demonstrating coroutines using Mu's delimited continuations.
+#
+# Expected output:
+#   1
+# (i.e. that the two given trees x and y have the same leaves, in the same
+# order from left to right)
+
+container tree:_elem [
+  val:_elem
+  left:&:tree:_elem
+  right:&:tree:_elem
+]
+
+def main [
+  local-scope
+  # x: ((a b) c)
+  # y: (a (b c))
+  a:&:tree:num <- new-tree 3
+  b:&:tree:num <- new-tree 4
+  c:&:tree:num <- new-tree 5
+  x1:&:tree:num <- new-tree a, b
+  x:&:tree:num <- new-tree x1, c
+  y1:&:tree:num <- new-tree b, c
+  y:&:tree:num <- new-tree a, y1
+  result:bool <- same-fringe x, y
+  $print result 10/newline
+]
+
+def same-fringe a:&:tree:_elem, b:&:tree:_elem -> result:bool [
+  local-scope
+  load-inputs
+  k1:continuation <- call-with-continuation-mark 100/mark, process, a
+  k2:continuation <- call-with-continuation-mark 100/mark, process, b
+  {
+    k1, x:_elem, a-done?:bool <- call k1
+    k2, y:_elem, b-done?:bool <- call k2
+    break-if a-done?
+    break-if b-done?
+    match?:bool <- equal x, y
+    return-unless match?, false
+    loop
+  }
+  result <- and a-done?, b-done?
+]
+
+# harness around traversal
+def process t:&:tree:_elem [
+  local-scope
+  load-inputs
+  return-continuation-until-mark 100/mark  # initial
+  traverse t
+  zero-val:&:_elem <- new _elem:type
+  return-continuation-until-mark 100/mark, *zero-val, true/done  # final
+  assert false, [continuation called past done]
+]
+
+# core traversal
+def traverse t:&:tree:_elem [
+  local-scope
+  load-inputs
+  return-unless t
+  l:&:tree:_elem <- get *t, left:offset
+  traverse l
+  r:&:tree:_elem <- get *t, right:offset
+  traverse r
+  return-if l
+  return-if r
+  # leaf
+  v:_elem <- get *t, val:offset
+  return-continuation-until-mark 100/mark, v, false/not-done
+]
+
+# details
+
+def new-tree x:_elem -> result:&:tree:_elem [
+  local-scope
+  load-inputs
+  result <- new {(tree _elem): type}
+  put *result, val:offset, x
+]
+
+def new-tree l:&:tree:_elem, r:&:tree:_elem -> result:&:tree:_elem [
+  local-scope
+  load-inputs
+  result <- new {(tree _elem): type}
+  put *result, left:offset, l
+  put *result, right:offset, r
+]
diff --git a/archive/2.vm/sandbox/001-editor.mu b/archive/2.vm/sandbox/001-editor.mu
new file mode 100644
index 00000000..b3399dbb
--- /dev/null
+++ b/archive/2.vm/sandbox/001-editor.mu
@@ -0,0 +1,464 @@
+## the basic editor data structure, and how it displays text to the screen
+
+# temporary main for this layer: just render the given text at the given
+# screen dimensions, then stop
+def main text:text [
+  local-scope
+  load-inputs
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  e:&:editor <- new-editor text, 0/left, 5/right
+  render null/screen, e
+  wait-for-event null/console
+  close-console
+]
+
+scenario editor-renders-text-to-screen [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    # top line of screen reserved for menu
+    .          .
+    .abc       .
+    .          .
+  ]
+]
+
+container editor [
+  # editable text: doubly linked list of characters (head contains a special sentinel)
+  data:&:duplex-list:char
+  top-of-screen:&:duplex-list:char
+  bottom-of-screen:&:duplex-list:char
+  # location before cursor inside data
+  before-cursor:&:duplex-list:char
+
+  # raw bounds of display area on screen
+  # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
+  left:num
+  right:num
+  bottom:num
+  # raw screen coordinates of cursor
+  cursor-row:num
+  cursor-column:num
+]
+
+# creates a new editor widget
+#   right is exclusive
+def new-editor s:text, left:num, right:num -> result:&:editor [
+  local-scope
+  load-inputs
+  # no clipping of bounds
+  right <- subtract right, 1
+  result <- new editor:type
+  # initialize screen-related fields
+  *result <- put *result, left:offset, left
+  *result <- put *result, right:offset, right
+  # initialize cursor coordinates
+  *result <- put *result, cursor-row:offset, 1/top
+  *result <- put *result, cursor-column:offset, left
+  # initialize empty contents
+  init:&:duplex-list:char <- push 167/§, null
+  *result <- put *result, data:offset, init
+  *result <- put *result, top-of-screen:offset, init
+  *result <- put *result, before-cursor:offset, init
+  result <- insert-text result, s
+  <editor-initialization>
+]
+
+def insert-text editor:&:editor, text:text -> editor:&:editor [
+  local-scope
+  load-inputs
+  curr:&:duplex-list:char <- get *editor, data:offset
+  insert curr, text
+]
+
+scenario editor-initializes-without-data [
+  local-scope
+  assume-screen 5/width, 3/height
+  run [
+    e:&:editor <- new-editor null/data, 2/left, 5/right
+    1:editor/raw <- copy *e
+  ]
+  memory-should-contain [
+    # 1,2 (data) <- just the § sentinel
+    # 3,4 (top of screen) <- the § sentinel
+    # 5 (bottom of screen) <- null since text fits on screen
+    5 <- 0
+    6 <- 0
+    # 7,8 (before cursor) <- the § sentinel
+    9 <- 2  # left
+    10 <- 4  # right  (inclusive)
+    11 <- 0  # bottom (not set until render)
+    12 <- 1  # cursor row
+    13 <- 2  # cursor column
+  ]
+  screen-should-contain [
+    .     .
+    .     .
+    .     .
+  ]
+]
+
+# Assumes cursor should be at coordinates (cursor-row, cursor-column) and
+# updates before-cursor to match. Might also move coordinates if they're
+# outside text.
+def render screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor, 1/top, 0/left
+  left:num <- get *editor, left:offset
+  screen-height:num <- screen-height screen
+  right:num <- get *editor, right:offset
+  # traversing editor
+  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
+  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
+  curr <- next curr
+  # traversing screen
+  color:num <- copy 7/white
+  row:num <- copy 1/top
+  column:num <- copy left
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  screen <- move-cursor screen, row, column
+  {
+    +next-character
+    break-unless curr
+    off-screen?:bool <- greater-or-equal row, screen-height
+    break-if off-screen?
+    # update editor.before-cursor
+    # Doing so at the start of each iteration ensures it stays one step behind
+    # the current character.
+    {
+      at-cursor-row?:bool <- equal row, cursor-row
+      break-unless at-cursor-row?
+      at-cursor?:bool <- equal column, cursor-column
+      break-unless at-cursor?
+      before-cursor <- copy prev
+    }
+    c:char <- get *curr, value:offset
+    <character-c-received>
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # adjust cursor if necessary
+      {
+        at-cursor-row?:bool <- equal row, cursor-row
+        break-unless at-cursor-row?
+        left-of-cursor?:bool <- lesser-than column, cursor-column
+        break-unless left-of-cursor?
+        cursor-column <- copy column
+        before-cursor <- prev curr
+      }
+      # clear rest of line in this window
+      clear-line-until screen, right
+      # skip to next line
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      curr <- next curr
+      prev <- next prev
+      loop +next-character
+    }
+    {
+      # at right? wrap. even if there's only one more letter left; we need
+      # room for clicking on the cursor after it.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment curr
+      loop +next-character
+    }
+    print screen, c, color
+    curr <- next curr
+    prev <- next prev
+    column <- add column, 1
+    loop
+  }
+  # save first character off-screen
+  *editor <- put *editor, bottom-of-screen:offset, curr
+  # is cursor to the right of the last line? move to end
+  {
+    at-cursor-row?:bool <- equal row, cursor-row
+    cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
+    before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
+    above-cursor-row?:bool <- lesser-than row, cursor-row
+    before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
+    break-unless before-cursor?
+    cursor-row <- copy row
+    cursor-column <- copy column
+    before-cursor <- copy prev
+  }
+  *editor <- put *editor, bottom:offset, row
+  *editor <- put *editor, cursor-row:offset, cursor-row
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  clear-line-until screen, right
+  row <- add row, 1
+  return row, left/column
+]
+
+def clear-screen-from screen:&:screen, row:num, column:num, left:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  # if it's the real screen, use the optimized primitive
+  {
+    break-if screen
+    clear-display-from row, column, left, right
+    return
+  }
+  # if not, go the slower route
+  screen <- move-cursor screen, row, column
+  clear-line-until screen, right
+  clear-rest-of-screen screen, row, left, right
+]
+
+def clear-rest-of-screen screen:&:screen, row:num, left:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  row <- add row, 1
+  # if it's the real screen, use the optimized primitive
+  {
+    break-if screen
+    clear-display-from row, left, left, right
+    return
+  }
+  screen <- move-cursor screen, row, left
+  screen-height:num <- screen-height screen
+  {
+    at-bottom-of-screen?:bool <- greater-or-equal row, screen-height
+    break-if at-bottom-of-screen?
+    screen <- move-cursor screen, row, left
+    clear-line-until screen, right
+    row <- add row, 1
+    loop
+  }
+]
+
+scenario editor-prints-multiple-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    .abc  .
+    .def  .
+    .     .
+  ]
+]
+
+scenario editor-handles-offsets [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [abc], 1/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    . abc .
+    .     .
+  ]
+]
+
+scenario editor-prints-multiple-lines-at-offset [
+  local-scope
+  assume-screen 5/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 1/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    . abc .
+    . def .
+    .     .
+  ]
+]
+
+scenario editor-wraps-long-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [abc def], 0/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    .abc ↩.
+    .def  .
+    .     .
+  ]
+  screen-should-contain-in-color 245/grey [
+    .     .
+    .    ↩.
+    .     .
+    .     .
+  ]
+]
+
+scenario editor-wraps-barely-long-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  run [
+    render screen, e
+  ]
+  # still wrap, even though the line would fit. We need room to click on the
+  # end of the line
+  screen-should-contain [
+    .     .
+    .abcd↩.
+    .e    .
+    .     .
+  ]
+  screen-should-contain-in-color 245/grey [
+    .     .
+    .    ↩.
+    .     .
+    .     .
+  ]
+]
+
+scenario editor-with-empty-text [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 5/right
+  run [
+    render screen, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .     .
+    .     .
+    .     .
+  ]
+  memory-should-contain [
+    3 <- 1  # cursor row
+    4 <- 0  # cursor column
+  ]
+]
+
+# just a little color for Mu code
+
+scenario render-colors-comments [
+  local-scope
+  assume-screen 5/width, 5/height
+  s:text <- new [abc
+# de
+f]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .     .
+    .abc  .
+    .# de .
+    .f    .
+    .     .
+  ]
+  screen-should-contain-in-color 12/lightblue, [
+    .     .
+    .     .
+    .# de .
+    .     .
+    .     .
+  ]
+  screen-should-contain-in-color 7/white, [
+    .     .
+    .abc  .
+    .     .
+    .f    .
+    .     .
+  ]
+]
+
+after <character-c-received> [
+  color <- get-color color, c
+]
+
+# so far the previous color is all the information we need; that may change
+def get-color color:num, c:char -> color:num [
+  local-scope
+  load-inputs
+  color-is-white?:bool <- equal color, 7/white
+  # if color is white and next character is '#', switch color to blue
+  {
+    break-unless color-is-white?
+    starting-comment?:bool <- equal c, 35/#
+    break-unless starting-comment?
+    trace 90, [app], [switch color back to blue]
+    return 12/lightblue
+  }
+  # if color is blue and next character is newline, switch color to white
+  {
+    color-is-blue?:bool <- equal color, 12/lightblue
+    break-unless color-is-blue?
+    ending-comment?:bool <- equal c, 10/newline
+    break-unless ending-comment?
+    trace 90, [app], [switch color back to white]
+    return 7/white
+  }
+  # if color is white (no comments) and next character is '<', switch color to red
+  {
+    break-unless color-is-white?
+    starting-assignment?:bool <- equal c, 60/<
+    break-unless starting-assignment?
+    return 1/red
+  }
+  # if color is red and next character is space, switch color to white
+  {
+    color-is-red?:bool <- equal color, 1/red
+    break-unless color-is-red?
+    ending-assignment?:bool <- equal c, 32/space
+    break-unless ending-assignment?
+    return 7/white
+  }
+  # otherwise no change
+  return color
+]
+
+scenario render-colors-assignment [
+  local-scope
+  assume-screen 8/width, 5/height
+  s:text <- new [abc
+d <- e
+f]
+  e:&:editor <- new-editor s, 0/left, 8/right
+  run [
+    render screen, e
+  ]
+  screen-should-contain [
+    .        .
+    .abc     .
+    .d <- e  .
+    .f       .
+    .        .
+  ]
+  screen-should-contain-in-color 1/red, [
+    .        .
+    .        .
+    .  <-    .
+    .        .
+    .        .
+  ]
+]
diff --git a/archive/2.vm/sandbox/002-typing.mu b/archive/2.vm/sandbox/002-typing.mu
new file mode 100644
index 00000000..ef3f25d2
--- /dev/null
+++ b/archive/2.vm/sandbox/002-typing.mu
@@ -0,0 +1,1144 @@
+## handling events from the keyboard, mouse, touch screen, ...
+
+# temporary main: interactive editor
+# hit ctrl-c to exit
+def! main text:text [
+  local-scope
+  load-inputs
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  editor:&:editor <- new-editor text, 5/left, 45/right
+  editor-render null/screen, editor
+  editor-event-loop null/screen, null/console, editor
+  close-console
+]
+
+def editor-event-loop screen:&:screen, console:&:console, editor:&:editor -> screen:&:screen, console:&:console, editor:&:editor [
+  local-scope
+  load-inputs
+  {
+    # looping over each (keyboard or touch) event as it occurs
+    +next-event
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-column:offset
+    screen <- move-cursor screen, cursor-row, cursor-column
+    e:event, found?:bool, quit?:bool, console <- read-event console
+    loop-unless found?
+    break-if quit?  # only in tests
+    trace 10, [app], [next-event]
+    # 'touch' event
+    t:touch-event, is-touch?:bool <- maybe-convert e, touch:variant
+    {
+      break-unless is-touch?
+      move-cursor editor, screen, t
+      loop +next-event
+    }
+    # keyboard events
+    {
+      break-if is-touch?
+      go-render?:bool <- handle-keyboard-event screen, editor, e
+      {
+        break-unless go-render?
+        screen <- editor-render screen, editor
+      }
+    }
+    loop
+  }
+]
+
+# process click, return if it was on current editor
+def move-cursor editor:&:editor, screen:&:screen, t:touch-event -> in-focus?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor, false
+  click-row:num <- get t, row:offset
+  return-unless click-row, false  # ignore clicks on 'menu'
+  click-column:num <- get t, column:offset
+  left:num <- get *editor, left:offset
+  too-far-left?:bool <- lesser-than click-column, left
+  return-if too-far-left?, false
+  right:num <- get *editor, right:offset
+  too-far-right?:bool <- greater-than click-column, right
+  return-if too-far-right?, false
+  # position cursor
+  <begin-move-cursor>
+  editor <- snap-cursor editor, screen, click-row, click-column
+  undo-coalesce-tag:num <- copy 0/never
+  <end-move-cursor>
+  # gain focus
+  return true
+]
+
+# Variant of 'render' that only moves the cursor (coordinates and
+# before-cursor). If it's past the end of a line, it 'slides' it left. If it's
+# past the last line it positions at end of last line.
+def snap-cursor editor:&:editor, screen:&:screen, target-row:num, target-column:num -> editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  screen-height:num <- screen-height screen
+  # count newlines until screen row
+  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
+  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
+  curr <- next curr
+  row:num <- copy 1/top
+  column:num <- copy left
+  *editor <- put *editor, cursor-row:offset, target-row
+  cursor-row:num <- copy target-row
+  *editor <- put *editor, cursor-column:offset, target-column
+  cursor-column:num <- copy target-column
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  {
+    +next-character
+    break-unless curr
+    off-screen?:bool <- greater-or-equal row, screen-height
+    break-if off-screen?
+    # update editor.before-cursor
+    # Doing so at the start of each iteration ensures it stays one step behind
+    # the current character.
+    {
+      at-cursor-row?:bool <- equal row, cursor-row
+      break-unless at-cursor-row?
+      at-cursor?:bool <- equal column, cursor-column
+      break-unless at-cursor?
+      before-cursor <- copy prev
+      *editor <- put *editor, before-cursor:offset, before-cursor
+    }
+    c:char <- get *curr, value:offset
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # adjust cursor if necessary
+      {
+        at-cursor-row?:bool <- equal row, cursor-row
+        break-unless at-cursor-row?
+        left-of-cursor?:bool <- lesser-than column, cursor-column
+        break-unless left-of-cursor?
+        cursor-column <- copy column
+        *editor <- put *editor, cursor-column:offset, cursor-column
+        before-cursor <- copy prev
+        *editor <- put *editor, before-cursor:offset, before-cursor
+      }
+      # skip to next line
+      row <- add row, 1
+      column <- copy left
+      curr <- next curr
+      prev <- next prev
+      loop +next-character
+    }
+    {
+      # at right? wrap. even if there's only one more letter left; we need
+      # room for clicking on the cursor after it.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      column <- copy left
+      row <- add row, 1
+      # don't increment curr/prev
+      loop +next-character
+    }
+    curr <- next curr
+    prev <- next prev
+    column <- add column, 1
+    loop
+  }
+  # is cursor to the right of the last line? move to end
+  {
+    at-cursor-row?:bool <- equal row, cursor-row
+    cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
+    before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
+    above-cursor-row?:bool <- lesser-than row, cursor-row
+    before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
+    break-unless before-cursor?
+    cursor-row <- copy row
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- copy column
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    before-cursor <- copy prev
+    *editor <- put *editor, before-cursor:offset, before-cursor
+  }
+]
+
+# Process an event 'e' and try to minimally update the screen.
+# Set 'go-render?' to true to indicate the caller must perform a non-minimal update.
+def handle-keyboard-event screen:&:screen, editor:&:editor, e:event -> go-render?:bool, screen:&:screen, editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor, false/don't-render
+  screen-width:num <- screen-width screen
+  screen-height:num <- screen-height screen
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  save-row:num <- copy cursor-row
+  save-column:num <- copy cursor-column
+  # character
+  {
+    c:char, is-unicode?:bool <- maybe-convert e, text:variant
+    break-unless is-unicode?
+    trace 10, [app], [handle-keyboard-event: special character]
+    # exceptions for special characters go here
+    <handle-special-character>
+    # ignore any other special characters
+    regular-character?:bool <- greater-or-equal c, 32/space
+    return-unless regular-character?, false/don't-render
+    # otherwise type it in
+    <begin-insert-character>
+    go-render? <- insert-at-cursor editor, c, screen
+    <end-insert-character>
+    return
+  }
+  # special key to modify the text or move the cursor
+  k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
+  assert is-keycode?, [event was of unknown type; neither keyboard nor mouse]
+  # handlers for each special key will go here
+  <handle-special-key>
+  return true/go-render
+]
+
+def insert-at-cursor editor:&:editor, c:char, screen:&:screen -> go-render?:bool, editor:&:editor, screen:&:screen [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  insert c, before-cursor
+  before-cursor <- next before-cursor
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  save-row:num <- copy cursor-row
+  save-column:num <- copy cursor-column
+  screen-width:num <- screen-width screen
+  screen-height:num <- screen-height screen
+  # occasionally we'll need to mess with the cursor
+  <insert-character-special-case>
+  # but mostly we'll just move the cursor right
+  cursor-column <- add cursor-column, 1
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  next:&:duplex-list:char <- next before-cursor
+  {
+    # at end of all text? no need to scroll? just print the character and leave
+    at-end?:bool <- equal next, null
+    break-unless at-end?
+    bottom:num <- subtract screen-height, 1
+    at-bottom?:bool <- equal save-row, bottom
+    at-right?:bool <- equal save-column, right
+    overflow?:bool <- and at-bottom?, at-right?
+    break-if overflow?
+    move-cursor screen, save-row, save-column
+    print screen, c
+    return false/don't-render
+  }
+  {
+    # not at right margin? print the character and rest of line
+    break-unless next
+    at-right?:bool <- greater-or-equal cursor-column, screen-width
+    break-if at-right?
+    curr:&:duplex-list:char <- copy before-cursor
+    move-cursor screen, save-row, save-column
+    curr-column:num <- copy save-column
+    {
+      # hit right margin? give up and let caller render
+      at-right?:bool <- greater-than curr-column, right
+      return-if at-right?, true/go-render
+      break-unless curr
+      # newline? done.
+      currc:char <- get *curr, value:offset
+      at-newline?:bool <- equal currc, 10/newline
+      break-if at-newline?
+      print screen, currc
+      curr-column <- add curr-column, 1
+      curr <- next curr
+      loop
+    }
+    return false/don't-render
+  }
+  return true/go-render
+]
+
+# helper for tests
+def editor-render screen:&:screen, editor:&:editor -> screen:&:screen, editor:&:editor [
+  local-scope
+  load-inputs
+  old-top-idx:num <- save-top-idx screen
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  row:num, column:num <- render screen, editor
+  draw-horizontal screen, row, left, right, 9480/horizontal-dotted
+  row <- add row, 1
+  clear-screen-from screen, row, left, left, right
+  assert-no-scroll screen, old-top-idx
+]
+
+scenario editor-handles-empty-event-queue [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  assume-console []
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-handles-mouse-clicks [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 1  # on the 'b'
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1  # cursor is at row 0..
+    4 <- 1  # ..and column 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-outside-text [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  $clear-trace
+  assume-console [
+    left-click 1, 7  # last line, to the right of text
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1  # cursor row
+    4 <- 3  # cursor column
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-outside-text-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  $clear-trace
+  assume-console [
+    left-click 1, 7  # interior line, to the right of text
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1  # cursor row
+    4 <- 3  # cursor column
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-outside-text-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  $clear-trace
+  assume-console [
+    left-click 3, 7  # below text
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2  # cursor row
+    4 <- 3  # cursor column
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-outside-column [
+  local-scope
+  assume-screen 10/width, 5/height
+  # editor occupies only left half of screen
+  e:&:editor <- new-editor [abc], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    # click on right half of screen
+    left-click 3, 8
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1  # no change to cursor row
+    4 <- 0  # ..or column
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-handles-mouse-clicks-in-menu-area [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    # click on first, 'menu' row
+    left-click 0, 3
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # no change to cursor
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+]
+
+scenario editor-inserts-characters-into-empty-editor [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    type [abc]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  check-trace-count-for-label 3, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # type two letters at different places
+  assume-console [
+    type [0]
+    left-click 1, 2
+    type [d]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0adbc     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 7, [print-character]  # 4 for first letter, 3 for second
+]
+
+scenario editor-inserts-characters-at-cursor-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 5  # right of last line
+    type [d]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor-5 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 5  # right of non-last line
+    type [e]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abce      .
+    .d         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 3, 5  # below all text
+    type [d]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor-4 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 3, 5  # below all text
+    type [e]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .de        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+scenario editor-inserts-characters-at-cursor-6 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 3, 5  # below all text
+    type [ef]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 2, [print-character]
+]
+
+scenario editor-moves-cursor-after-inserting-characters [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [ab], 0/left, 5/right
+  editor-render screen, e
+  assume-console [
+    type [01]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .01ab      .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+]
+
+# if the cursor reaches the right margin, wrap the line
+
+scenario editor-wraps-line-on-insert [
+  local-scope
+  assume-screen 5/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 5/right
+  editor-render screen, e
+  # type a letter
+  assume-console [
+    type [e]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # no wrap yet
+  screen-should-contain [
+    .     .
+    .eabc .
+    .┈┈┈┈┈.
+    .     .
+    .     .
+  ]
+  # type a second letter
+  assume-console [
+    type [f]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # now wrap
+  screen-should-contain [
+    .     .
+    .efab↩.
+    .c    .
+    .┈┈┈┈┈.
+    .     .
+  ]
+]
+
+scenario editor-wraps-line-on-insert-2 [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  s:text <- new [abcdefg
+defg]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  # type more text at the start
+  assume-console [
+    left-click 3, 0
+    type [abc]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor is not wrapped
+  memory-should-contain [
+    3 <- 3
+    4 <- 3
+  ]
+  # but line is wrapped
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .efg       .
+    .abcd↩     .
+    .efg       .
+  ]
+]
+
+after <insert-character-special-case> [
+  # if the line wraps at the cursor, move cursor to start of next row
+  {
+    # if either:
+    # a) we're at the end of the line and at the column of the wrap indicator, or
+    # b) we're not at end of line and just before the column of the wrap indicator
+    wrap-column:num <- copy right
+    before-wrap-column:num <- subtract wrap-column, 1
+    at-wrap?:bool <- greater-or-equal cursor-column, wrap-column
+    just-before-wrap?:bool <- greater-or-equal cursor-column, before-wrap-column
+    next:&:duplex-list:char <- next before-cursor
+    # at end of line? next == 0 || next.value == 10/newline
+    at-end-of-line?:bool <- equal next, null
+    {
+      break-if at-end-of-line?
+      next-character:char <- get *next, value:offset
+      at-end-of-line? <- equal next-character, 10/newline
+    }
+    # break unless ((eol? and at-wrap?) or (~eol? and just-before-wrap?))
+    move-cursor-to-next-line?:bool <- copy false
+    {
+      break-if at-end-of-line?
+      move-cursor-to-next-line? <- copy just-before-wrap?
+      # if we're moving the cursor because it's in the middle of a wrapping
+      # line, adjust it to left-most column
+      potential-new-cursor-column:num <- copy left
+    }
+    {
+      break-unless at-end-of-line?
+      move-cursor-to-next-line? <- copy at-wrap?
+      # if we're moving the cursor because it's at the end of a wrapping line,
+      # adjust it to one past the left-most column to make room for the
+      # newly-inserted wrap-indicator
+      potential-new-cursor-column:num <- add left, 1/make-room-for-wrap-indicator
+    }
+    break-unless move-cursor-to-next-line?
+    cursor-column <- copy potential-new-cursor-column
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    cursor-row <- add cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    # if we're out of the screen, scroll down
+    {
+      below-screen?:bool <- greater-or-equal cursor-row, screen-height
+      break-unless below-screen?
+      <scroll-down>
+    }
+    return true/go-render
+  }
+]
+
+scenario editor-wraps-cursor-after-inserting-characters-in-middle-of-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  assume-console [
+    left-click 1, 3  # right before the wrap icon
+    type [f]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abcf↩     .
+    .de        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 2  # cursor row
+    4 <- 0  # cursor column
+  ]
+]
+
+scenario editor-wraps-cursor-after-inserting-characters-at-end-of-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # create an editor containing two lines
+  s:text <- new [abc
+xyz]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abc       .
+    .xyz       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    left-click 1, 4  # at end of first line
+    type [de]  # trigger wrap
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .e         .
+    .xyz       .
+    .┈┈┈┈┈     .
+  ]
+]
+
+scenario editor-wraps-cursor-to-left-margin [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcde], 2/left, 7/right
+  assume-console [
+    left-click 1, 5  # line is full; no wrap icon yet
+    type [01]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .  abc0↩   .
+    .  1de     .
+    .  ┈┈┈┈┈   .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 2  # cursor row
+    4 <- 3  # cursor column
+  ]
+]
+
+# if newline, move cursor to start of next line, and maybe align indent with previous line
+
+container editor [
+  indent?:bool
+]
+
+after <editor-initialization> [
+  *result <- put *result, indent?:offset, true
+]
+
+scenario editor-moves-cursor-down-after-inserting-newline [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  assume-console [
+    type [0
+1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0         .
+    .1abc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <handle-special-character> [
+  {
+    newline?:bool <- equal c, 10/newline
+    break-unless newline?
+    <begin-insert-enter>
+    insert-new-line-and-indent editor, screen
+    <end-insert-enter>
+    return true/go-render
+  }
+]
+
+def insert-new-line-and-indent editor:&:editor, screen:&:screen -> editor:&:editor, screen:&:screen [
+  local-scope
+  load-inputs
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  screen-height:num <- screen-height screen
+  # update cursor coordinates
+  at-start-of-wrapped-line?:bool <- at-start-of-wrapped-line? editor
+  {
+    break-if at-start-of-wrapped-line?
+    cursor-row <- add cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+  }
+  cursor-column <- copy left
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  # maybe scroll
+  {
+    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal, never greater
+    break-unless below-screen?
+    <scroll-down2>
+    cursor-row <- subtract cursor-row, 1  # bring back into screen range
+    *editor <- put *editor, cursor-row:offset, cursor-row
+  }
+  # insert newline
+  insert 10/newline, before-cursor
+  before-cursor <- next before-cursor
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  # indent if necessary
+  indent?:bool <- get *editor, indent?:offset
+  return-unless indent?
+  d:&:duplex-list:char <- get *editor, data:offset
+  end-of-previous-line:&:duplex-list:char <- prev before-cursor
+  indent:num <- line-indent end-of-previous-line, d
+  i:num <- copy 0
+  {
+    indent-done?:bool <- greater-or-equal i, indent
+    break-if indent-done?
+    insert-at-cursor editor, 32/space, screen
+    i <- add i, 1
+    loop
+  }
+]
+
+def at-start-of-wrapped-line? editor:&:editor -> result:bool [
+  local-scope
+  load-inputs
+  left:num <- get *editor, left:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  cursor-at-left?:bool <- equal cursor-column, left
+  return-unless cursor-at-left?, false
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  before-before-cursor:&:duplex-list:char <- prev before-cursor
+  return-unless before-before-cursor, false  # cursor is at start of editor
+  char-before-cursor:char <- get *before-cursor, value:offset
+  cursor-after-newline?:bool <- equal char-before-cursor, 10/newline
+  return-if cursor-after-newline?, false
+  # if cursor is at left margin and not at start, but previous character is not a newline,
+  # then we're at start of a wrapped line
+  return true
+]
+
+# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
+# the number of spaces at the start of the line containing 'curr'.
+def line-indent curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
+  local-scope
+  load-inputs
+  result:num <- copy 0
+  return-unless curr
+  at-start?:bool <- equal curr, start
+  return-if at-start?
+  {
+    curr <- prev curr
+    break-unless curr
+    at-start?:bool <- equal curr, start
+    break-if at-start?
+    c:char <- get *curr, value:offset
+    at-newline?:bool <- equal c, 10/newline
+    break-if at-newline?
+    # if c is a space, increment result
+    is-space?:bool <- equal c, 32/space
+    {
+      break-unless is-space?
+      result <- add result, 1
+    }
+    # if c is not a space, reset result
+    {
+      break-if is-space?
+      result <- copy 0
+    }
+    loop
+  }
+]
+
+scenario editor-moves-cursor-down-after-inserting-newline-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 1/left, 10/right
+  assume-console [
+    type [0
+1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    . 0        .
+    . 1abc     .
+    . ┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-clears-previous-line-completely-after-inserting-newline [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    press enter
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # line should be fully cleared
+  screen-should-contain [
+    .          .
+    .          .
+    .abcd↩     .
+    .e         .
+    .┈┈┈┈┈     .
+  ]
+]
+
+scenario editor-splits-wrapped-line-after-inserting-newline [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcdef], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    left-click 2, 0
+    press enter
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .ef        .
+    .┈┈┈┈┈     .
+  ]
+  memory-should-contain [
+    10 <- 2  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+]
+
+scenario editor-inserts-indent-after-newline [
+  local-scope
+  assume-screen 10/width, 10/height
+  s:text <- new [ab
+  cd
+ef]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  # position cursor after 'cd' and hit 'newline'
+  assume-console [
+    left-click 2, 8
+    type [
+]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor should be below start of previous line
+  memory-should-contain [
+    3 <- 3  # cursor row
+    4 <- 2  # cursor column (indented)
+  ]
+]
+
+scenario editor-skips-indent-around-paste [
+  local-scope
+  assume-screen 10/width, 10/height
+  s:text <- new [ab
+  cd
+ef]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  # position cursor after 'cd' and hit 'newline' surrounded by paste markers
+  assume-console [
+    left-click 2, 8
+    press 65507  # start paste
+    press enter
+    press 65506  # end paste
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor should be below start of previous line
+  memory-should-contain [
+    3 <- 3  # cursor row
+    4 <- 0  # cursor column (not indented)
+  ]
+]
+
+after <handle-special-key> [
+  {
+    paste-start?:bool <- equal k, 65507/paste-start
+    break-unless paste-start?
+    *editor <- put *editor, indent?:offset, false
+    return true/go-render
+  }
+]
+
+after <handle-special-key> [
+  {
+    paste-end?:bool <- equal k, 65506/paste-end
+    break-unless paste-end?
+    *editor <- put *editor, indent?:offset, true
+    return true/go-render
+  }
+]
+
+## helpers
+
+def draw-horizontal screen:&:screen, row:num, x:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  height:num <- screen-height screen
+  past-bottom?:bool <- greater-or-equal row, height
+  return-if past-bottom?
+  style:char, style-found?:bool <- next-input
+  {
+    break-if style-found?
+    style <- copy 9472/horizontal
+  }
+  color:num, color-found?:bool <- next-input
+  {
+    # default color to white
+    break-if color-found?
+    color <- copy 245/grey
+  }
+  bg-color:num, bg-color-found?:bool <- next-input
+  {
+    break-if bg-color-found?
+    bg-color <- copy 0/black
+  }
+  screen <- move-cursor screen, row, x
+  {
+    continue?:bool <- lesser-or-equal x, right  # right is inclusive, to match editor semantics
+    break-unless continue?
+    print screen, style, color, bg-color
+    x <- add x, 1
+    loop
+  }
+]
diff --git a/archive/2.vm/sandbox/003-shortcuts.mu b/archive/2.vm/sandbox/003-shortcuts.mu
new file mode 100644
index 00000000..c9e66d5b
--- /dev/null
+++ b/archive/2.vm/sandbox/003-shortcuts.mu
@@ -0,0 +1,2800 @@
+## special shortcuts for manipulating the editor
+# Some keys on the keyboard generate unicode characters, others generate
+# terminfo key codes. We need to modify different places in the two cases.
+
+# tab - insert two spaces
+
+scenario editor-inserts-two-spaces-on-tab [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [ab
+cd]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press tab
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .  ab      .
+    .cd        .
+  ]
+  # we render at most two editor rows worth (one row for each space)
+  check-trace-count-for-label-lesser-than 10, [print-character]
+]
+
+scenario editor-inserts-two-spaces-and-wraps-line-on-tab [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcd], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press tab
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .  ab↩     .
+    .cd        .
+  ]
+  # we re-render the whole editor
+  check-trace-count-for-label-greater-than 10, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    tab?:bool <- equal c, 9/tab
+    break-unless tab?
+    <begin-insert-character>
+    # todo: decompose insert-at-cursor into editor update and screen update,
+    # so that 'tab' doesn't render the current line multiple times
+    insert-at-cursor editor, 32/space, screen
+    go-render? <- insert-at-cursor editor, 32/space, screen
+    <end-insert-character>
+    return
+  }
+]
+
+# backspace - delete character before cursor
+
+scenario editor-handles-backspace-key [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 1
+    press backspace
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .bc        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 0
+  ]
+  check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
+]
+
+after <handle-special-character> [
+  {
+    delete-previous-character?:bool <- equal c, 8/backspace
+    break-unless delete-previous-character?
+    <begin-backspace-character>
+    go-render?:bool, backspaced-cell:&:duplex-list:char <- delete-before-cursor editor, screen
+    <end-backspace-character>
+    return
+  }
+]
+
+# return values:
+#   go-render? - whether caller needs to update the screen
+#   backspaced-cell - value deleted (or 0 if nothing was deleted) so we can save it for undo, etc.
+def delete-before-cursor editor:&:editor, screen:&:screen -> go-render?:bool, backspaced-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  data:&:duplex-list:char <- get *editor, data:offset
+  # if at start of text (before-cursor at § sentinel), return
+  prev:&:duplex-list:char <- prev before-cursor
+  return-unless prev, false/no-more-render, null/nothing-deleted
+  trace 10, [app], [delete-before-cursor]
+  original-row:num <- get *editor, cursor-row:offset
+  move-cursor-coordinates-left editor
+  backspaced-cell:&:duplex-list:char <- copy before-cursor
+  data <- remove before-cursor, data  # will also neatly trim next/prev pointers in backspaced-cell/before-cursor
+  before-cursor <- copy prev
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  screen-width:num <- screen-width screen
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  # did we just backspace over a newline?
+  same-row?:bool <- equal cursor-row, original-row
+  return-unless same-row?, true/go-render
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  curr:&:duplex-list:char <- next before-cursor
+  screen <- move-cursor screen, cursor-row, cursor-column
+  curr-column:num <- copy cursor-column
+  {
+    # hit right margin? give up and let caller render
+    at-right?:bool <- greater-or-equal curr-column, right
+    return-if at-right?, true/go-render
+    break-unless curr
+    # newline? done.
+    currc:char <- get *curr, value:offset
+    at-newline?:bool <- equal currc, 10/newline
+    break-if at-newline?
+    screen <- print screen, currc
+    curr-column <- add curr-column, 1
+    curr <- next curr
+    loop
+  }
+  # we're guaranteed not to be at the right margin
+  space:char <- copy 32/space
+  screen <- print screen, space
+  go-render? <- copy false
+]
+
+def move-cursor-coordinates-left editor:&:editor -> editor:&:editor [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  left:num <- get *editor, left:offset
+  # if not at left margin, move one character left
+  {
+    at-left-margin?:bool <- equal cursor-column, left
+    break-if at-left-margin?
+    trace 10, [app], [decrementing cursor column]
+    cursor-column <- subtract cursor-column, 1
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    return
+  }
+  # if at left margin, we must move to previous row:
+  top-of-screen?:bool <- equal cursor-row, 1  # exclude menu bar
+  {
+    break-if top-of-screen?
+    cursor-row <- subtract cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+  }
+  {
+    break-unless top-of-screen?
+    # no scroll, so do nothing
+  }
+  {
+    # case 1: if previous character was newline, figure out how long the previous line is
+    previous-character:char <- get *before-cursor, value:offset
+    previous-character-is-newline?:bool <- equal previous-character, 10/newline
+    break-unless previous-character-is-newline?
+    # compute length of previous line
+    trace 10, [app], [switching to previous line]
+    d:&:duplex-list:char <- get *editor, data:offset
+    end-of-line:num <- previous-line-length before-cursor, d
+    right:num <- get *editor, right:offset
+    width:num <- subtract right, left
+    wrap?:bool <- greater-than end-of-line, width
+    {
+      break-unless wrap?
+      _, column-offset:num <- divide-with-remainder end-of-line, width
+      cursor-column <- add left, column-offset
+      *editor <- put *editor, cursor-column:offset, cursor-column
+    }
+    {
+      break-if wrap?
+      cursor-column <- add left, end-of-line
+      *editor <- put *editor, cursor-column:offset, cursor-column
+    }
+    return
+  }
+  # case 2: if previous-character was not newline, we're just at a wrapped line
+  trace 10, [app], [wrapping to previous line]
+  right:num <- get *editor, right:offset
+  cursor-column <- subtract right, 1  # leave room for wrap icon
+  *editor <- put *editor, cursor-column:offset, cursor-column
+]
+
+# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
+# the length of the previous line before the 'curr' pointer.
+def previous-line-length curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
+  local-scope
+  load-inputs
+  result:num <- copy 0
+  return-unless curr
+  at-start?:bool <- equal curr, start
+  return-if at-start?
+  {
+    curr <- prev curr
+    break-unless curr
+    at-start?:bool <- equal curr, start
+    break-if at-start?
+    c:char <- get *curr, value:offset
+    at-newline?:bool <- equal c, 10/newline
+    break-if at-newline?
+    result <- add result, 1
+    loop
+  }
+]
+
+scenario editor-clears-last-line-on-backspace [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [ab
+cd]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  assume-console [
+    left-click 2, 0
+    press backspace
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 2
+  ]
+]
+
+scenario editor-joins-and-wraps-lines-on-backspace [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with two long-ish but non-wrapping lines
+  s:text <- new [abc def
+ghi jkl]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # position the cursor at the start of the second and hit backspace
+  assume-console [
+    left-click 2, 0
+    press backspace
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # resulting single line should wrap correctly
+  screen-should-contain [
+    .          .
+    .abc defgh↩.
+    .i jkl     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-wraps-long-lines-on-backspace [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor in part of the screen with a long line
+  e:&:editor <- new-editor [abc def ghij], 0/left, 8/right
+  editor-render screen, e
+  # confirm that it wraps
+  screen-should-contain [
+    .          .
+    .abc def↩  .
+    . ghij     .
+    .┈┈┈┈┈┈┈┈  .
+  ]
+  $clear-trace
+  # position the cursor somewhere in the middle of the top screen line and hit backspace
+  assume-console [
+    left-click 1, 4
+    press backspace
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # resulting single line should wrap correctly and not overflow its bounds
+  screen-should-contain [
+    .          .
+    .abcdef ↩  .
+    .ghij      .
+    .┈┈┈┈┈┈┈┈  .
+    .          .
+  ]
+]
+
+# delete - delete character at cursor
+
+scenario editor-handles-delete-key [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press delete
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .bc        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
+  $clear-trace
+  assume-console [
+    press delete
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .c         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 2, [print-character]  # new length to overwrite
+]
+
+after <handle-special-key> [
+  {
+    delete-next-character?:bool <- equal k, 65522/delete
+    break-unless delete-next-character?
+    <begin-delete-character>
+    go-render?:bool, deleted-cell:&:duplex-list:char <- delete-at-cursor editor, screen
+    <end-delete-character>
+    return
+  }
+]
+
+def delete-at-cursor editor:&:editor, screen:&:screen -> go-render?:bool, deleted-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  data:&:duplex-list:char <- get *editor, data:offset
+  deleted-cell:&:duplex-list:char <- next before-cursor
+  return-unless deleted-cell, false/don't-render
+  currc:char <- get *deleted-cell, value:offset
+  data <- remove deleted-cell, data
+  deleted-newline?:bool <- equal currc, 10/newline
+  return-if deleted-newline?, true/go-render
+  # wasn't a newline? render rest of line
+  curr:&:duplex-list:char <- next before-cursor  # refresh after remove above
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  screen <- move-cursor screen, cursor-row, cursor-column
+  curr-column:num <- copy cursor-column
+  screen-width:num <- screen-width screen
+  {
+    # hit right margin? give up and let caller render
+    at-right?:bool <- greater-or-equal curr-column, screen-width
+    return-if at-right?, true/go-render
+    break-unless curr
+    currc:char <- get *curr, value:offset
+    at-newline?:bool <- equal currc, 10/newline
+    break-if at-newline?
+    screen <- print screen, currc
+    curr-column <- add curr-column, 1
+    curr <- next curr
+    loop
+  }
+  # we're guaranteed not to be at the right margin
+  space:char <- copy 32/space
+  screen <- print screen, space
+  go-render? <- copy false
+]
+
+# right arrow
+
+scenario editor-moves-cursor-right-with-key [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press right-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .a0bc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 3, [print-character]  # 0 and following characters
+]
+
+after <handle-special-key> [
+  {
+    move-to-next-character?:bool <- equal k, 65514/right-arrow
+    break-unless move-to-next-character?
+    # if not at end of text
+    next-cursor:&:duplex-list:char <- next before-cursor
+    break-unless next-cursor
+    # scan to next character
+    <begin-move-cursor>
+    before-cursor <- copy next-cursor
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    go-render?:bool <- move-cursor-coordinates-right editor, screen-height
+    screen <- move-cursor screen, cursor-row, cursor-column
+    undo-coalesce-tag:num <- copy 2/right-arrow
+    <end-move-cursor>
+    return
+  }
+]
+
+def move-cursor-coordinates-right editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor before-cursor:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  # if crossed a newline, move cursor to start of next row
+  {
+    old-cursor-character:char <- get *before-cursor, value:offset
+    was-at-newline?:bool <- equal old-cursor-character, 10/newline
+    break-unless was-at-newline?
+    cursor-row <- add cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- copy left
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
+    return-unless below-screen?, false/don't-render
+    cursor-row <- subtract cursor-row, 1  # bring back into screen range
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    return true/go-render
+  }
+  # if the line wraps, move cursor to start of next row
+  {
+    # if we're at the column just before the wrap indicator
+    wrap-column:num <- subtract right, 1
+    at-wrap?:bool <- equal cursor-column, wrap-column
+    break-unless at-wrap?
+    # and if next character isn't newline
+    next:&:duplex-list:char <- next before-cursor
+    break-unless next
+    next-character:char <- get *next, value:offset
+    newline?:bool <- equal next-character, 10/newline
+    break-if newline?
+    cursor-row <- add cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- copy left
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
+    return-unless below-screen?, false/no-more-render
+    cursor-row <- subtract cursor-row, 1  # bring back into screen range
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    return true/go-render
+  }
+  # otherwise move cursor one character right
+  cursor-column <- add cursor-column, 1
+  *editor <- put *editor, cursor-column:offset, cursor-column
+  go-render? <- copy false
+]
+
+scenario editor-moves-cursor-to-next-line-with-right-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # type right-arrow a few times to get to start of second line
+  assume-console [
+    press right-arrow
+    press right-arrow
+    press right-arrow
+    press right-arrow  # next line
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  check-trace-count-for-label 0, [print-character]
+  # type something and ensure it goes where it should
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0d        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 2, [print-character]  # new length of second line
+]
+
+scenario editor-moves-cursor-to-next-line-with-right-arrow-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 1/left, 10/right
+  editor-render screen, e
+  assume-console [
+    press right-arrow
+    press right-arrow
+    press right-arrow
+    press right-arrow  # next line
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    . abc      .
+    . 0d       .
+    . ┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcdef], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 3
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  # line just barely wrapping
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  # position cursor at last character before wrap and hit right-arrow
+  assume-console [
+    left-click 1, 3
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  # now hit right arrow again
+  assume-console [
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcdef], 1/left, 6/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 4
+    press right-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    . abcd↩    .
+    . ef       .
+    . ┈┈┈┈┈    .
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # move to end of line, press right-arrow, type a character
+  assume-console [
+    left-click 1, 3
+    press right-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # new character should be in next line
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0d        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 2, [print-character]
+]
+
+# todo: ctrl-right: next word-end
+
+# left arrow
+
+scenario editor-moves-cursor-left-with-key [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 2
+    press left-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .a0bc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 3, [print-character]
+]
+
+after <handle-special-key> [
+  {
+    move-to-previous-character?:bool <- equal k, 65515/left-arrow
+    break-unless move-to-previous-character?
+    trace 10, [app], [left arrow]
+    # if not at start of text (before-cursor at § sentinel)
+    prev:&:duplex-list:char <- prev before-cursor
+    return-unless prev, false/don't-render
+    <begin-move-cursor>
+    move-cursor-coordinates-left editor
+    before-cursor <- copy prev
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    undo-coalesce-tag:num <- copy 1/left-arrow
+    <end-move-cursor>
+    return
+  }
+]
+
+scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with two lines
+  s:text <- new [abc
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # position cursor at start of second line (so there's no previous newline)
+  assume-console [
+    left-click 2, 0
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with three lines
+  s:text <- new [abc
+def
+g]
+  e:&:editor <- new-editor s:text, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # position cursor further down (so there's a newline before the character at
+  # the cursor)
+  assume-console [
+    left-click 3, 0
+    press left-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def0      .
+    .g         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  check-trace-count-for-label 1, [print-character]  # just the '0'
+]
+
+scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def
+g]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # position cursor at start of text, press left-arrow, then type a character
+  assume-console [
+    left-click 1, 0
+    press left-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # left-arrow should have had no effect
+  screen-should-contain [
+    .          .
+    .0abc      .
+    .def       .
+    .g         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  check-trace-count-for-label 4, [print-character]  # length of first line
+]
+
+scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-4 [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with text containing an empty line
+  s:text <- new [abc
+
+d]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e:&:editor
+  $clear-trace
+  # position cursor right after empty line
+  assume-console [
+    left-click 3, 0
+    press left-arrow
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0         .
+    .d         .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  check-trace-count-for-label 1, [print-character]  # just the '0'
+]
+
+scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with a wrapping line
+  e:&:editor <- new-editor [abcdef], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # position cursor right after empty line
+  assume-console [
+    left-click 2, 0
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1  # previous row
+    4 <- 3  # right margin except wrap icon
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-across-screen-lines-to-wrapping-line-with-left-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with a wrapping line followed by a second line
+  s:text <- new [abcdef
+g]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .ef        .
+    .g         .
+    .┈┈┈┈┈     .
+  ]
+  # position cursor right after empty line
+  assume-console [
+    left-click 3, 0
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2  # previous row
+    4 <- 2  # end of wrapped line
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-across-screen-lines-to-non-wrapping-line-with-left-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  # initialize editor with a line on the verge of wrapping, followed by a second line
+  s:text <- new [abcd
+e]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # position cursor right after empty line
+  assume-console [
+    left-click 2, 0
+    press left-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1  # previous row
+    4 <- 4  # end of wrapped line
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+# todo: ctrl-left: previous word-start
+
+# up arrow
+
+scenario editor-moves-to-previous-line-with-up-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 2, 1
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .a0bc      .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <handle-special-key> [
+  {
+    move-to-previous-line?:bool <- equal k, 65517/up-arrow
+    break-unless move-to-previous-line?
+    <begin-move-cursor>
+    move-to-previous-line editor
+    undo-coalesce-tag:num <- copy 3/up-arrow
+    <end-move-cursor>
+    return
+  }
+]
+
+def move-to-previous-line editor:&:editor -> editor:&:editor [
+  local-scope
+  load-inputs
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  already-at-top?:bool <- lesser-or-equal cursor-row, 1/top
+  {
+    # if cursor not at top, move it
+    break-if already-at-top?
+    # if not at newline, move to start of line (previous newline)
+    # then scan back another line
+    # if either step fails, give up without modifying cursor or coordinates
+    curr:&:duplex-list:char <- copy before-cursor
+    old:&:duplex-list:char <- copy curr
+    {
+      at-left?:bool <- equal cursor-column, left
+      break-if at-left?
+      curr <- before-previous-screen-line curr, editor
+      no-motion?:bool <- equal curr, old
+      return-if no-motion?
+    }
+    {
+      curr <- before-previous-screen-line curr, editor
+      no-motion?:bool <- equal curr, old
+      return-if no-motion?
+    }
+    before-cursor <- copy curr
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    cursor-row <- subtract cursor-row, 1
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    # scan ahead to right column or until end of line
+    target-column:num <- copy cursor-column
+    cursor-column <- copy left
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    {
+      done?:bool <- greater-or-equal cursor-column, target-column
+      break-if done?
+      curr:&:duplex-list:char <- next before-cursor
+      break-unless curr
+      currc:char <- get *curr, value:offset
+      at-newline?:bool <- equal currc, 10/newline
+      break-if at-newline?
+      #
+      before-cursor <- copy curr
+      *editor <- put *editor, before-cursor:offset, before-cursor
+      cursor-column <- add cursor-column, 1
+      *editor <- put *editor, cursor-column:offset, cursor-column
+      loop
+    }
+  }
+]
+
+# Takes a pointer into the doubly-linked list, scans back to before start of
+# previous *wrapped* line.
+# Returns original if no next newline.
+# Beware: never return null pointer.
+def before-previous-screen-line in:&:duplex-list:char, editor:&:editor -> out:&:duplex-list:char [
+  local-scope
+  load-inputs
+  curr:&:duplex-list:char <- copy in
+  c:char <- get *curr, value:offset
+  # compute max, number of characters to skip
+  #   1 + len%(width-1)
+  #   except rotate second term to vary from 1 to width-1 rather than 0 to width-2
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  max-line-length:num <- subtract right, left, -1/exclusive-right, 1/wrap-icon
+  sentinel:&:duplex-list:char <- get *editor, data:offset
+  len:num <- previous-line-length curr, sentinel
+  {
+    break-if len
+    # empty line; just skip this newline
+    prev:&:duplex-list:char <- prev curr
+    return-unless prev, curr
+    return prev
+  }
+  _, max:num <- divide-with-remainder len, max-line-length
+  # remainder 0 => scan one width-worth
+  {
+    break-if max
+    max <- copy max-line-length
+  }
+  max <- add max, 1
+  count:num <- copy 0
+  # skip 'max' characters
+  {
+    done?:bool <- greater-or-equal count, max
+    break-if done?
+    prev:&:duplex-list:char <- prev curr
+    break-unless prev
+    curr <- copy prev
+    count <- add count, 1
+    loop
+  }
+  return curr
+]
+
+scenario editor-adjusts-column-at-previous-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [ab
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 2, 3
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .ab0       .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-adjusts-column-at-empty-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 2, 3
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-moves-to-previous-line-from-zero-margin [
+  local-scope
+  assume-screen 10/width, 5/height
+  # start out with three lines
+  s:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # click on the third line and hit up-arrow, so you end up just after a newline
+  assume-console [
+    left-click 3, 0
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0def      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-moves-to-previous-line-from-left-margin [
+  local-scope
+  assume-screen 10/width, 5/height
+  # start out with three lines
+  s:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor s, 1/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # click on the third line and hit up-arrow, so you end up just after a newline
+  assume-console [
+    left-click 3, 1
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    . abc      .
+    . 0def     .
+    . ghi      .
+    . ┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-moves-to-top-line-in-presence-of-wrapped-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abcde], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd↩     .
+    .e         .
+    .┈┈┈┈┈     .
+  ]
+  $clear-trace
+  assume-console [
+    left-click 2, 0
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0abc↩     .
+    .de        .
+    .┈┈┈┈┈     .
+  ]
+]
+
+scenario editor-moves-to-top-line-in-presence-of-wrapped-line-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+defgh]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abc       .
+    .defg↩     .
+    .h         .
+    .┈┈┈┈┈     .
+  ]
+  $clear-trace
+  assume-console [
+    left-click 3, 0
+    press up-arrow
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0abc      .
+    .defg↩     .
+    .h         .
+    .┈┈┈┈┈     .
+  ]
+]
+
+# down arrow
+
+scenario editor-moves-to-next-line-with-down-arrow [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [abc
+def]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # cursor starts out at (1, 0)
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # ..and ends at (2, 0)
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .0def      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <handle-special-key> [
+  {
+    move-to-next-line?:bool <- equal k, 65516/down-arrow
+    break-unless move-to-next-line?
+    <begin-move-cursor>
+    move-to-next-line editor, screen-height
+    undo-coalesce-tag:num <- copy 4/down-arrow
+    <end-move-cursor>
+    return
+  }
+]
+
+def move-to-next-line editor:&:editor, screen-height:num -> editor:&:editor [
+  local-scope
+  load-inputs
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  last-line:num <- subtract screen-height, 1
+  bottom:num <- get *editor, bottom:offset
+  at-bottom-of-screen?:bool <- greater-or-equal bottom, last-line
+  return-unless before-cursor
+  next:&:duplex-list:char <- next before-cursor
+  return-unless next
+  already-at-bottom?:bool <- greater-or-equal cursor-row, last-line
+    # if cursor not at bottom, move it
+    return-if already-at-bottom?
+    target-column:num <- copy cursor-column
+    # scan to start of next line
+    {
+      next:&:duplex-list:char <- next before-cursor
+      break-unless next
+      done?:bool <- greater-or-equal cursor-column, right
+      break-if done?
+      cursor-column <- add cursor-column, 1
+      before-cursor <- copy next
+      c:char <- get *next, value:offset
+      at-newline?:bool <- equal c, 10/newline
+      break-if at-newline?
+      loop
+    }
+    return-unless next
+    cursor-row <- add cursor-row, 1
+    cursor-column <- copy left
+    {
+      next:&:duplex-list:char <- next before-cursor
+      break-unless next
+      c:char <- get *next, value:offset
+      at-newline?:bool <- equal c, 10/newline
+      break-if at-newline?
+      done?:bool <- greater-or-equal cursor-column, target-column
+      break-if done?
+      cursor-column <- add cursor-column, 1
+      before-cursor <- copy next
+      loop
+    }
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    *editor <- put *editor, cursor-row:offset, cursor-row
+]
+
+scenario editor-adjusts-column-at-next-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # second line is shorter than first
+  s:text <- new [abcde
+fg
+hi]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # move to end of first line, then press down
+  assume-console [
+    left-click 1, 8
+    press down-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor doesn't go vertically down, it goes to end of shorter line
+  memory-should-contain [
+    3 <- 2
+    4 <- 2
+  ]
+  check-trace-count-for-label 0, [print-character]
+  assume-console [
+    type [0]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abcde     .
+    .fg0       .
+    .hi        .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+# ctrl-a/home - move cursor to start of line
+
+scenario editor-moves-to-start-of-line-with-ctrl-a [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line, press ctrl-a
+  assume-console [
+    left-click 2, 3
+    press ctrl-a
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of line
+  memory-should-contain [
+    4 <- 2
+    5 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    move-to-start-of-line?:bool <- equal c, 1/ctrl-a
+    break-unless move-to-start-of-line?
+    <begin-move-cursor>
+    move-to-start-of-screen-line editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return false/don't-render
+  }
+]
+
+after <handle-special-key> [
+  {
+    move-to-start-of-line?:bool <- equal k, 65521/home
+    break-unless move-to-start-of-line?
+    <begin-move-cursor>
+    move-to-start-of-screen-line editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return false/don't-render
+  }
+]
+
+# handles wrapped lines
+# precondition: cursor-column should be in a consistent state
+def move-to-start-of-screen-line editor:&:editor -> editor:&:editor [
+  local-scope
+  load-inputs
+  # update cursor column
+  left:num <- get *editor, left:offset
+  col:num <- get *editor, cursor-column:offset
+  # update before-cursor
+  curr:&:duplex-list:char <- get *editor, before-cursor:offset
+  # while not at start of line, move
+  {
+    done?:bool <- equal col, left
+    break-if done?
+    assert curr, [move-to-start-of-line tried to move before start of text]
+    curr <- prev curr
+    col <- subtract col, 1
+    loop
+  }
+  *editor <- put *editor, cursor-column:offset, col
+  *editor <- put *editor, before-cursor:offset, curr
+]
+
+scenario editor-moves-to-start-of-line-with-ctrl-a-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line (no newline before), press ctrl-a
+  assume-console [
+    left-click 1, 3
+    press ctrl-a
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of line
+  memory-should-contain [
+    4 <- 1
+    5 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-start-of-line-with-home [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  $clear-trace
+  # start on second line, press 'home'
+  assume-console [
+    left-click 2, 3
+    press home
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of line
+  memory-should-contain [
+    3 <- 2
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-start-of-line-with-home-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line (no newline before), press 'home'
+  assume-console [
+    left-click 1, 3
+    press home
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of line
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-start-of-screen-line-with-ctrl-a [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [123456], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .1234↩     .
+    .56        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  $clear-trace
+  # start on second line, press ctrl-a then up
+  assume-console [
+    left-click 2, 1
+    press ctrl-a
+    press up-arrow
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to start of first line
+  memory-should-contain [
+    4 <- 1  # cursor-row
+    5 <- 0  # cursor-column
+  ]
+  check-trace-count-for-label 0, [print-character]
+  # make sure before-cursor is in sync
+  assume-console [
+    type [a]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .a123↩     .
+    .456       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1  # cursor-row
+    5 <- 1  # cursor-column
+  ]
+]
+
+# ctrl-e/end - move cursor to end of line
+
+scenario editor-moves-to-end-of-line-with-ctrl-e [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line, press ctrl-e
+  assume-console [
+    left-click 1, 1
+    press ctrl-e
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    4 <- 1
+    5 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+  # editor inserts future characters at cursor
+  assume-console [
+    type [z]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 4
+  ]
+  screen-should-contain [
+    .          .
+    .123z      .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 1, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    move-to-end-of-line?:bool <- equal c, 5/ctrl-e
+    break-unless move-to-end-of-line?
+    <begin-move-cursor>
+    move-to-end-of-line editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return false/don't-render
+  }
+]
+
+after <handle-special-key> [
+  {
+    move-to-end-of-line?:bool <- equal k, 65520/end
+    break-unless move-to-end-of-line?
+    <begin-move-cursor>
+    move-to-end-of-line editor
+    undo-coalesce-tag:num <- copy 0/never
+    <end-move-cursor>
+    return false/don't-render
+  }
+]
+
+def move-to-end-of-line editor:&:editor -> editor:&:editor [
+  local-scope
+  load-inputs
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  right:num <- get *editor, right:offset
+  # while not at end of line, move
+  {
+    next:&:duplex-list:char <- next before-cursor
+    break-unless next  # end of text
+    nextc:char <- get *next, value:offset
+    at-end-of-line?:bool <- equal nextc, 10/newline
+    break-if at-end-of-line?
+    cursor-column <- add cursor-column, 1
+    at-right?:bool <- equal cursor-column, right
+    break-if at-right?
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    before-cursor <- copy next
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    loop
+  }
+]
+
+scenario editor-moves-to-end-of-line-with-ctrl-e-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line (no newline after), press ctrl-e
+  assume-console [
+    left-click 2, 1
+    press ctrl-e
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    4 <- 2
+    5 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-end-of-line-with-end [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line, press 'end'
+  assume-console [
+    left-click 1, 1
+    press end
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-end-of-line-with-end-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line (no newline after), press 'end'
+  assume-console [
+    left-click 2, 1
+    press end
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    3 <- 2
+    4 <- 3
+  ]
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-moves-to-end-of-wrapped-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123456
+789]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line, press 'end'
+  assume-console [
+    left-click 1, 1
+    press end
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves to end of line
+  memory-should-contain [
+    10 <- 1
+    11 <- 3
+  ]
+  # no prints
+  check-trace-count-for-label 0, [print-character]
+  # before-cursor is also consistent
+  assume-console [
+    type [a]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .123a↩     .
+    .456       .
+    .789       .
+    .┈┈┈┈┈     .
+  ]
+]
+
+# ctrl-u - delete text from start of line until (but not at) cursor
+
+scenario editor-deletes-to-start-of-line-with-ctrl-u [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line, press ctrl-u
+  assume-console [
+    left-click 2, 2
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to start of line
+  screen-should-contain [
+    .          .
+    .123       .
+    .6         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    delete-to-start-of-line?:bool <- equal c, 21/ctrl-u
+    break-unless delete-to-start-of-line?
+    <begin-delete-to-start-of-line>
+    deleted-cells:&:duplex-list:char <- delete-to-start-of-line editor
+    <end-delete-to-start-of-line>
+    go-render?:bool <- minimal-render-for-ctrl-u screen, editor, deleted-cells
+    return
+  }
+]
+
+def minimal-render-for-ctrl-u screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
+  local-scope
+  load-inputs
+  curr-column:num <- get *editor, cursor-column:offset
+  # accumulate the current line as text and render it
+  buf:&:buffer:char <- new-buffer 30  # accumulator for the text we need to render
+  curr:&:duplex-list:char <- get *editor, before-cursor:offset
+  i:num <- copy curr-column
+  right:num <- get *editor, right:offset
+  {
+    # if we have a wrapped line, give up and render the whole screen
+    wrap?:bool <- greater-or-equal i, right
+    return-if wrap?, true/go-render
+    curr <- next curr
+    break-unless curr
+    c:char <- get *curr, value:offset
+    b:bool <- equal c, 10
+    break-if b
+    buf <- append buf, c
+    i <- add i, 1
+    loop
+  }
+  # if the line used to be wrapped, give up and render the whole screen
+  num-deleted-cells:num <- length deleted-cells
+  old-row-len:num <- add i, num-deleted-cells
+  left:num <- get *editor, left:offset
+  end:num <- subtract right, left
+  wrap?:bool <- greater-or-equal old-row-len, end
+  return-if wrap?, true/go-render
+  curr-line:text <- buffer-to-array buf
+  curr-row:num <- get *editor, cursor-row:offset
+  render-code screen, curr-line, curr-column, right, curr-row
+  return false/dont-render
+]
+
+def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
+  local-scope
+  load-inputs
+  # compute range to delete
+  init:&:duplex-list:char <- get *editor, data:offset
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  start:&:duplex-list:char <- copy before-cursor
+  end:&:duplex-list:char <- next before-cursor
+  {
+    at-start-of-text?:bool <- equal start, init
+    break-if at-start-of-text?
+    curr:char <- get *start, value:offset
+    at-start-of-line?:bool <- equal curr, 10/newline
+    break-if at-start-of-line?
+    start <- prev start
+    assert start, [delete-to-start-of-line tried to move before start of text]
+    loop
+  }
+  # snip it out
+  result:&:duplex-list:char <- next start
+  remove-between start, end
+  # adjust cursor
+  before-cursor <- copy start
+  *editor <- put *editor, before-cursor:offset, before-cursor
+  left:num <- get *editor, left:offset
+  *editor <- put *editor, cursor-column:offset, left
+  # if the line wrapped before, we may need to adjust cursor-row as well
+  right:num <- get *editor, right:offset
+  width:num <- subtract right, left
+  num-deleted:num <- length result
+  cursor-row-adjustment:num <- divide-with-remainder num-deleted, width
+  return-unless cursor-row-adjustment
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-row <- subtract cursor-row, cursor-row-adjustment
+  put *editor, cursor-row:offset, cursor-row
+]
+
+def render-code screen:&:screen, s:text, left:num, right:num, row:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  return-unless s
+  color:num <- copy 7/white
+  column:num <- copy left
+  screen <- move-cursor screen, row, column
+  screen-height:num <- screen-height screen
+  i:num <- copy 0
+  len:num <- length *s
+  {
+    +next-character
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    done? <- greater-or-equal row, screen-height
+    break-if done?
+    c:char <- index *s, i
+    <character-c-received>
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # clear rest of line in this window
+      {
+        done?:bool <- greater-than column, right
+        break-if done?
+        space:char <- copy 32/space
+        print screen, space
+        column <- add column, 1
+        loop
+      }
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      i <- add i, 1
+      loop +next-character
+    }
+    {
+      # at right? wrap.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment i
+      loop +next-character
+    }
+    i <- add i, 1
+    print screen, c, color
+    column <- add column, 1
+    loop
+  }
+  was-at-left?:bool <- equal column, left
+  clear-line-until screen, right
+  {
+    break-if was-at-left?
+    row <- add row, 1
+  }
+  move-cursor screen, row, left
+]
+
+scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line (no newline before), press ctrl-u
+  assume-console [
+    left-click 1, 2
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to start of line
+  screen-should-contain [
+    .          .
+    .3         .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start past end of line, press ctrl-u
+  assume-console [
+    left-click 1, 3
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to start of line
+  screen-should-contain [
+    .          .
+    .          .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-deletes-to-start-of-final-line-with-ctrl-u [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start past end of final line, press ctrl-u
+  assume-console [
+    left-click 2, 3
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to start of line
+  screen-should-contain [
+    .          .
+    .123       .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u [
+  local-scope
+  assume-screen 10/width, 10/height
+  # first line starts out wrapping
+  s:text <- new [123456
+789]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .1234↩     .
+    .56        .
+    .789       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  $clear-trace
+  # ctrl-u enough of the first line that it's no longer wrapping
+  assume-console [
+    left-click 1, 3
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # entire screen needs to be refreshed
+  screen-should-contain [
+    .          .
+    .456       .
+    .789       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  check-trace-count-for-label 45, [print-character]
+]
+
+# sometimes hitting ctrl-u needs to adjust the cursor row
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-2 [
+  local-scope
+  assume-screen 10/width, 10/height
+  # third line starts out wrapping
+  s:text <- new [1
+2
+345678
+9]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .1         .
+    .2         .
+    .3456↩     .
+    .78        .
+    .9         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # position cursor on screen line after the wrap and hit ctrl-u
+  assume-console [
+    left-click 4, 1  # on '8'
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .1         .
+    .2         .
+    .8         .
+    .9         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # cursor moves up one screen line
+  memory-should-contain [
+    10 <- 3  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+]
+
+# line wrapping twice (taking up 3 screen lines)
+scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-3 [
+  local-scope
+  assume-screen 10/width, 10/height
+  # third line starts out wrapping
+  s:text <- new [1
+2
+3456789abcd
+e]
+  e:&:editor <- new-editor s, 0/left, 5/right
+  editor-render screen, e
+  assume-console [
+    left-click 4, 1  # on '8'
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .1         .
+    .2         .
+    .3456↩     .
+    .789a↩     .
+    .bcd       .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  assume-console [
+    left-click 5, 1
+    press ctrl-u
+  ]
+  run [
+    editor-event-loop screen, console, e
+    10:num/raw <- get *e, cursor-row:offset
+    11:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .1         .
+    .2         .
+    .cd        .
+    .e         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  # make sure we adjusted cursor-row
+  memory-should-contain [
+    10 <- 3  # cursor-row
+    11 <- 0  # cursor-column
+  ]
+]
+
+# ctrl-k - delete text from cursor to end of line (but not the newline)
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on first line, press ctrl-k
+  assume-console [
+    left-click 1, 1
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to end of line
+  screen-should-contain [
+    .          .
+    .1         .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 9, [print-character]
+]
+
+after <handle-special-character> [
+  {
+    delete-to-end-of-line?:bool <- equal c, 11/ctrl-k
+    break-unless delete-to-end-of-line?
+    <begin-delete-to-end-of-line>
+    deleted-cells:&:duplex-list:char <- delete-to-end-of-line editor
+    <end-delete-to-end-of-line>
+    # checks if we can do a minimal render and if we can it will do a minimal render
+    go-render?:bool <- minimal-render-for-ctrl-k screen, editor, deleted-cells
+    return
+  }
+]
+
+def minimal-render-for-ctrl-k screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
+  local-scope
+  load-inputs
+  # if we deleted nothing, there's nothing to render
+  return-unless deleted-cells, false/dont-render
+  # if the line used to wrap before, give up and render the whole screen
+  curr-column:num <- get *editor, cursor-column:offset
+  num-deleted-cells:num <- length deleted-cells
+  old-row-len:num <- add curr-column, num-deleted-cells
+  left:num <- get *editor, left:offset
+  right:num <- get *editor, right:offset
+  end:num <- subtract right, left
+  wrap?:bool <- greater-or-equal old-row-len, end
+  return-if wrap?, true/go-render
+  clear-line-until screen, right
+  return false/dont-render
+]
+
+def delete-to-end-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
+  local-scope
+  load-inputs
+  # compute range to delete
+  start:&:duplex-list:char <- get *editor, before-cursor:offset
+  end:&:duplex-list:char <- next start
+  {
+    at-end-of-text?:bool <- equal end, null
+    break-if at-end-of-text?
+    curr:char <- get *end, value:offset
+    at-end-of-line?:bool <- equal curr, 10/newline
+    break-if at-end-of-line?
+    end <- next end
+    loop
+  }
+  # snip it out
+  result <- next start
+  remove-between start, end
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start on second line (no newline after), press ctrl-k
+  assume-console [
+    left-click 2, 1
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes to end of line
+  screen-should-contain [
+    .          .
+    .123       .
+    .4         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 9, [print-character]
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start at end of line
+  assume-console [
+    left-click 1, 2
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes just last character
+  screen-should-contain [
+    .          .
+    .12        .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 8, [print-character]
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start past end of line
+  assume-console [
+    left-click 1, 3
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes nothing
+  screen-should-contain [
+    .          .
+    .123       .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 7, [print-character]
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start at end of text
+  assume-console [
+    left-click 2, 2
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes just the final character
+  screen-should-contain [
+    .          .
+    .123       .
+    .45        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 8, [print-character]
+]
+
+scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [
+  local-scope
+  assume-screen 10/width, 5/height
+  s:text <- new [123
+456]
+  e:&:editor <- new-editor s, 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  # start past end of text
+  assume-console [
+    left-click 2, 3
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor deletes nothing
+  screen-should-contain [
+    .          .
+    .123       .
+    .456       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # no prints necessary
+  check-trace-count-for-label 0, [print-character]
+]
+
+scenario editor-deletes-to-end-of-wrapped-line-with-ctrl-k [
+  local-scope
+  assume-screen 10/width, 5/height
+  # create an editor with the first line wrapping to a second screen row
+  s:text <- new [1234
+567]
+  e:&:editor <- new-editor s, 0/left, 4/right
+  editor-render screen, e
+  $clear-trace
+  # delete all of the first wrapped line
+  assume-console [
+    press ctrl-k
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # screen shows an empty unwrapped first line
+  screen-should-contain [
+    .          .
+    .          .
+    .567       .
+    .┈┈┈┈      .
+    .          .
+  ]
+  # entire screen is refreshed
+  check-trace-count-for-label 16, [print-character]
+]
+
+# takes a pointer into the doubly-linked list, scans ahead at most 'max'
+# positions until the next newline
+# returns original if no next newline
+# beware: never return null pointer.
+def before-start-of-next-line original:&:duplex-list:char, max:num -> curr:&:duplex-list:char [
+  local-scope
+  load-inputs
+  count:num <- copy 0
+  curr:&:duplex-list:char <- copy original
+  # skip the initial newline if it exists
+  {
+    c:char <- get *curr, value:offset
+    at-newline?:bool <- equal c, 10/newline
+    break-unless at-newline?
+    curr <- next curr
+    count <- add count, 1
+  }
+  {
+    return-unless curr, original
+    done?:bool <- greater-or-equal count, max
+    break-if done?
+    c:char <- get *curr, value:offset
+    at-newline?:bool <- equal c, 10/newline
+    break-if at-newline?
+    curr <- next curr
+    count <- add count, 1
+    loop
+  }
+  return-unless curr, original
+  return curr
+]
+
+# ctrl-/ - comment/uncomment current line
+
+after <handle-special-character> [
+  {
+    comment-toggle?:bool <- equal c, 31/ctrl-slash
+    break-unless comment-toggle?
+    cursor-column:num <- get *editor, cursor-column:offset
+    data:&:duplex-list:char <- get *editor, data:offset
+    <begin-insert-character>
+    before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
+    line-start:&:duplex-list:char <- next before-line-start
+    commented-out?:bool <- match line-start, [#? ]  # comment prefix
+    {
+      break-unless commented-out?
+      # uncomment
+      data <- remove line-start, 3/length-comment-prefix, data
+      cursor-column <- subtract cursor-column, 3/length-comment-prefix
+      *editor <- put *editor, cursor-column:offset, cursor-column
+      go-render? <- render-line-from-start screen, editor, 3/size-of-comment-leader
+    }
+    {
+      break-if commented-out?
+      # comment
+      insert before-line-start, [#? ]
+      cursor-column <- add cursor-column, 3/length-comment-prefix
+      *editor <- put *editor, cursor-column:offset, cursor-column
+      go-render? <- render-line-from-start screen, editor, 0
+    }
+    <end-insert-character>
+    return
+  }
+]
+
+# Render just from the start of the current line, and only if it wasn't
+# wrapping before (include margin) and isn't wrapping now. Otherwise just tell
+# the caller to go-render? the entire screen.
+def render-line-from-start screen:&:screen, editor:&:editor, right-margin:num -> go-render?:bool, screen:&:screen [
+  local-scope
+  load-inputs
+  before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
+  line-start:&:duplex-list:char <- next before-line-start
+  color:num <- copy 7/white
+  left:num <- get *editor, left:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  screen <- move-cursor screen, cursor-row, left
+  right:num <- get *editor, right:offset
+  end:num <- subtract right, right-margin
+  i:num <- copy 0
+  curr:&:duplex-list:char <- copy line-start
+  {
+    render-all?:bool <- greater-or-equal i, end
+    return-if render-all?, true/go-render
+    break-unless curr
+    c:char <- get *curr, value:offset
+    newline?:bool <- equal c, 10/newline
+    break-if newline?
+    color <- get-color color, c
+    print screen, c, color
+    curr <- next curr
+    i <- add i, 1
+    loop
+  }
+  clear-line-until screen, right
+  return false/dont-render
+]
+
+def before-start-of-screen-line editor:&:editor -> result:&:duplex-list:char [
+  local-scope
+  load-inputs
+  cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  {
+    next:&:duplex-list:char <- next cursor
+    break-unless next
+    cursor <- copy next
+  }
+  result <- before-previous-screen-line cursor, editor
+]
+
+scenario editor-comments-empty-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 5/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .#?        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 3
+  ]
+  check-trace-count-for-label 5, [print-character]
+]
+
+scenario editor-comments-at-start-of-contents [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [ab], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .#? ab     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 3
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-comments-at-end-of-contents [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [ab], 0/left, 10/right
+  editor-render screen, e
+  $clear-trace
+  assume-console [
+    left-click 1, 7
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .#? ab     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    4 <- 1
+    5 <- 5
+  ]
+  check-trace-count-for-label 10, [print-character]
+  # toggle to uncomment
+  $clear-trace
+  assume-console [
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+    4:num/raw <- get *e, cursor-row:offset
+    5:num/raw <- get *e, cursor-column:offset
+  ]
+  screen-should-contain [
+    .          .
+    .ab        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  check-trace-count-for-label 10, [print-character]
+]
+
+scenario editor-comments-almost-wrapping-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # editor starts out with a non-wrapping line
+  e:&:editor <- new-editor [abcd], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .abcd      .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  $clear-trace
+  # on commenting the line is now wrapped
+  assume-console [
+    left-click 1, 7
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .#? a↩     .
+    .bcd       .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+]
+
+scenario editor-uncomments-just-wrapping-line [
+  local-scope
+  assume-screen 10/width, 5/height
+  # editor starts out with a comment that wraps the line
+  e:&:editor <- new-editor [#? ab], 0/left, 5/right
+  editor-render screen, e
+  screen-should-contain [
+    .          .
+    .#? a↩     .
+    .b         .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+  $clear-trace
+  # on uncommenting the line is no longer wrapped
+  assume-console [
+    left-click 1, 7
+    press ctrl-slash
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .ab        .
+    .┈┈┈┈┈     .
+    .          .
+  ]
+]
diff --git a/archive/2.vm/sandbox/004-programming-environment.mu b/archive/2.vm/sandbox/004-programming-environment.mu
new file mode 100644
index 00000000..1454144b
--- /dev/null
+++ b/archive/2.vm/sandbox/004-programming-environment.mu
@@ -0,0 +1,268 @@
+## putting the environment together out of editors
+
+def! main [
+  local-scope
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  env:&:environment <- new-programming-environment null/filesystem, null/screen
+  render-all null/screen, env, render
+  event-loop null/screen, null/console, env, null/filesystem
+]
+
+container environment [
+  current-sandbox:&:editor
+]
+
+def new-programming-environment resources:&:resources, screen:&:screen, test-sandbox-editor-contents:text -> result:&:environment [
+  local-scope
+  load-inputs
+  width:num <- screen-width screen
+  result <- new environment:type
+  # sandbox editor
+  current-sandbox:&:editor <- new-editor test-sandbox-editor-contents, 0/left, width/right
+  *result <- put *result, current-sandbox:offset, current-sandbox
+  <programming-environment-initialization>
+]
+
+def event-loop screen:&:screen, console:&:console, env:&:environment, resources:&:resources -> screen:&:screen, console:&:console, env:&:environment, resources:&:resources [
+  local-scope
+  load-inputs
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  # if we fall behind we'll stop updating the screen, but then we have to
+  # render the entire screen when we catch up.
+  # todo: test this
+  render-all-on-no-more-events?:bool <- copy false
+  {
+    # looping over each (keyboard or touch) event as it occurs
+    +next-event
+    e:event, found?:bool, quit?:bool, console <- read-event console
+    loop-unless found?
+    break-if quit?  # only in tests
+    trace 10, [app], [next-event]
+    <handle-event>
+    # check for global events that will trigger regardless of which editor has focus
+    {
+      k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
+      break-unless is-keycode?
+      <global-keypress>
+    }
+    {
+      c:char, is-unicode?:bool <- maybe-convert e:event, text:variant
+      break-unless is-unicode?
+      <global-type>
+    }
+    # 'touch' event
+    {
+      t:touch-event, is-touch?:bool <- maybe-convert e:event, touch:variant
+      break-unless is-touch?
+      # ignore all but 'left-click' events for now
+      # todo: test this
+      touch-type:num <- get t, type:offset
+      is-left-click?:bool <- equal touch-type, 65513/mouse-left
+      loop-unless is-left-click?, +next-event
+      click-row:num <- get t, row:offset
+      click-column:num <- get t, column:offset
+      # later exceptions for non-editor touches will go here
+      <global-touch>
+      move-cursor current-sandbox, screen, t
+      screen <- update-cursor screen, current-sandbox, env
+      loop +next-event
+    }
+    # 'resize' event - redraw editor
+    # todo: test this after supporting resize in assume-console
+    {
+      r:resize-event, is-resize?:bool <- maybe-convert e:event, resize:variant
+      break-unless is-resize?
+      env, screen <- resize screen, env
+      screen <- render-all screen, env, render-without-moving-cursor
+      loop +next-event
+    }
+    # not global and not a touch event
+    {
+      render?:bool <- handle-keyboard-event screen, current-sandbox, e:event
+      # try to batch up rendering if there are more events queued up
+      render-all-on-no-more-events? <- or render-all-on-no-more-events?, render?
+      more-events?:bool <- has-more-events? console
+      {
+        break-if more-events?
+        break-unless render-all-on-no-more-events?
+        render-all-on-no-more-events? <- copy false
+        screen <- render-all screen, env, render
+      }
+      screen <- update-cursor screen, current-sandbox, env
+    }
+    loop
+  }
+]
+
+def resize screen:&:screen, env:&:environment -> env:&:environment, screen:&:screen [
+  local-scope
+  load-inputs
+  clear-screen screen  # update screen dimensions
+  width:num <- screen-width screen
+  # update sandbox editor
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  right:num <- subtract width, 1
+  *current-sandbox <- put *current-sandbox right:offset, right
+  # reset cursor
+  *current-sandbox <- put *current-sandbox, cursor-row:offset, 1
+  *current-sandbox <- put *current-sandbox, cursor-column:offset, 0
+]
+
+# Variant of 'render' that updates cursor-row and cursor-column based on
+# before-cursor (rather than the other way around). If before-cursor moves
+# off-screen, it resets cursor-row and cursor-column.
+def render-without-moving-cursor screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
+  local-scope
+  load-inputs
+  return-unless editor, 1/top, 0/left
+  left:num <- get *editor, left:offset
+  screen-height:num <- screen-height screen
+  right:num <- get *editor, right:offset
+  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
+  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
+  curr <- next curr
+  color:num <- copy 7/white
+  row:num <- copy 1/top
+  column:num <- copy left
+  # save before-cursor
+  old-before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  # initialze cursor-row/cursor-column/before-cursor to the top of the screen
+  # by default
+  *editor <- put *editor, cursor-row:offset, row
+  *editor <- put *editor, cursor-column:offset, column
+  top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
+  *editor <- put *editor, before-cursor:offset, top-of-screen
+  screen <- move-cursor screen, row, column
+  {
+    +next-character
+    break-unless curr
+    off-screen?:bool <- greater-or-equal row, screen-height
+    break-if off-screen?
+    # if we find old-before-cursor still on the new resized screen, update
+    # editor.cursor-row and editor.cursor-column based on
+    # old-before-cursor
+    {
+      at-cursor?:bool <- equal old-before-cursor, prev
+      break-unless at-cursor?
+      *editor <- put *editor, cursor-row:offset, row
+      *editor <- put *editor, cursor-column:offset, column
+      *editor <- put *editor, before-cursor:offset, old-before-cursor
+    }
+    c:char <- get *curr, value:offset
+    <character-c-received>
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # clear rest of line in this window
+      clear-line-until screen, right
+      # skip to next line
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      curr <- next curr
+      prev <- next prev
+      loop +next-character
+    }
+    {
+      # at right? wrap. even if there's only one more letter left; we need
+      # room for clicking on the cursor after it.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment curr
+      loop +next-character
+    }
+    print screen, c, color
+    curr <- next curr
+    prev <- next prev
+    column <- add column, 1
+    loop
+  }
+  # save first character off-screen
+  *editor <- put *editor, bottom-of-screen:offset, curr
+  *editor <- put *editor, bottom:offset, row
+  return row, column
+]
+
+type render-recipe = (recipe (address screen) (address editor) -> number number (address screen) (address editor))
+
+def render-all screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
+  local-scope
+  load-inputs
+  trace 10, [app], [render all]
+  # top menu
+  trace 11, [app], [render top menu]
+  width:num <- screen-width screen
+  draw-horizontal screen, 0, 0/left, width, 32/space, 0/black, 238/grey
+  button-start:num <- subtract width, 20
+  button-on-screen?:bool <- greater-or-equal button-start, 0
+  assert button-on-screen?, [screen too narrow for menu]
+  screen <- move-cursor screen, 0/row, button-start
+  print screen, [ run (F4) ], 255/white, 161/reddish
+  #
+  screen <- render-sandbox-side screen, env, render-editor
+  <end-render-components>  # no early returns permitted
+  #
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  screen <- update-cursor screen, current-sandbox, env
+]
+
+# replaced in a later layer
+def render-sandbox-side screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
+  local-scope
+  load-inputs
+  trace 11, [app], [render sandboxes]
+  old-top-idx:num <- save-top-idx screen
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  left:num <- get *current-sandbox, left:offset
+  right:num <- get *current-sandbox, right:offset
+  row:num, column:num, screen, current-sandbox <- call render-editor, screen, current-sandbox
+  # draw solid line after code (you'll see why in later layers)
+  draw-horizontal screen, row, left, right
+  row <- add row, 1
+  clear-screen-from screen, row, left, left, right
+  #
+  assert-no-scroll screen, old-top-idx
+]
+
+def update-cursor screen:&:screen, current-sandbox:&:editor, env:&:environment -> screen:&:screen [
+  local-scope
+  load-inputs
+  <update-cursor-special-cases>
+  cursor-row:num <- get *current-sandbox, cursor-row:offset
+  cursor-column:num <- get *current-sandbox, cursor-column:offset
+  screen <- move-cursor screen, cursor-row, cursor-column
+]
+
+scenario backspace-over-text [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 15/height
+  # recipes.mu is empty
+  assume-resources [
+  ]
+  # sandbox editor contains an instruction without storing outputs
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # run the code in the editors
+  assume-console [
+    type [a]
+    press backspace
+  ]
+  run [
+    event-loop screen, console, env, resources
+    10:num/raw <- get *screen, cursor-row:offset
+    11:num/raw <- get *screen, cursor-column:offset
+  ]
+  memory-should-contain [
+    10 <- 1
+    11 <- 0
+  ]
+]
diff --git a/archive/2.vm/sandbox/005-sandbox.mu b/archive/2.vm/sandbox/005-sandbox.mu
new file mode 100644
index 00000000..632a5df1
--- /dev/null
+++ b/archive/2.vm/sandbox/005-sandbox.mu
@@ -0,0 +1,1081 @@
+## running code from the editor and creating sandboxes
+#
+# Running code in the sandbox editor prepends its contents to a list of
+# (non-editable) sandboxes below the editor, showing the result and maybe a
+# few other things (later layers).
+#
+# This layer draws the menubar buttons in non-editable sandboxes but they
+# don't do anything yet. Later layers implement each button.
+
+def! main [
+  local-scope
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  env:&:environment <- new-programming-environment null/filesystem, null/screen
+  env <- restore-sandboxes env, null/filesystem
+  render-all null/screen, env, render
+  event-loop null/screen, null/console, env, null/filesystem
+]
+
+container environment [
+  sandbox:&:sandbox  # list of sandboxes, from top to bottom. TODO: switch to &:list:sandbox
+  render-from:num
+  number-of-sandboxes:num
+]
+
+after <programming-environment-initialization> [
+  *result <- put *result, render-from:offset, -1
+]
+
+container sandbox [
+  data:text
+  response:text
+  # coordinates to track clicks
+  # constraint: will be 0 for sandboxes at positions before env.render-from
+  starting-row-on-screen:num
+  code-ending-row-on-screen:num  # past end of code
+  screen:&:screen  # prints in the sandbox go here
+  next-sandbox:&:sandbox
+]
+
+scenario run-and-show-results [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 15/height
+  # recipes.mu is empty
+  assume-resources [
+  ]
+  # sandbox editor contains an instruction without storing outputs
+  env:&:environment <- new-programming-environment resources, screen, [divide-with-remainder 11, 3]
+  render-all screen, env, render
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints the results
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .divide-with-remainder 11, 3                       .
+    .3                                                 .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  screen-should-contain-in-color 7/white, [
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .divide-with-remainder 11, 3                       .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+  screen-should-contain-in-color 245/grey, [
+    .                                                  .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+    .3                                                 .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # run another command
+  assume-console [
+    left-click 1, 80
+    type [add 2, 2]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints both sandboxes
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .divide-with-remainder 11, 3                       .
+    .3                                                 .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+after <global-keypress> [
+  # F4? load all code and run all sandboxes.
+  {
+    do-run?:bool <- equal k, 65532/F4
+    break-unless do-run?
+    screen <- update-status screen, [running...       ], 245/grey
+    error?:bool <- run-sandboxes env, resources, screen
+    # F4 might update warnings and results on both sides
+    screen <- render-all screen, env, render
+    {
+      break-if error?
+      screen <- update-status screen, [                 ], 245/grey
+    }
+    screen <- update-cursor screen, current-sandbox, env
+    loop +next-event
+  }
+]
+
+def run-sandboxes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, resources:&:resources, screen:&:screen [
+  local-scope
+  load-inputs
+  errors-found?:bool <- update-recipes env, resources, screen
+  # check contents of editor
+  <begin-run-sandboxes>
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  {
+    sandbox-contents:text <- editor-contents current-sandbox
+    break-unless sandbox-contents
+    # if contents exist, first save them
+    # run them and turn them into a new sandbox
+    new-sandbox:&:sandbox <- new sandbox:type
+    *new-sandbox <- put *new-sandbox, data:offset, sandbox-contents
+    # push to head of sandbox list
+    dest:&:sandbox <- get *env, sandbox:offset
+    *new-sandbox <- put *new-sandbox, next-sandbox:offset, dest
+    *env <- put *env, sandbox:offset, new-sandbox
+    # update sandbox count
+    sandbox-count:num <- get *env, number-of-sandboxes:offset
+    sandbox-count <- add sandbox-count, 1
+    *env <- put *env, number-of-sandboxes:offset, sandbox-count
+    # save all sandboxes
+    # needs to be before running them, in case we die when running
+    save-sandboxes env, resources
+    # clear sandbox editor
+    init:&:duplex-list:char <- push 167/§, null
+    *current-sandbox <- put *current-sandbox, data:offset, init
+    *current-sandbox <- put *current-sandbox, top-of-screen:offset, init
+  }
+  # run all sandboxes
+  curr:&:sandbox <- get *env, sandbox:offset
+  idx:num <- copy 0
+  {
+    break-unless curr
+    curr <- update-sandbox curr, env, idx
+    curr <- get *curr, next-sandbox:offset
+    idx <- add idx, 1
+    loop
+  }
+  <end-run-sandboxes>
+  {
+    break-if resources  # ignore this in tests
+    $system [./snapshot_lesson]
+  }
+]
+
+# load code from disk
+# replaced in a later layer (whereupon errors-found? will actually be set)
+def update-recipes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, screen:&:screen [
+  local-scope
+  load-inputs
+  in:text <- slurp resources, [lesson/recipes.mu]
+  reload in
+  errors-found? <- copy false
+]
+
+# replaced in a later layer
+def update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  response:text, _, fake-screen:&:screen <- run-sandboxed data
+  *sandbox <- put *sandbox, response:offset, response
+  *sandbox <- put *sandbox, screen:offset, fake-screen
+]
+
+def update-status screen:&:screen, msg:text, color:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  screen <- move-cursor screen, 0, 2
+  screen <- print screen, msg, color, 238/grey/background
+]
+
+def save-sandboxes env:&:environment, resources:&:resources -> resources:&:resources [
+  local-scope
+  load-inputs
+  trace 11, [app], [save sandboxes]
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  # first clear previous versions, in case we deleted some sandbox
+  $system [rm lesson/[0-9]* >/dev/null 2>/dev/null]  # some shells can't handle '>&'
+  curr:&:sandbox <- get *env, sandbox:offset
+  idx:num <- copy 0
+  {
+    break-unless curr
+    resources <- save-sandbox resources, curr, idx
+    idx <- add idx, 1
+    curr <- get *curr, next-sandbox:offset
+    loop
+  }
+]
+
+def save-sandbox resources:&:resources, sandbox:&:sandbox, sandbox-index:num -> resources:&:resources [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  filename:text <- append [lesson/], sandbox-index
+  resources <- dump resources, filename, data
+  <end-save-sandbox>
+]
+
+def! render-sandbox-side screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
+  local-scope
+  load-inputs
+  trace 11, [app], [render sandbox side]
+  old-top-idx:num <- save-top-idx screen
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  row:num, column:num <- copy 1, 0
+  left:num <- get *current-sandbox, left:offset
+  right:num <- get *current-sandbox, right:offset
+  # render sandbox editor
+  render-from:num <- get *env, render-from:offset
+  {
+    render-current-sandbox?:bool <- equal render-from, -1
+    break-unless render-current-sandbox?
+    row, column, screen, current-sandbox <- call render-editor, screen, current-sandbox
+  }
+  # render sandboxes
+  draw-horizontal screen, row, left, right
+  sandbox:&:sandbox <- get *env, sandbox:offset
+  row, screen <- render-sandboxes screen, sandbox, left, right, row, render-from, 0, env
+  clear-rest-of-screen screen, row, left, right
+  #
+  assert-no-scroll screen, old-top-idx
+]
+
+def render-sandboxes screen:&:screen, sandbox:&:sandbox, left:num, right:num, row:num, render-from:num, idx:num -> row:num, screen:&:screen, sandbox:&:sandbox [
+  local-scope
+  load-inputs
+  env:&:environment, _/optional <- next-input
+  return-unless sandbox
+  screen-height:num <- screen-height screen
+  hidden?:bool <- lesser-than idx, render-from
+  {
+    break-if hidden?
+    # render sandbox menu
+    row <- add row, 1
+    at-bottom?:bool <- greater-or-equal row, screen-height
+    return-if at-bottom?
+    screen <- move-cursor screen, row, left
+    screen <- render-sandbox-menu screen, idx, left, right
+    # save menu row so we can detect clicks to it later
+    *sandbox <- put *sandbox, starting-row-on-screen:offset, row
+    # render sandbox contents
+    row <- add row, 1
+    screen <- move-cursor screen, row, left
+    sandbox-data:text <- get *sandbox, data:offset
+    row, screen <- render-code screen, sandbox-data, left, right, row
+    *sandbox <- put *sandbox, code-ending-row-on-screen:offset, row
+    # render sandbox warnings, screen or response, in that order
+    sandbox-response:text <- get *sandbox, response:offset
+    <render-sandbox-results>
+    {
+      sandbox-screen:&:screen <- get *sandbox, screen:offset
+      empty-screen?:bool <- fake-screen-is-empty? sandbox-screen
+      break-if empty-screen?
+      row, screen <- render-screen screen, sandbox-screen, left, right, row
+    }
+    {
+      break-unless empty-screen?
+      <render-sandbox-response>
+      row, screen <- render-text screen, sandbox-response, left, right, 245/grey, row
+    }
+    +render-sandbox-end
+    at-bottom?:bool <- greater-or-equal row, screen-height
+    return-if at-bottom?
+    # draw solid line after sandbox
+    draw-horizontal screen, row, left, right
+  }
+  # if hidden, reset row attributes
+  {
+    break-unless hidden?
+    *sandbox <- put *sandbox, starting-row-on-screen:offset, 0
+    *sandbox <- put *sandbox, code-ending-row-on-screen:offset, 0
+    <end-render-sandbox-reset-hidden>
+  }
+  # draw next sandbox
+  next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
+  next-idx:num <- add idx, 1
+  row, screen <- render-sandboxes screen, next-sandbox, left, right, row, render-from, next-idx, env
+]
+
+def render-sandbox-menu screen:&:screen, sandbox-index:num, left:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  move-cursor-to-column screen, left
+  edit-button-left:num, edit-button-right:num, copy-button-left:num, copy-button-right:num, delete-button-left:num <- sandbox-menu-columns left, right
+  print screen, sandbox-index, 232/dark-grey, 245/grey
+  start-buttons:num <- subtract edit-button-left, 1
+  clear-line-until screen, start-buttons, 245/grey
+  print screen, [edit], 232/black, 25/background-blue
+  clear-line-until screen, edit-button-right, 25/background-blue
+  print screen, [copy], 232/black, 58/background-green
+  clear-line-until screen, copy-button-right, 58/background-green
+  print screen, [delete], 232/black, 52/background-red
+  clear-line-until screen, right, 52/background-red
+]
+
+scenario skip-rendering-sandbox-menu-past-bottom-row [
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 6/height
+  # recipes.mu is empty
+  assume-resources [
+    [lesson/0] <- [|add 2, 2|]
+    [lesson/1] <- [|add 1, 1|]
+  ]
+  # create two sandboxes such that the top one just barely fills the screen
+  env:&:environment <- new-programming-environment resources, screen, []
+  env <- restore-sandboxes env, resources
+  run [
+    render-all screen, env, render
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .──────────────────────────────────────────────────.
+  ]
+]
+
+# divide up the menu bar for a sandbox into 3 segments, for edit/copy/delete buttons
+# delete-button-right == right
+# all left/right pairs are inclusive
+def sandbox-menu-columns left:num, right:num -> edit-button-left:num, edit-button-right:num, copy-button-left:num, copy-button-right:num, delete-button-left:num [
+  local-scope
+  load-inputs
+  start-buttons:num <- add left, 4/space-for-sandbox-index
+  buttons-space:num <- subtract right, start-buttons
+  button-width:num <- divide-with-remainder buttons-space, 3  # integer division
+  buttons-wide-enough?:bool <- greater-or-equal button-width, 8
+  assert buttons-wide-enough?, [sandbox must be at least 30 or so characters wide]
+  edit-button-left:num <- copy start-buttons
+  copy-button-left:num <- add start-buttons, button-width
+  edit-button-right:num <- subtract copy-button-left, 1
+  delete-button-left:num <- subtract right, button-width
+  copy-button-right:num <- subtract delete-button-left, 1
+]
+
+# print a text 's' to 'editor' in 'color' starting at 'row'
+# clear rest of last line, move cursor to next line
+def render-text screen:&:screen, s:text, left:num, right:num, color:num, row:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  return-unless s
+  column:num <- copy left
+  screen <- move-cursor screen, row, column
+  screen-height:num <- screen-height screen
+  i:num <- copy 0
+  len:num <- length *s
+  {
+    +next-character
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    done? <- greater-or-equal row, screen-height
+    break-if done?
+    c:char <- index *s, i
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # clear rest of line in this window
+      {
+        done?:bool <- greater-than column, right
+        break-if done?
+        space:char <- copy 32/space
+        print screen, space
+        column <- add column, 1
+        loop
+      }
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      i <- add i, 1
+      loop +next-character
+    }
+    {
+      # at right? wrap.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment i
+      loop +next-character
+    }
+    i <- add i, 1
+    print screen, c, color
+    column <- add column, 1
+    loop
+  }
+  was-at-left?:bool <- equal column, left
+  clear-line-until screen, right
+  {
+    break-if was-at-left?
+    row <- add row, 1
+  }
+  move-cursor screen, row, left
+]
+
+scenario render-text-wraps-barely-long-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  run [
+    render-text screen, [abcde], 0/left, 4/right, 7/white, 1/row
+  ]
+  screen-should-contain [
+    .     .
+    .abcd↩.
+    .e    .
+    .     .
+  ]
+]
+
+# assumes programming environment has no sandboxes; restores them from previous session
+def restore-sandboxes env:&:environment, resources:&:resources -> env:&:environment [
+  local-scope
+  load-inputs
+  # read all scenarios, pushing them to end of a list of scenarios
+  idx:num <- copy 0
+  curr:&:sandbox <- copy null
+  prev:&:sandbox <- copy null
+  {
+    filename:text <- append [lesson/], idx
+    contents:text <- slurp resources, filename
+    break-unless contents  # stop at first error; assuming file didn't exist
+                           # todo: handle empty sandbox
+    # create new sandbox for file
+    curr <- new sandbox:type
+    *curr <- put *curr, data:offset, contents
+    <end-restore-sandbox>
+    {
+      break-if idx
+      *env <- put *env, sandbox:offset, curr
+    }
+    {
+      break-unless idx
+      *prev <- put *prev, next-sandbox:offset, curr
+    }
+    idx <- add idx, 1
+    prev <- copy curr
+    loop
+  }
+  # update sandbox count
+  *env <- put *env, number-of-sandboxes:offset, idx
+]
+
+# print the fake sandbox screen to 'screen' with appropriate delimiters
+# leave cursor at start of next line
+def render-screen screen:&:screen, sandbox-screen:&:screen, left:num, right:num, row:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  return-unless sandbox-screen
+  # print 'screen:'
+  row <- render-text screen, [screen:], left, right, 245/grey, row
+  screen <- move-cursor screen, row, left
+  # start printing sandbox-screen
+  column:num <- copy left
+  s-width:num <- screen-width sandbox-screen
+  s-height:num <- screen-height sandbox-screen
+  buf:&:@:screen-cell <- get *sandbox-screen, data:offset
+  stop-printing:num <- add left, s-width, 3
+  max-column:num <- min stop-printing, right
+  i:num <- copy 0
+  len:num <- length *buf
+  screen-height:num <- screen-height screen
+  {
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    done? <- greater-or-equal row, screen-height
+    break-if done?
+    column <- copy left
+    screen <- move-cursor screen, row, column
+    # initial leader for each row: two spaces and a '.'
+    space:char <- copy 32/space
+    print screen, space, 245/grey
+    print screen, space, 245/grey
+    full-stop:char <- copy 46/period
+    print screen, full-stop, 245/grey
+    column <- add left, 3
+    {
+      # print row
+      row-done?:bool <- greater-or-equal column, max-column
+      break-if row-done?
+      curr:screen-cell <- index *buf, i
+      c:char <- get curr, contents:offset
+      color:num <- get curr, color:offset
+      {
+        # damp whites down to grey
+        white?:bool <- equal color, 7/white
+        break-unless white?
+        color <- copy 245/grey
+      }
+      print screen, c, color
+      column <- add column, 1
+      i <- add i, 1
+      loop
+    }
+    # print final '.'
+    print screen, full-stop, 245/grey
+    column <- add column, 1
+    {
+      # clear rest of current line
+      line-done?:bool <- greater-than column, right
+      break-if line-done?
+      print screen, space
+      column <- add column, 1
+      loop
+    }
+    row <- add row, 1
+    loop
+  }
+]
+
+scenario run-updates-results [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 12/height
+  # define a recipe (no indent for the 'add' line below so column numbers are more obvious)
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      ||
+      |recipe foo [|
+      |  local-scope|
+      |  z:num <- add 2, 2|
+      |  reply z|
+      |]|
+    ]
+  ]
+  # sandbox editor contains an instruction without storing outputs
+  env:&:environment <- new-programming-environment resources, screen, [foo]  # contents of sandbox editor
+  render-all screen, env, render
+  $clear-trace
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # the new sandbox should be saved to disk
+  trace-should-contain [
+    app: save sandboxes
+  ]
+  # make a change (incrementing one of the args to 'add'), then rerun
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      ||
+      |recipe foo [|
+      |  local-scope|
+      |  z:num <- add 2, 3|
+      |  reply z|
+      |]|
+    ]
+  ]
+  $clear-trace
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen updates the result on the right
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .5                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # no need to save sandboxes all over again
+  trace-should-not-contain [
+    app: save sandboxes
+  ]
+]
+
+scenario run-instruction-manages-screen-per-sandbox [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # empty recipes
+  assume-resources [
+  ]
+  # sandbox editor contains an instruction
+  env:&:environment <- new-programming-environment resources, screen, [print screen, 4]  # contents of sandbox editor
+  render-all screen, env, render
+  # run the code in the editor
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that it prints a little toy screen
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .print screen, 4                                   .
+    .screen:                                           .
+    .  .4                             .                .
+    .  .                              .                .
+    .  .                              .                .
+    .  .                              .                .
+    .  .                              .                .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+def editor-contents editor:&:editor -> result:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 80
+  curr:&:duplex-list:char <- get *editor, data:offset
+  # skip § sentinel
+  assert curr, [editor without data is illegal; must have at least a sentinel]
+  curr <- next curr
+  return-unless curr, null
+  {
+    break-unless curr
+    c:char <- get *curr, value:offset
+    buf <- append buf, c
+    curr <- next curr
+    loop
+  }
+  result <- buffer-to-array buf
+]
+
+scenario editor-provides-edited-contents [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  assume-console [
+    left-click 1, 2
+    type [def]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    s:text <- editor-contents e
+    1:@:char/raw <- copy *s
+  ]
+  memory-should-contain [
+    1:array:character <- [abdefc]
+  ]
+]
+
+# scrolling through sandboxes
+
+scenario scrolling-down-past-bottom-of-sandbox-editor [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # initialize
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
+  render-all screen, env, render
+  assume-console [
+    # create a sandbox
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-down'
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .␣   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-up'
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor displays again
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+# page-down updates render-from to scroll sandboxes
+after <global-keypress> [
+  {
+    page-down?:bool <- equal k, 65518/page-down
+    break-unless page-down?
+    sandbox:&:sandbox <- get *env, sandbox:offset
+    break-unless sandbox
+    # slide down if possible
+    {
+      render-from:num <- get *env, render-from:offset
+      number-of-sandboxes:num <- get *env, number-of-sandboxes:offset
+      max:num <- subtract number-of-sandboxes, 1
+      at-end?:bool <- greater-or-equal render-from, max
+      break-if at-end?
+      render-from <- add render-from, 1
+      *env <- put *env, render-from:offset, render-from
+    }
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, current-sandbox, env
+    loop +next-event
+  }
+]
+
+# update-cursor takes render-from into account
+after <update-cursor-special-cases> [
+  {
+    render-from:num <- get *env, render-from:offset
+    scrolling?:bool <- greater-or-equal render-from, 0
+    break-unless scrolling?
+    cursor-column:num <- get *current-sandbox, left:offset
+    screen <- move-cursor screen, 2/row, cursor-column  # highlighted sandbox will always start at row 2
+    return
+  }
+]
+
+# 'page-up' is like 'page-down': updates first-sandbox-to-render when necessary
+after <global-keypress> [
+  {
+    page-up?:bool <- equal k, 65519/page-up
+    break-unless page-up?
+    render-from:num <- get *env, render-from:offset
+    at-beginning?:bool <- equal render-from, -1
+    break-if at-beginning?
+    render-from <- subtract render-from, 1
+    *env <- put *env, render-from:offset, render-from
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, current-sandbox, env
+    loop +next-event
+  }
+]
+
+# sandbox belonging to 'env' whose next-sandbox is 'in'
+# return null if there's no such sandbox, either because 'in' doesn't exist in 'env', or because it's the first sandbox
+def previous-sandbox env:&:environment, in:&:sandbox -> out:&:sandbox [
+  local-scope
+  load-inputs
+  curr:&:sandbox <- get *env, sandbox:offset
+  return-unless curr, null
+  next:&:sandbox <- get *curr, next-sandbox:offset
+  {
+    return-unless next, null
+    found?:bool <- equal next, in
+    break-if found?
+    curr <- copy next
+    next <- get *curr, next-sandbox:offset
+    loop
+  }
+  return curr
+]
+
+scenario scrolling-through-multiple-sandboxes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  cursor:char <- copy 9251/␣
+  print screen, cursor
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-down'
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .␣   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-down' again
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # just second sandbox displayed
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-down' again
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # no change
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-up'
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # back to displaying both sandboxes without editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # back to displaying both sandboxes as well as editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # no change
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario scrolling-manages-sandbox-index-correctly [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create a sandbox
+  assume-console [
+    press ctrl-n
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-down' and 'page-up' a couple of times. sandbox index should be stable
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # back to displaying both sandboxes as well as editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # hit 'page-down'
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .  # no change
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
diff --git a/archive/2.vm/sandbox/006-sandbox-copy.mu b/archive/2.vm/sandbox/006-sandbox-copy.mu
new file mode 100644
index 00000000..0eae6cf7
--- /dev/null
+++ b/archive/2.vm/sandbox/006-sandbox-copy.mu
@@ -0,0 +1,286 @@
+## the 'copy' button makes it easy to duplicate a sandbox, and thence to
+## see code operate in multiple situations
+
+scenario copy-a-sandbox-to-editor [
+  local-scope
+  trace-until 50/app  # trace too long
+  assume-screen 50/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+  # click at left edge of 'copy' button
+  assume-console [
+    left-click 3, 19
+  ]
+  run [
+    event-loop screen, console, env
+  ]
+  # it copies into editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .add 1, 1                                          .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .0add 1, 1                                         .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+]
+
+scenario copy-a-sandbox-to-editor-2 [
+  local-scope
+  trace-until 50/app  # trace too long
+  assume-screen 50/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+  # click at right edge of 'copy' button (just before 'delete')
+  assume-console [
+    left-click 3, 33
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # it copies into editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .add 1, 1                                          .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .0add 1, 1                                         .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+]
+
+after <global-touch> [
+  # support 'copy' button
+  {
+    copy?:bool <- should-attempt-copy? click-row, click-column, env
+    break-unless copy?
+    copy?, env <- try-copy-sandbox click-row, env
+    break-unless copy?
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, current-sandbox, env
+    loop +next-event
+  }
+]
+
+# some preconditions for attempting to copy a sandbox
+def should-attempt-copy? click-row:num, click-column:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  # are we below the sandbox editor?
+  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, env
+  return-unless click-sandbox-area?, false
+  # narrower, is the click in the columns spanning the 'copy' button?
+  first-sandbox:&:editor <- get *env, current-sandbox:offset
+  assert first-sandbox, [!!]
+  sandbox-left-margin:num <- get *first-sandbox, left:offset
+  sandbox-right-margin:num <- get *first-sandbox, right:offset
+  _, _, copy-button-left:num, copy-button-right:num, _ <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
+  copy-button-vertical-area?:bool <- within-range? click-column, copy-button-left, copy-button-right
+  return-unless copy-button-vertical-area?, false
+  # finally, is sandbox editor empty?
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  result <- empty-editor? current-sandbox
+]
+
+def try-copy-sandbox click-row:num, env:&:environment -> clicked-on-copy-button?:bool, env:&:environment [
+  local-scope
+  load-inputs
+  # identify the sandbox to copy, if the click was actually on the 'copy' button
+  sandbox:&:sandbox <- find-sandbox env, click-row
+  return-unless sandbox, false
+  clicked-on-copy-button? <- copy true
+  text:text <- get *sandbox, data:offset
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  current-sandbox <- insert-text current-sandbox, text
+  # reset scroll
+  *env <- put *env, render-from:offset, -1
+]
+
+def find-sandbox env:&:environment, click-row:num -> result:&:sandbox [
+  local-scope
+  load-inputs
+  curr-sandbox:&:sandbox <- get *env, sandbox:offset
+  {
+    break-unless curr-sandbox
+    start:num <- get *curr-sandbox, starting-row-on-screen:offset
+    found?:bool <- equal click-row, start
+    return-if found?, curr-sandbox
+    curr-sandbox <- get *curr-sandbox, next-sandbox:offset
+    loop
+  }
+  return null/not-found
+]
+
+def click-on-sandbox-area? click-row:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  first-sandbox:&:sandbox <- get *env, sandbox:offset
+  return-unless first-sandbox, false
+  first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
+  result <- greater-or-equal click-row, first-sandbox-begins
+]
+
+def empty-editor? editor:&:editor -> result:bool [
+  local-scope
+  load-inputs
+  head:&:duplex-list:char <- get *editor, data:offset
+  first:&:duplex-list:char <- next head
+  result <- not first
+]
+
+def within-range? x:num, low:num, high:num -> result:bool [
+  local-scope
+  load-inputs
+  not-too-far-left?:bool <- greater-or-equal x, low
+  not-too-far-right?:bool <- lesser-or-equal x, high
+  result <- and not-too-far-left? not-too-far-right?
+]
+
+scenario copy-fails-if-sandbox-editor-not-empty [
+  local-scope
+  trace-until 50/app  # trace too long
+  assume-screen 50/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # type something into the sandbox editor, then click on the 'copy' button
+  assume-console [
+    left-click 2, 20  # put cursor in sandbox editor
+    type [0]  # type something
+    left-click 3, 20  # click 'copy' button
+  ]
+  run [
+    event-loop screen, console, env
+  ]
+  # copy doesn't happen
+  screen-should-contain [
+    .                               run (F4)           .
+    .0                                                 .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .01                                                .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
diff --git a/archive/2.vm/sandbox/007-sandbox-delete.mu b/archive/2.vm/sandbox/007-sandbox-delete.mu
new file mode 100644
index 00000000..01f01f42
--- /dev/null
+++ b/archive/2.vm/sandbox/007-sandbox-delete.mu
@@ -0,0 +1,345 @@
+## deleting sandboxes
+
+scenario deleting-sandboxes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 15/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # run a few commands
+  assume-console [
+    type [divide-with-remainder 11, 3]
+    press F4
+    type [add 2, 2]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .divide-with-remainder 11, 3                       .
+    .3                                                 .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # delete second sandbox by clicking on left edge of 'delete' button
+  assume-console [
+    left-click 7, 34
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # delete first sandbox by clicking at right edge of 'delete' button
+  assume-console [
+    left-click 3, 49
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+after <global-touch> [
+  # support 'delete' button
+  {
+    delete?:bool <- should-attempt-delete? click-row, click-column, env
+    break-unless delete?
+    delete?, env <- try-delete-sandbox click-row, env
+    break-unless delete?
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, current-sandbox, env
+    loop +next-event
+  }
+]
+
+# some preconditions for attempting to delete a sandbox
+def should-attempt-delete? click-row:num, click-column:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  # are we below the sandbox editor?
+  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, env
+  return-unless click-sandbox-area?, false
+  # narrower, is the click in the columns spanning the 'copy' button?
+  first-sandbox:&:editor <- get *env, current-sandbox:offset
+  assert first-sandbox, [!!]
+  sandbox-left-margin:num <- get *first-sandbox, left:offset
+  sandbox-right-margin:num <- get *first-sandbox, right:offset
+  _, _, _, _, delete-button-left:num <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
+  result <- within-range? click-column, delete-button-left, sandbox-right-margin
+]
+
+def try-delete-sandbox click-row:num, env:&:environment -> clicked-on-delete-button?:bool, env:&:environment [
+  local-scope
+  load-inputs
+  # identify the sandbox to delete, if the click was actually on the 'delete' button
+  sandbox:&:sandbox <- find-sandbox env, click-row
+  return-unless sandbox, false
+  clicked-on-delete-button? <- copy true
+  env <- delete-sandbox env, sandbox
+]
+
+def delete-sandbox env:&:environment, sandbox:&:sandbox -> env:&:environment [
+  local-scope
+  load-inputs
+  curr-sandbox:&:sandbox <- get *env, sandbox:offset
+  first-sandbox?:bool <- equal curr-sandbox, sandbox
+  {
+    # first sandbox? pop
+    break-unless first-sandbox?
+    next-sandbox:&:sandbox <- get *curr-sandbox, next-sandbox:offset
+    *env <- put *env, sandbox:offset, next-sandbox
+  }
+  {
+    # not first sandbox?
+    break-if first-sandbox?
+    prev-sandbox:&:sandbox <- copy curr-sandbox
+    curr-sandbox <- get *curr-sandbox, next-sandbox:offset
+    {
+      assert curr-sandbox, [sandbox not found! something is wrong.]
+      found?:bool <- equal curr-sandbox, sandbox
+      break-if found?
+      prev-sandbox <- copy curr-sandbox
+      curr-sandbox <- get *curr-sandbox, next-sandbox:offset
+      loop
+    }
+    # snip sandbox out of its list
+    next-sandbox:&:sandbox <- get *curr-sandbox, next-sandbox:offset
+    *prev-sandbox <- put *prev-sandbox, next-sandbox:offset, next-sandbox
+  }
+  # update sandbox count
+  sandbox-count:num <- get *env, number-of-sandboxes:offset
+  sandbox-count <- subtract sandbox-count, 1
+  *env <- put *env, number-of-sandboxes:offset, sandbox-count
+  # reset scroll if deleted sandbox was last
+  {
+    break-if next-sandbox
+    render-from:num <- get *env, render-from:offset
+    reset-scroll?:bool <- equal render-from, sandbox-count
+    break-unless reset-scroll?
+    *env <- put *env, render-from:offset, -1
+  }
+]
+
+scenario deleting-sandbox-after-scroll [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press page-down
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 6, 34
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario deleting-top-sandbox-after-scroll [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press page-down
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 2, 34
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario deleting-final-sandbox-after-scroll [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press page-down
+    press page-down
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 2, 34
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # implicitly scroll up to first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario deleting-updates-sandbox-count [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+  ]
+  # delete the second sandbox, then try to scroll down twice
+  assume-console [
+    left-click 3, 34
+    press page-down
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # shouldn't go past last sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
diff --git a/archive/2.vm/sandbox/008-sandbox-edit.mu b/archive/2.vm/sandbox/008-sandbox-edit.mu
new file mode 100644
index 00000000..fb3981bf
--- /dev/null
+++ b/archive/2.vm/sandbox/008-sandbox-edit.mu
@@ -0,0 +1,319 @@
+## editing sandboxes after they've been created
+
+scenario clicking-on-sandbox-edit-button-moves-it-to-editor [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # click at left edge of 'edit' button
+  assume-console [
+    left-click 3, 4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # it pops back into editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .add 2, 2                                          .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .0add 2, 2                                         .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario clicking-on-sandbox-edit-button-moves-it-to-editor-2 [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  # empty recipes
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # click at right edge of 'edit' button (just before 'copy')
+  assume-console [
+    left-click 3, 18
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # it pops back into editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .add 2, 2                                          .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [0]
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .0add 2, 2                                         .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+after <global-touch> [
+  # support 'edit' button
+  {
+    edit?:bool <- should-attempt-edit? click-row, click-column, env
+    break-unless edit?
+    edit?, env <- try-edit-sandbox click-row, env
+    break-unless edit?
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, current-sandbox, env
+    loop +next-event
+  }
+]
+
+# some preconditions for attempting to edit a sandbox
+def should-attempt-edit? click-row:num, click-column:num, env:&:environment -> result:bool [
+  local-scope
+  load-inputs
+  # are we below the sandbox editor?
+  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, env
+  return-unless click-sandbox-area?, false
+  # narrower, is the click in the columns spanning the 'edit' button?
+  first-sandbox:&:editor <- get *env, current-sandbox:offset
+  assert first-sandbox, [!!]
+  sandbox-left-margin:num <- get *first-sandbox, left:offset
+  sandbox-right-margin:num <- get *first-sandbox, right:offset
+  edit-button-left:num, edit-button-right:num, _ <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
+  edit-button-vertical-area?:bool <- within-range? click-column, edit-button-left, edit-button-right
+  return-unless edit-button-vertical-area?, false
+  # finally, is sandbox editor empty?
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  result <- empty-editor? current-sandbox
+]
+
+def try-edit-sandbox click-row:num, env:&:environment -> clicked-on-edit-button?:bool, env:&:environment [
+  local-scope
+  load-inputs
+  # identify the sandbox to edit, if the click was actually on the 'edit' button
+  sandbox:&:sandbox <- find-sandbox env, click-row
+  return-unless sandbox, false
+  clicked-on-edit-button? <- copy true
+  # 'edit' button = 'copy' button + 'delete' button
+  text:text <- get *sandbox, data:offset
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  current-sandbox <- insert-text current-sandbox, text
+  env <- delete-sandbox env, sandbox
+  # reset scroll
+  *env <- put *env, render-from:offset, -1
+]
+
+scenario sandbox-with-print-can-be-edited [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # empty recipes
+  assume-resources [
+  ]
+  # right editor contains a print instruction
+  env:&:environment <- new-programming-environment resources, screen, [print screen, 4]
+  render-all screen, env, render
+  # run the sandbox
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .print screen, 4                                   .
+    .screen:                                           .
+    .  .4                             .                .
+    .  .                              .                .
+    .  .                              .                .
+    .  .                              .                .
+    .  .                              .                .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # edit the sandbox
+  assume-console [
+    left-click 3, 18
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .print screen, 4                                   .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+  ]
+]
+
+scenario editing-sandbox-after-scrolling-resets-scroll [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press page-down
+    press page-down
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # edit the second sandbox
+  assume-console [
+    left-click 2, 10
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .add 2, 2                                          .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario editing-sandbox-updates-sandbox-count [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+  ]
+  # edit the second sandbox, then resave
+  assume-console [
+    left-click 3, 10
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # no change in contents
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 1, 1                                          .
+    .2                                                 .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+  ]
+  # now try to scroll past end
+  assume-console [
+    press page-down
+    press page-down
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # screen should show just final sandbox with the right index (1)
+  screen-should-contain [
+    .                               run (F4)           .
+    .──────────────────────────────────────────────────.
+    .1   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
diff --git a/archive/2.vm/sandbox/009-sandbox-test.mu b/archive/2.vm/sandbox/009-sandbox-test.mu
new file mode 100644
index 00000000..c22916a7
--- /dev/null
+++ b/archive/2.vm/sandbox/009-sandbox-test.mu
@@ -0,0 +1,233 @@
+## clicking on sandbox results to 'fix' them and turn sandboxes into tests
+
+scenario sandbox-click-on-result-toggles-color-to-green [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # basic recipe
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  reply 4|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  # run it
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # click on the '4' in the result
+  $clear-trace
+  assume-console [
+    left-click 5, 21
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # color toggles to green
+  screen-should-contain-in-color 2/green, [
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .4                                                 .
+    .                                                  .
+  ]
+  # don't render entire sandbox side
+  check-trace-count-for-label-lesser-than 250, [print-character]  # say 5 sandbox lines
+  # cursor should remain unmoved
+  run [
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # now change the result
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  reply 3|
+      |]|
+    ]
+  ]
+  # then rerun
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # result turns red
+  screen-should-contain-in-color 1/red, [
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .3                                                 .
+    .                                                  .
+  ]
+]
+
+# this requires tracking a couple more things
+container sandbox [
+  response-starting-row-on-screen:num
+  expected-response:text
+]
+
+# include expected response when saving or restoring a sandbox
+before <end-save-sandbox> [
+  {
+    expected-response:text <- get *sandbox, expected-response:offset
+    break-unless expected-response
+    filename <- append filename, [.out]
+    resources <- dump resources, filename, expected-response
+  }
+]
+
+before <end-restore-sandbox> [
+  {
+    filename <- append filename, [.out]
+    contents <- slurp resources, filename
+    break-unless contents
+    *curr <- put *curr, expected-response:offset, contents
+  }
+]
+
+# clicks on sandbox responses save it as 'expected'
+after <global-touch> [
+  # check if it's inside the output of any sandbox
+  {
+    sandbox-left-margin:num <- get *current-sandbox, left:offset
+    click-column:num <- get t, column:offset
+    on-sandbox-side?:bool <- greater-or-equal click-column, sandbox-left-margin
+    break-unless on-sandbox-side?
+    first-sandbox:&:sandbox <- get *env, sandbox:offset
+    break-unless first-sandbox
+    first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
+    click-row:num <- get t, row:offset
+    below-sandbox-editor?:bool <- greater-or-equal click-row, first-sandbox-begins
+    break-unless below-sandbox-editor?
+    # identify the sandbox whose output is being clicked on
+    sandbox:&:sandbox, sandbox-index:num <- find-click-in-sandbox-output env, click-row
+    break-unless sandbox
+    # update it
+    sandbox <- toggle-expected-response sandbox
+    # minimal update to disk
+    save-sandbox resources, sandbox, sandbox-index
+    # minimal update to screen
+    sandbox-right-margin:num <- get *current-sandbox, right:offset
+    row:num <- render-sandbox-response screen, sandbox, sandbox-left-margin, sandbox-right-margin
+    {
+      height:num <- screen-height screen
+      at-bottom?:bool <- greater-or-equal row, height
+      break-if at-bottom?
+      draw-horizontal screen, row, sandbox-left-margin, sandbox-right-margin
+    }
+    screen <- update-cursor screen, current-sandbox, env
+    loop +next-event
+  }
+]
+
+def find-click-in-sandbox-output env:&:environment, click-row:num -> sandbox:&:sandbox, sandbox-index:num [
+  local-scope
+  load-inputs
+  # assert click-row >= sandbox.starting-row-on-screen
+  sandbox:&:sandbox <- get *env, sandbox:offset
+  start:num <- get *sandbox, starting-row-on-screen:offset
+  clicked-on-sandboxes?:bool <- greater-or-equal click-row, start
+  assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor]
+  # while click-row < sandbox.next-sandbox.starting-row-on-screen
+  sandbox-index <- copy 0
+  {
+    next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
+    break-unless next-sandbox
+    next-start:num <- get *next-sandbox, starting-row-on-screen:offset
+    found?:bool <- lesser-than click-row, next-start
+    break-if found?
+    sandbox <- copy next-sandbox
+    sandbox-index <- add sandbox-index, 1
+    loop
+  }
+  # return sandbox if click is in its output region
+  response-starting-row:num <- get *sandbox, response-starting-row-on-screen:offset
+  return-unless response-starting-row, null/no-click-in-sandbox-output, 0/sandbox-index
+  click-in-response?:bool <- greater-or-equal click-row, response-starting-row
+  return-unless click-in-response?, null/no-click-in-sandbox-output, 0/sandbox-index
+  return sandbox, sandbox-index
+]
+
+def toggle-expected-response sandbox:&:sandbox -> sandbox:&:sandbox [
+  local-scope
+  load-inputs
+  expected-response:text <- get *sandbox, expected-response:offset
+  {
+    # if expected-response is set, reset
+    break-unless expected-response
+    *sandbox <- put *sandbox, expected-response:offset, null
+  }
+  {
+    # if not, set expected response to the current response
+    break-if expected-response
+    response:text <- get *sandbox, response:offset
+    *sandbox <- put *sandbox, expected-response:offset, response
+  }
+]
+
+# when rendering a sandbox, color it in red/green if expected response exists
+after <render-sandbox-response> [
+  {
+    break-unless sandbox-response
+    *sandbox <- put *sandbox, response-starting-row-on-screen:offset, row
+    row <- render-sandbox-response screen, sandbox, left, right
+    jump +render-sandbox-end
+  }
+]
+
+def render-sandbox-response screen:&:screen, sandbox:&:sandbox, left:num, right:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  sandbox-response:text <- get *sandbox, response:offset
+  expected-response:text <- get *sandbox, expected-response:offset
+  row:num <- get *sandbox response-starting-row-on-screen:offset
+  {
+    break-if expected-response
+    row <- render-text screen, sandbox-response, left, right, 245/grey, row
+    return
+  }
+  response-is-expected?:bool <- equal expected-response, sandbox-response
+  {
+    break-if response-is-expected?
+    row <- render-text screen, sandbox-response, left, right, 1/red, row
+  }
+  {
+    break-unless response-is-expected?:bool
+    row <- render-text screen, sandbox-response, left, right, 2/green, row
+  }
+]
+
+before <end-render-sandbox-reset-hidden> [
+  *sandbox <- put *sandbox, response-starting-row-on-screen:offset, 0
+]
diff --git a/archive/2.vm/sandbox/010-sandbox-trace.mu b/archive/2.vm/sandbox/010-sandbox-trace.mu
new file mode 100644
index 00000000..d544dd8c
--- /dev/null
+++ b/archive/2.vm/sandbox/010-sandbox-trace.mu
@@ -0,0 +1,243 @@
+## clicking on the code typed into a sandbox toggles its trace
+
+scenario sandbox-click-on-code-toggles-app-trace [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  # run a stash instruction
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [stash [abc]]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .stash [abc]                                       .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # click on the code in the sandbox
+  assume-console [
+    left-click 4, 21
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # trace now printed and cursor shouldn't have budged
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .stash [abc]                                       .
+    .abc                                               .
+  ]
+  screen-should-contain-in-color 245/grey, [
+    .                                                  .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+    .                                                  .
+    .abc                                               .
+  ]
+  # click again on the same region
+  assume-console [
+    left-click 4, 25
+  ]
+  run [
+    event-loop screen, console, env, resources
+    print screen, cursor
+  ]
+  # trace hidden again
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .stash [abc]                                       .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario sandbox-shows-app-trace-and-result [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  # run a stash instruction and some code
+  assume-resources [
+  ]
+  test-sandbox:text <- new [stash [abc]
+add 2, 2]
+  env:&:environment <- new-programming-environment resources, screen, test-sandbox
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .stash [abc]                                       .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # click on the code in the sandbox
+  assume-console [
+    left-click 4, 21
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # trace now printed above result
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .stash [abc]                                       .
+    .add 2, 2                                          .
+    .abc                                               .
+    .7 instructions run                                .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+  ]
+]
+
+scenario clicking-on-app-trace-does-nothing [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [stash 123456789]
+  render-all screen, env, render
+  # create and expand the trace
+  assume-console [
+    press F4
+    left-click 4, 1
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .stash 123456789                                   .
+    .123456789                                         .
+  ]
+  # click on the stash under the edit-button region (or any of the other buttons, really)
+  assume-console [
+    left-click 5, 7
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # no change; doesn't die
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .stash 123456789                                   .
+    .123456789                                         .
+  ]
+]
+
+container sandbox [
+  trace:text
+  display-trace?:bool
+]
+
+# replaced in a later layer
+def! update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  response:text, _, fake-screen:&:screen, trace:text <- run-sandboxed data
+  *sandbox <- put *sandbox, response:offset, response
+  *sandbox <- put *sandbox, screen:offset, fake-screen
+  *sandbox <- put *sandbox, trace:offset, trace
+]
+
+# clicks on sandbox code toggle its display-trace? flag
+after <global-touch> [
+  # check if it's inside the code of any sandbox
+  {
+    sandbox-left-margin:num <- get *current-sandbox, left:offset
+    click-column:num <- get t, column:offset
+    on-sandbox-side?:bool <- greater-or-equal click-column, sandbox-left-margin
+    break-unless on-sandbox-side?
+    first-sandbox:&:sandbox <- get *env, sandbox:offset
+    break-unless first-sandbox
+    first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
+    click-row:num <- get t, row:offset
+    below-sandbox-editor?:bool <- greater-or-equal click-row, first-sandbox-begins
+    break-unless below-sandbox-editor?
+    # identify the sandbox whose code is being clicked on
+    sandbox:&:sandbox <- find-click-in-sandbox-code env, click-row
+    break-unless sandbox
+    # toggle its display-trace? property
+    x:bool <- get *sandbox, display-trace?:offset
+    x <- not x
+    *sandbox <- put *sandbox, display-trace?:offset, x
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, current-sandbox, env
+    loop +next-event
+  }
+]
+
+def find-click-in-sandbox-code env:&:environment, click-row:num -> sandbox:&:sandbox [
+  local-scope
+  load-inputs
+  # assert click-row >= sandbox.starting-row-on-screen
+  sandbox <- get *env, sandbox:offset
+  start:num <- get *sandbox, starting-row-on-screen:offset
+  clicked-on-sandboxes?:bool <- greater-or-equal click-row, start
+  assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor]
+  # while click-row < sandbox.next-sandbox.starting-row-on-screen
+  {
+    next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
+    break-unless next-sandbox
+    next-start:num <- get *next-sandbox, starting-row-on-screen:offset
+    found?:bool <- lesser-than click-row, next-start
+    break-if found?
+    sandbox <- copy next-sandbox
+    loop
+  }
+  # return sandbox if click is in its code region
+  code-ending-row:num <- get *sandbox, code-ending-row-on-screen:offset
+  click-above-response?:bool <- lesser-than click-row, code-ending-row
+  start:num <- get *sandbox, starting-row-on-screen:offset
+  click-below-menu?:bool <- greater-than click-row, start
+  click-on-sandbox-code?:bool <- and click-above-response?, click-below-menu?
+  {
+    break-if click-on-sandbox-code?
+    return null/no-click-in-sandbox-output
+  }
+  return sandbox
+]
+
+# when rendering a sandbox, dump its trace before response/warning if display-trace? property is set
+after <render-sandbox-results> [
+  {
+    display-trace?:bool <- get *sandbox, display-trace?:offset
+    break-unless display-trace?
+    sandbox-trace:text <- get *sandbox, trace:offset
+    break-unless sandbox-trace  # nothing to print; move on
+    row, screen <- render-text screen, sandbox-trace, left, right, 245/grey, row
+  }
+  <render-sandbox-trace-done>
+]
diff --git a/archive/2.vm/sandbox/011-errors.mu b/archive/2.vm/sandbox/011-errors.mu
new file mode 100644
index 00000000..2f59d1fe
--- /dev/null
+++ b/archive/2.vm/sandbox/011-errors.mu
@@ -0,0 +1,687 @@
+## handling malformed programs
+
+container environment [
+  recipe-errors:text
+]
+
+# load code from disk, save any errors
+def! update-recipes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, screen:&:screen [
+  local-scope
+  load-inputs
+  in:text <- slurp resources, [lesson/recipes.mu]
+  recipe-errors:text <- reload in
+  *env <- put *env, recipe-errors:offset, recipe-errors
+  # if recipe editor has errors, stop
+  {
+    break-unless recipe-errors
+    update-status screen, [errors found     ], 1/red
+    errors-found? <- copy true
+    return
+  }
+  errors-found? <- copy false
+]
+
+before <end-render-components> [
+  trace 11, [app], [render status]
+  recipe-errors:text <- get *env, recipe-errors:offset
+  {
+    break-unless recipe-errors
+    update-status screen, [errors found     ], 1/red
+  }
+]
+
+container environment [
+  error-index:num  # index of first sandbox with an error (or -1 if none)
+]
+
+after <programming-environment-initialization> [
+  *result <- put *result, error-index:offset, -1
+]
+
+after <begin-run-sandboxes> [
+  *env <- put *env, error-index:offset, -1
+]
+
+before <end-run-sandboxes> [
+  {
+    error-index:num <- get *env, error-index:offset
+    sandboxes-completed-successfully?:bool <- equal error-index, -1
+    break-if sandboxes-completed-successfully?
+    errors-found? <- copy true
+  }
+]
+
+before <end-render-components> [
+  {
+    break-if recipe-errors
+    error-index:num <- get *env, error-index:offset
+    sandboxes-completed-successfully?:bool <- equal error-index, -1
+    break-if sandboxes-completed-successfully?
+    error-index-text:text <- to-text error-index
+    status:text <- interpolate [errors found (_)    ], error-index-text
+    update-status screen, status, 1/red
+  }
+]
+
+container sandbox [
+  errors:text
+]
+
+def! update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [
+  local-scope
+  load-inputs
+  {
+    recipe-errors:text <- get *env, recipe-errors:offset
+    break-unless recipe-errors
+    *sandbox <- put *sandbox, errors:offset, recipe-errors
+    return
+  }
+  data:text <- get *sandbox, data:offset
+  response:text, errors:text, fake-screen:&:screen, trace:text, completed?:bool <- run-sandboxed data
+  *sandbox <- put *sandbox, response:offset, response
+  *sandbox <- put *sandbox, errors:offset, errors
+  *sandbox <- put *sandbox, screen:offset, fake-screen
+  *sandbox <- put *sandbox, trace:offset, trace
+  {
+    break-if errors
+    break-if completed?
+    errors <- new [took too long!
+]
+    *sandbox <- put *sandbox, errors:offset, errors
+  }
+  {
+    break-unless errors
+    error-index:num <- get *env, error-index:offset
+    error-not-set?:bool <- equal error-index, -1
+    break-unless error-not-set?
+    *env <- put *env, error-index:offset, idx
+  }
+]
+
+# make sure we render any trace
+after <render-sandbox-trace-done> [
+  {
+    sandbox-errors:text <- get *sandbox, errors:offset
+    break-unless sandbox-errors
+    *sandbox <- put *sandbox, response-starting-row-on-screen:offset, 0  # no response
+    {
+      break-unless env
+      recipe-errors:text <- get *env, recipe-errors:offset
+      row, screen <- render-text screen, recipe-errors, left, right, 1/red, row
+    }
+    row, screen <- render-text screen, sandbox-errors, left, right, 1/red, row
+    # don't try to print anything more for this sandbox
+    jump +render-sandbox-end
+  }
+]
+
+scenario run-shows-errors-in-get [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  get 123:num, foo:offset|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  screen-should-contain [
+    .                               run (F4)           .
+    .foo                                               .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                 run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .foo: unknown element 'foo' in container 'number'  .
+    .foo: first ingredient of 'get' should be a contai↩.
+    .ner, but got '123:num'                            .
+  ]
+  screen-should-contain-in-color 1/red, [
+    .  errors found                                    .
+    .                                                  .
+  ]
+]
+
+scenario run-updates-status-with-first-erroneous-sandbox [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    # create invalid sandbox 1
+    type [get foo, x:offset]
+    press F4
+    # create invalid sandbox 0
+    type [get foo, x:offset]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # status line shows that error is in first sandbox
+  screen-should-contain [
+    .  errors found (0)             run (F4)           .
+  ]
+]
+
+scenario run-updates-status-with-first-erroneous-sandbox-2 [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    # create invalid sandbox 2
+    type [get foo, x:offset]
+    press F4
+    # create invalid sandbox 1
+    type [get foo, x:offset]
+    press F4
+    # create valid sandbox 0
+    type [add 2, 2]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # status line shows that error is in second sandbox
+  screen-should-contain [
+    .  errors found (1)             run (F4)           .
+  ]
+]
+
+scenario run-hides-errors-from-past-sandboxes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [get foo, x:offset]  # invalid
+  render-all screen, env, render
+  assume-console [
+    press F4  # generate error
+  ]
+  event-loop screen, console, env, resources
+  assume-console [
+    left-click 3, 10
+    press ctrl-k
+    type [add 2, 2]  # valid code
+    press F4  # update sandbox
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # error should disappear
+  screen-should-contain [
+    .                               run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .add 2, 2                                          .
+    .4                                                 .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario run-updates-errors-for-shape-shifting-recipes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # define a shape-shifting recipe with an error
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo x:_elem -> z:_elem [|
+      |  local-scope|
+      |  load-ingredients|
+      |  y:&:num <- copy null|
+      |  z <- add x, y|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo 2]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .  errors found (0)             run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo 2                                             .
+    .foo_2: 'add' requires number ingredients, but got↩.
+    . 'y'                                              .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # now rerun everything
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # error should remain unchanged
+  screen-should-contain [
+    .  errors found (0)             run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo 2                                             .
+    .foo_3: 'add' requires number ingredients, but got↩.
+    . 'y'                                              .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario run-avoids-spurious-errors-on-reloading-shape-shifting-recipes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # overload a well-known shape-shifting recipe
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe length l:&:list:_elem -> n:num [|
+      |]|
+    ]
+  ]
+  # call code that uses other variants of it, but not it itself
+  test-sandbox:text <- new [x:&:list:num <- copy null
+to-text x]
+  env:&:environment <- new-programming-environment resources, screen, test-sandbox
+  render-all screen, env, render
+  # run it once
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  # no errors anywhere on screen (can't check anything else, since to-text will return an address)
+  screen-should-contain-in-color 1/red, [
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .             <-                                   .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+  # rerun everything
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # still no errors
+  screen-should-contain-in-color 1/red, [
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .             <-                                   .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+  ]
+]
+
+scenario run-shows-missing-type-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  x <- copy 0|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                 run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .foo: missing type for 'x' in 'x <- copy 0'        .
+  ]
+]
+
+scenario run-shows-unbalanced-bracket-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # recipe is incomplete (unbalanced '[')
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo \\\[|
+      |  x <- copy 0|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                 run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .9: unbalanced '\\[' for recipe                      .
+    .9: unbalanced '\\[' for recipe                      .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario run-shows-get-on-non-container-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  x:&:point <- new point:type|
+      |  get x:&:point, 1:offset|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                 run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .foo: first ingredient of 'get' should be a contai↩.
+    .ner, but got 'x:&:point'                          .
+  ]
+]
+
+scenario run-shows-non-literal-get-argument-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  x:num <- copy 0|
+      |  y:&:point <- new point:type|
+      |  get *y:&:point, x:num|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                 run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .foo: second ingredient of 'get' should have type ↩.
+    .'offset', but got 'x:num'                         .
+  ]
+]
+
+scenario run-shows-errors-every-time [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # try to run a file with an error
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  x:num <- copy y:num|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .  errors found                 run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .foo: tried to read ingredient 'y' in 'x:num <- co↩.
+    .py y:num' but it hasn't been written to yet       .
+  ]
+  # rerun the file, check for the same error
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found                 run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo                                               .
+    .foo: tried to read ingredient 'y' in 'x:num <- co↩.
+    .py y:num' but it hasn't been written to yet       .
+  ]
+]
+
+scenario run-instruction-and-print-errors [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 15/height
+  assume-resources [
+  ]
+  # editor contains an illegal instruction
+  env:&:environment <- new-programming-environment resources, screen, [get 1:&:point, 1:offset]
+  render-all screen, env, render
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints error message in red
+  screen-should-contain [
+    .  errors found (0)             run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .get 1:&:point, 1:offset                           .
+    .first ingredient of 'get' should be a container, ↩.
+    .but got '1:&:point'                               .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  screen-should-contain-in-color 1/red, [
+    .  errors found (0)                                .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .                                                  .
+    .first ingredient of 'get' should be a container,  .
+    .but got '1:&:point'                               .
+    .                                                  .
+    .                                                  .
+  ]
+]
+
+scenario run-instruction-and-print-errors-only-once [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 10/height
+  assume-resources [
+  ]
+  # editor contains an illegal instruction
+  env:&:environment <- new-programming-environment resources, screen, [get 1234:num, foo:offset]
+  render-all screen, env, render
+  # run the code in the editors multiple times
+  assume-console [
+    press F4
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints error message just once
+  screen-should-contain [
+    .  errors found (0)             run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .get 1234:num, foo:offset                          .
+    .unknown element 'foo' in container 'number'       .
+    .first ingredient of 'get' should be a container, ↩.
+    .but got '1234:num'                                .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario sandbox-can-handle-infinite-loop [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  assume-resources [
+  ]
+  # editor contains an infinite loop
+  test-sandbox:text <- new [{
+loop
+}]
+  env:&:environment <- new-programming-environment resources, screen, test-sandbox
+  render-all screen, env, render
+  # run the sandbox
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  screen-should-contain [
+    .  errors found (0)             run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .{                                                 .
+    .loop                                              .
+    .}                                                 .
+    .took too long!                                    .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
+
+scenario sandbox-with-errors-shows-trace [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # generate a stash and a error
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      |recipe foo [|
+      |  local-scope|
+      |  a:num <- next-ingredient|
+      |  b:num <- next-ingredient|
+      |  stash [dividing by], b|
+      |  _, c:num <- divide-with-remainder a, b|
+      |  reply b|
+      |]|
+    ]
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [foo 4, 0]
+  render-all screen, env, render
+  # run
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  # screen prints error message
+  screen-should-contain [
+    .  errors found (0)             run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo 4, 0                                          .
+    .foo: divide by zero in '_, c:num <- divide-with-r↩.
+    .emainder a, b'                                    .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+  # click on the call in the sandbox
+  assume-console [
+    left-click 4, 15
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # screen should expand trace
+  screen-should-contain [
+    .  errors found (0)             run (F4)           .
+    .                                                  .
+    .──────────────────────────────────────────────────.
+    .0   edit           copy           delete          .
+    .foo 4, 0                                          .
+    .dividing by 0                                     .
+    .14 instructions run                               .
+    .foo: divide by zero in '_, c:num <- divide-with-r↩.
+    .emainder a, b'                                    .
+    .──────────────────────────────────────────────────.
+    .                                                  .
+  ]
+]
diff --git a/archive/2.vm/sandbox/012-editor-undo.mu b/archive/2.vm/sandbox/012-editor-undo.mu
new file mode 100644
index 00000000..69afd207
--- /dev/null
+++ b/archive/2.vm/sandbox/012-editor-undo.mu
@@ -0,0 +1,1907 @@
+## undo/redo
+
+# for every undoable event, create a type of *operation* that contains all the
+# information needed to reverse it
+exclusive-container operation [
+  typing:insert-operation
+  move:move-operation
+  delete:delete-operation
+]
+
+container insert-operation [
+  before-row:num
+  before-column:num
+  before-top-of-screen:&:duplex-list:char
+  after-row:num
+  after-column:num
+  after-top-of-screen:&:duplex-list:char
+  # inserted text is from 'insert-from' until 'insert-until'; list doesn't have to terminate
+  insert-from:&:duplex-list:char
+  insert-until:&:duplex-list:char
+  tag:num  # event causing this operation; might be used to coalesce runs of similar events
+    # 0: no coalesce (enter+indent)
+    # 1: regular alphanumeric characters
+]
+
+container move-operation [
+  before-row:num
+  before-column:num
+  before-top-of-screen:&:duplex-list:char
+  after-row:num
+  after-column:num
+  after-top-of-screen:&:duplex-list:char
+  tag:num  # event causing this operation; might be used to coalesce runs of similar events
+    # 0: no coalesce (touch events, etc)
+    # 1: left arrow
+    # 2: right arrow
+    # 3: up arrow
+    # 4: down arrow
+]
+
+container delete-operation [
+  before-row:num
+  before-column:num
+  before-top-of-screen:&:duplex-list:char
+  after-row:num
+  after-column:num
+  after-top-of-screen:&:duplex-list:char
+  deleted-text:&:duplex-list:char
+  delete-from:&:duplex-list:char
+  delete-until:&:duplex-list:char
+  tag:num  # event causing this operation; might be used to coalesce runs of similar events
+    # 0: no coalesce (ctrl-k, ctrl-u)
+    # 1: backspace
+    # 2: delete
+]
+
+# every editor accumulates a list of operations to undo/redo
+container editor [
+  undo:&:list:&:operation
+  redo:&:list:&:operation
+]
+
+# ctrl-z - undo operation
+after <handle-special-character> [
+  {
+    undo?:bool <- equal c, 26/ctrl-z
+    break-unless undo?
+    undo:&:list:&:operation <- get *editor, undo:offset
+    break-unless undo
+    op:&:operation <- first undo
+    undo <- rest undo
+    *editor <- put *editor, undo:offset, undo
+    redo:&:list:&:operation <- get *editor, redo:offset
+    redo <- push op, redo
+    *editor <- put *editor, redo:offset, redo
+    <handle-undo>
+    return true/go-render
+  }
+]
+
+# ctrl-y - redo operation
+after <handle-special-character> [
+  {
+    redo?:bool <- equal c, 25/ctrl-y
+    break-unless redo?
+    redo:&:list:&:operation <- get *editor, redo:offset
+    break-unless redo
+    op:&:operation <- first redo
+    redo <- rest redo
+    *editor <- put *editor, redo:offset, redo
+    undo:&:list:&:operation <- get *editor, undo:offset
+    undo <- push op, undo
+    *editor <- put *editor, undo:offset, undo
+    <handle-redo>
+    return true/go-render
+  }
+]
+
+# undo typing
+
+scenario editor-can-undo-typing [
+  local-scope
+  # create an editor and type a character
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [0]
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # character should be gone
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .1         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+# save operation to undo
+after <begin-insert-character> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset
+]
+before <end-insert-character> [
+  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  undo:&:list:&:operation <- get *editor, undo:offset
+  {
+    # if previous operation was an insert, coalesce this operation with it
+    break-unless undo
+    op:&:operation <- first undo
+    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
+    break-unless is-insert?
+    previous-coalesce-tag:num <- get typing, tag:offset
+    break-unless previous-coalesce-tag
+    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    insert-until:&:duplex-list:char <- next before-cursor
+    typing <- put typing, insert-until:offset, insert-until
+    typing <- put typing, after-row:offset, cursor-row
+    typing <- put typing, after-column:offset, cursor-column
+    typing <- put typing, after-top-of-screen:offset, top-after
+    *op <- merge 0/insert-operation, typing
+    break +done-adding-insert-operation
+  }
+  # if not, create a new operation
+  insert-from:&:duplex-list:char <- next cursor-before
+  insert-to:&:duplex-list:char <- next insert-from
+  op:&:operation <- new operation:type
+  *op <- merge 0/insert-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, insert-from, insert-to, 1/coalesce
+  editor <- add-operation editor, op
+  +done-adding-insert-operation
+]
+
+# enter operations never coalesce with typing before or after
+after <begin-insert-enter> [
+  cursor-row-before:num <- copy cursor-row
+  cursor-column-before:num <- copy cursor-column
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset
+]
+before <end-insert-enter> [
+  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-row:offset
+  # never coalesce
+  insert-from:&:duplex-list:char <- next cursor-before
+  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+  insert-to:&:duplex-list:char <- next before-cursor
+  op:&:operation <- new operation:type
+  *op <- merge 0/insert-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, insert-from, insert-to, 0/never-coalesce
+  editor <- add-operation editor, op
+]
+
+# Everytime you add a new operation to the undo stack, be sure to clear the
+# redo stack, because it's now obsolete.
+# Beware: since we're counting cursor moves as operations, this means just
+# moving the cursor can lose work on the undo stack.
+def add-operation editor:&:editor, op:&:operation -> editor:&:editor [
+  local-scope
+  load-inputs
+  undo:&:list:&:operation <- get *editor, undo:offset
+  undo <- push op undo
+  *editor <- put *editor, undo:offset, undo
+  redo:&:list:&:operation <- get *editor, redo:offset
+  redo <- copy null
+  *editor <- put *editor, redo:offset, redo
+]
+
+after <handle-undo> [
+  {
+    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
+    break-unless is-insert?
+    start:&:duplex-list:char <- get typing, insert-from:offset
+    end:&:duplex-list:char <- get typing, insert-until:offset
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    before-cursor:&:duplex-list:char <- prev start
+    *editor <- put *editor, before-cursor:offset, before-cursor
+    remove-between before-cursor, end
+    cursor-row <- get typing, before-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get typing, before-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get typing, before-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+scenario editor-can-undo-typing-multiple [
+  local-scope
+  # create an editor and type multiple characters
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [012]
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # all characters must be gone
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-can-undo-typing-multiple-2 [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [a], 0/left, 10/right
+  editor-render screen, e
+  # type some characters
+  assume-console [
+    type [012]
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .012a      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # back to original text
+  screen-should-contain [
+    .          .
+    .a         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [3]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .3a        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-can-undo-typing-enter [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [  abc], 0/left, 10/right
+  editor-render screen, e
+  # new line
+  assume-console [
+    left-click 1, 8
+    press enter
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .  abc     .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # line is indented
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 2
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 5
+  ]
+  # back to original text
+  screen-should-contain [
+    .          .
+    .  abc     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be at end of line
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .  abc1    .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+# redo typing
+
+scenario editor-redo-typing [
+  local-scope
+  # create an editor, type something, undo
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [a], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [012]
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # all characters must be back
+  screen-should-contain [
+    .          .
+    .012a      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [3]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0123a     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <handle-redo> [
+  {
+    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
+    break-unless is-insert?
+    before-cursor <- get *editor, before-cursor:offset
+    insert-from:&:duplex-list:char <- get typing, insert-from:offset  # ignore insert-to because it's already been spliced away
+    # assert insert-to matches next(before-cursor)
+    splice before-cursor, insert-from
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    cursor-row <- get typing, after-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get typing, after-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get typing, after-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+scenario editor-redo-typing-empty [
+  local-scope
+  # create an editor, type something, undo
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [012]
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # all characters must be back
+  screen-should-contain [
+    .          .
+    .012       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [3]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .0123      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+scenario editor-work-clears-redo-stack [
+  local-scope
+  # create an editor with some text, do some work, undo
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [1]
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  # do some more work
+  assume-console [
+    type [0]
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .0abc      .
+    .def       .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # nothing should happen
+  screen-should-contain [
+    .          .
+    .0abc      .
+    .def       .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-redo-typing-and-enter-and-tab [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  # insert some text and tabs, hit enter, some more text and tabs
+  assume-console [
+    press tab
+    type [ab]
+    press tab
+    type [cd]
+    press enter
+    press tab
+    type [efg]
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .    efg   .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 7
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # typing in second line deleted, but not indent
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # indent and newline deleted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 8
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # empty screen
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 8
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo again
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # newline and indent inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo again
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # indent and newline deleted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 7
+  ]
+  screen-should-contain [
+    .          .
+    .  ab  cd  .
+    .    efg   .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+# undo cursor movement
+
+scenario editor-can-undo-touch [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 3, 1
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # click undone
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .1abc      .
+    .def       .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+after <begin-move-cursor> [
+  cursor-row-before:num <- get *editor, cursor-row:offset
+  cursor-column-before:num <- get *editor, cursor-column:offset
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-move-cursor> [
+  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+  cursor-row:num <- get *editor, cursor-row:offset
+  cursor-column:num <- get *editor, cursor-column:offset
+  {
+    break-unless undo-coalesce-tag
+    # if previous operation was also a move, and also had the same coalesce
+    # tag, coalesce with it
+    undo:&:list:&:operation <- get *editor, undo:offset
+    break-unless undo
+    op:&:operation <- first undo
+    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
+    break-unless is-move?
+    previous-coalesce-tag:num <- get move, tag:offset
+    coalesce?:bool <- equal undo-coalesce-tag, previous-coalesce-tag
+    break-unless coalesce?
+    move <- put move, after-row:offset, cursor-row
+    move <- put move, after-column:offset, cursor-column
+    move <- put move, after-top-of-screen:offset, top-after
+    *op <- merge 1/move-operation, move
+    break +done-adding-move-operation
+  }
+  op:&:operation <- new operation:type
+  *op <- merge 1/move-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, undo-coalesce-tag
+  editor <- add-operation editor, op
+  +done-adding-move-operation
+]
+
+after <handle-undo> [
+  {
+    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
+    break-unless is-move?
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    cursor-row <- get move, before-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get move, before-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get move, before-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+scenario editor-can-undo-left-arrow [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 3, 1
+    press left-arrow
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 3
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .g1hi      .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-up-arrow [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 3, 1
+    press up-arrow
+  ]
+  editor-event-loop screen, console, e
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 3
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .g1hi      .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-down-arrow [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 2, 1
+    press down-arrow
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-ctrl-a [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor, then to start of line
+  assume-console [
+    left-click 2, 1
+    press ctrl-a
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-home [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor, then to start of line
+  assume-console [
+    left-click 2, 1
+    press home
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-ctrl-e [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor, then to start of line
+  assume-console [
+    left-click 2, 1
+    press ctrl-e
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-end [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor, then to start of line
+  assume-console [
+    left-click 2, 1
+    press end
+  ]
+  editor-event-loop screen, console, e
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves back
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .d1ef      .
+    .ghi       .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+scenario editor-can-undo-multiple-arrows-in-the-same-direction [
+  local-scope
+  # create an editor with some text
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # move the cursor
+  assume-console [
+    left-click 2, 1
+    press right-arrow
+    press right-arrow
+    press up-arrow
+  ]
+  editor-event-loop screen, console, e
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # up-arrow is undone
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 3
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # both right-arrows are undone
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 2
+    4 <- 1
+  ]
+]
+
+# redo cursor movement
+
+scenario editor-redo-touch [
+  local-scope
+  # create an editor with some text, click on a character, undo
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def
+ghi]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    left-click 3, 1
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # cursor moves to left-click
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 3
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .g1hi      .
+    .┈┈┈┈┈┈┈┈┈┈.
+  ]
+]
+
+after <handle-redo> [
+  {
+    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
+    break-unless is-move?
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    cursor-row <- get move, after-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get move, after-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get move, after-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+scenario editor-separates-undo-insert-from-undo-cursor-move [
+  local-scope
+  # create an editor, type some text, move the cursor, type some more text
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  assume-console [
+    type [abc]
+    left-click 1, 1
+    type [d]
+  ]
+  editor-event-loop screen, console, e
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  screen-should-contain [
+    .          .
+    .adbc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # last letter typed is deleted
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # no change to screen; cursor moves
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  # undo again
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # screen empty
+  screen-should-contain [
+    .          .
+    .          .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # first insert
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  # redo again
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # cursor moves
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # cursor moves
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # redo again
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+    3:num/raw <- get *e, cursor-row:offset
+    4:num/raw <- get *e, cursor-column:offset
+  ]
+  # second insert
+  screen-should-contain [
+    .          .
+    .adbc      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+]
+
+# undo backspace
+
+scenario editor-can-undo-and-redo-backspace [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit backspace
+  assume-console [
+    type [abc]
+    press backspace
+    press backspace
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 3
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  screen-should-contain [
+    .          .
+    .a         .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+# save operation to undo
+after <begin-backspace-character> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-backspace-character> [
+  {
+    break-unless backspaced-cell  # backspace failed; don't add an undo operation
+    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-row:offset
+    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    undo:&:list:&:operation <- get *editor, undo:offset
+    {
+      # if previous operation was an insert, coalesce this operation with it
+      break-unless undo
+      op:&:operation <- first undo
+      deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
+      break-unless is-delete?
+      previous-coalesce-tag:num <- get deletion, tag:offset
+      coalesce?:bool <- equal previous-coalesce-tag, 1/coalesce-backspace
+      break-unless coalesce?
+      deletion <- put deletion, delete-from:offset, before-cursor
+      backspaced-so-far:&:duplex-list:char <- get deletion, deleted-text:offset
+      splice backspaced-cell, backspaced-so-far
+      deletion <- put deletion, deleted-text:offset, backspaced-cell
+      deletion <- put deletion, after-row:offset, cursor-row
+      deletion <- put deletion, after-column:offset, cursor-column
+      deletion <- put deletion, after-top-of-screen:offset, top-after
+      *op <- merge 2/delete-operation, deletion
+      break +done-adding-backspace-operation
+    }
+    # if not, create a new operation
+    op:&:operation <- new operation:type
+    deleted-until:&:duplex-list:char <- next before-cursor
+    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, backspaced-cell/deleted, before-cursor/delete-from, deleted-until, 1/coalesce-backspace
+    editor <- add-operation editor, op
+    +done-adding-backspace-operation
+  }
+]
+
+after <handle-undo> [
+  {
+    deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
+    break-unless is-delete?
+    anchor:&:duplex-list:char <- get deletion, delete-from:offset
+    break-unless anchor
+    deleted:&:duplex-list:char <- get deletion, deleted-text:offset
+    old-cursor:&:duplex-list:char <- last deleted
+    splice anchor, deleted
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    before-cursor <- copy old-cursor
+    cursor-row <- get deletion, before-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get deletion, before-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get deletion, before-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+after <handle-redo> [
+  {
+    deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
+    break-unless is-delete?
+    start:&:duplex-list:char <- get deletion, delete-from:offset
+    end:&:duplex-list:char <- get deletion, delete-until:offset
+    data:&:duplex-list:char <- get *editor, data:offset
+    remove-between start, end
+    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
+    cursor-row <- get deletion, after-row:offset
+    *editor <- put *editor, cursor-row:offset, cursor-row
+    cursor-column <- get deletion, after-column:offset
+    *editor <- put *editor, cursor-column:offset, cursor-column
+    top:&:duplex-list:char <- get deletion, before-top-of-screen:offset
+    *editor <- put *editor, top-of-screen:offset, top
+  }
+]
+
+# undo delete
+
+scenario editor-can-undo-and-redo-delete [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit delete and backspace a few times
+  assume-console [
+    type [abcdef]
+    left-click 1, 2
+    press delete
+    press backspace
+    press delete
+    press delete
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .af        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # undo deletes
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  screen-should-contain [
+    .          .
+    .adef      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo backspace
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .abdef     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # undo first delete
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .abcdef    .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo first delete
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  screen-should-contain [
+    .          .
+    .abdef     .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo backspace
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  screen-should-contain [
+    .          .
+    .adef      .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  # redo deletes
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  screen-should-contain [
+    .          .
+    .af        .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <begin-delete-character> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-delete-character> [
+  {
+    break-unless deleted-cell  # delete failed; don't add an undo operation
+    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-column:offset
+    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    undo:&:list:&:operation <- get *editor, undo:offset
+    {
+      # if previous operation was an insert, coalesce this operation with it
+      break-unless undo
+      op:&:operation <- first undo
+      deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
+      break-unless is-delete?
+      previous-coalesce-tag:num <- get deletion, tag:offset
+      coalesce?:bool <- equal previous-coalesce-tag, 2/coalesce-delete
+      break-unless coalesce?
+      delete-until:&:duplex-list:char <- next before-cursor
+      deletion <- put deletion, delete-until:offset, delete-until
+      deleted-so-far:&:duplex-list:char <- get deletion, deleted-text:offset
+      deleted-so-far <- append deleted-so-far, deleted-cell
+      deletion <- put deletion, deleted-text:offset, deleted-so-far
+      deletion <- put deletion, after-row:offset, cursor-row
+      deletion <- put deletion, after-column:offset, cursor-column
+      deletion <- put deletion, after-top-of-screen:offset, top-after
+      *op <- merge 2/delete-operation, deletion
+      break +done-adding-delete-operation
+    }
+    # if not, create a new operation
+    op:&:operation <- new operation:type
+    deleted-until:&:duplex-list:char <- next before-cursor
+    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cell/deleted, before-cursor/delete-from, deleted-until, 2/coalesce-delete
+    editor <- add-operation editor, op
+    +done-adding-delete-operation
+  }
+]
+
+# undo ctrl-k
+
+scenario editor-can-undo-and-redo-ctrl-k [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit delete and backspace a few times
+  assume-console [
+    left-click 1, 1
+    press ctrl-k
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .a         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  screen-should-contain [
+    .          .
+    .a         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 1
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .a1        .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <begin-delete-to-end-of-line> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-delete-to-end-of-line> [
+  {
+    break-unless deleted-cells  # delete failed; don't add an undo operation
+    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-column:offset
+    deleted-until:&:duplex-list:char <- next before-cursor
+    op:&:operation <- new operation:type
+    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cells/deleted, before-cursor/delete-from, deleted-until, 0/never-coalesce
+    editor <- add-operation editor, op
+    +done-adding-delete-operation
+  }
+]
+
+# undo ctrl-u
+
+scenario editor-can-undo-and-redo-ctrl-u [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  contents:text <- new [abc
+def]
+  e:&:editor <- new-editor contents, 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit delete and backspace a few times
+  assume-console [
+    left-click 1, 2
+    press ctrl-u
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .c         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  # undo
+  assume-console [
+    press ctrl-z
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .abc       .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 2
+  ]
+  # redo
+  assume-console [
+    press ctrl-y
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  # first line inserted
+  screen-should-contain [
+    .          .
+    .c         .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+  3:num/raw <- get *e, cursor-row:offset
+  4:num/raw <- get *e, cursor-column:offset
+  memory-should-contain [
+    3 <- 1
+    4 <- 0
+  ]
+  # cursor should be in the right place
+  assume-console [
+    type [1]
+  ]
+  run [
+    editor-event-loop screen, console, e
+  ]
+  screen-should-contain [
+    .          .
+    .1c        .
+    .def       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
+
+after <begin-delete-to-start-of-line> [
+  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
+]
+before <end-delete-to-start-of-line> [
+  {
+    break-unless deleted-cells  # delete failed; don't add an undo operation
+    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
+    op:&:operation <- new operation:type
+    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
+    deleted-until:&:duplex-list:char <- next before-cursor
+    cursor-row:num <- get *editor, cursor-row:offset
+    cursor-column:num <- get *editor, cursor-column:offset
+    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cells/deleted, before-cursor/delete-from, deleted-until, 0/never-coalesce
+    editor <- add-operation editor, op
+    +done-adding-delete-operation
+  }
+]
+
+scenario editor-can-undo-and-redo-ctrl-u-2 [
+  local-scope
+  # create an editor
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [], 0/left, 10/right
+  editor-render screen, e
+  # insert some text and hit delete and backspace a few times
+  assume-console [
+    type [abc]
+    press ctrl-u
+    press ctrl-z
+  ]
+  editor-event-loop screen, console, e
+  screen-should-contain [
+    .          .
+    .abc       .
+    .┈┈┈┈┈┈┈┈┈┈.
+    .          .
+  ]
+]
diff --git a/archive/2.vm/sandbox/Readme.md b/archive/2.vm/sandbox/Readme.md
new file mode 100644
index 00000000..e8acd78b
--- /dev/null
+++ b/archive/2.vm/sandbox/Readme.md
@@ -0,0 +1,33 @@
+Variant of [the Mu programming environment](../edit) that runs just the sandbox.
+
+Suitable for people who want to run their favorite terminal-based editor with
+Mu. Just run editor and sandbox inside split panes atop tmux. For example,
+here's Mu running alongside vim:
+
+<img alt='tmux+vim example' src='../html/tmux-vim-sandbox.png'>
+
+To set this up:
+
+  a) copy the lines in tmux.conf into `$HOME/.tmux.conf`.
+
+  b) copy the file `mu_run` somewhere in your `$PATH`.
+
+Now when you start tmux, split it into two vertical panes, run `./mu sandbox`
+on the right pane and your editor on the left. You should be able to hit F4 in
+either side to run the sandbox.
+
+Known issues: you have to explicitly save inside your editor before hitting
+F4, unlike with `./mu edit`.
+
+---
+
+Appendix: keyboard shortcuts
+
+  _moving_
+  - `ctrl-a` or `home`: move cursor to start of line
+  - `ctrl-e` or `end`: move cursor to end of line
+
+  _modifying text_
+  - `ctrl-k`: delete text from cursor to end of line
+  - `ctrl-u`: delete text from start of line until just before cursor
+  - `ctrl-/`: comment/uncomment current line (using a special leader to ignore real comments https://www.reddit.com/r/vim/comments/4ootmz/_/d4ehmql)
diff --git a/archive/2.vm/sandbox/mu_run b/archive/2.vm/sandbox/mu_run
new file mode 100755
index 00000000..b96cfd1c
--- /dev/null
+++ b/archive/2.vm/sandbox/mu_run
@@ -0,0 +1,16 @@
+#!/usr/bin/zsh
+# Little bit of glue to support running Mu from Vim over tmux.
+
+export ALREADY_FOCUSED=0
+tmux list-panes |grep "^1.*active" -q && export ALREADY_FOCUSED=1
+if [[ $ALREADY_FOCUSED -eq 0 ]]
+then
+  tmux select-pane -t 1
+fi
+
+tmux send-keys 'F4'
+
+if [[ $ALREADY_FOCUSED -eq 0 ]]
+then
+  tmux last-pane
+fi
diff --git a/archive/2.vm/sandbox/tmux.conf b/archive/2.vm/sandbox/tmux.conf
new file mode 100644
index 00000000..7816b1eb
--- /dev/null
+++ b/archive/2.vm/sandbox/tmux.conf
@@ -0,0 +1,3 @@
+# Hotkey for running Mu over tmux
+# Assumes exactly two panes, with vim running on the left side and `./mu sandbox` running on the right side.
+bind-key -n F4 run mu_run
diff --git a/archive/2.vm/screen.mu b/archive/2.vm/screen.mu
new file mode 100644
index 00000000..58ecaa60
--- /dev/null
+++ b/archive/2.vm/screen.mu
@@ -0,0 +1,29 @@
+# example program: managing the display using 'screen' objects
+
+# The zero screen below means 'use the real screen'. Tests can also use fake
+# screens.
+def main [
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  10:char <- copy 97/a
+  print null/screen, 10:char/a, 1/red, 2/green
+  1:num/raw, 2:num/raw <- cursor-position null/screen
+  wait-for-event null/console
+  clear-screen null/screen
+  move-cursor null/screen, 0/row, 4/column
+  10:char <- copy 98/b
+  print null/screen, 10:char
+  wait-for-event null/console
+  move-cursor null/screen, 0/row, 0/column
+  clear-line null/screen
+  wait-for-event null/console
+  cursor-down null/screen
+  wait-for-event null/console
+  cursor-right null/screen
+  wait-for-event null/console
+  cursor-left null/screen
+  wait-for-event null/console
+  cursor-up null/screen
+  wait-for-event null/console
+  close-console
+]
diff --git a/archive/2.vm/snapshot_lesson b/archive/2.vm/snapshot_lesson
new file mode 100755
index 00000000..cd4723c1
--- /dev/null
+++ b/archive/2.vm/snapshot_lesson
@@ -0,0 +1,12 @@
+#!/bin/sh
+# Hacky little helper called from edit/ and sandbox/ apps to save a snapshot
+# of lesson/ using git.
+set -e
+
+test -d lesson/.git || exit 0  # give up if it's not a git repo
+
+cd lesson
+# explicitly say '--all' for git 1.9
+git add --all .
+# bug in git: git diff -q messes up --exit-code
+git diff HEAD --exit-code >/dev/null || git commit -a -m . >/dev/null
diff --git a/archive/2.vm/static-dispatch.mu b/archive/2.vm/static-dispatch.mu
new file mode 100644
index 00000000..a0157d13
--- /dev/null
+++ b/archive/2.vm/static-dispatch.mu
@@ -0,0 +1,29 @@
+# Example program showing how multiple functions with the same name can
+# coexist, and how we select between them.
+#
+# Expected output:
+#   4
+#   7
+#   7
+
+def test a:num -> b:num [
+  local-scope
+  load-inputs
+  b <- add a, 1
+]
+
+def test a:num, b:num -> c:num [
+  local-scope
+  load-inputs
+  c <- add a, b
+]
+
+def main [
+  local-scope
+  a:num <- test 3  # selects single-input version
+  $print a, 10/newline
+  b:num <- test 3, 4  # selects double-input version
+  $print b, 10/newline
+  c:num <- test 3, 4, 5  # prefers double- to single-input version
+  $print c, 10/newline
+]
diff --git a/archive/2.vm/tangle.mu b/archive/2.vm/tangle.mu
new file mode 100644
index 00000000..91f12dea
--- /dev/null
+++ b/archive/2.vm/tangle.mu
@@ -0,0 +1,36 @@
+# example program: constructing functions out of order
+#
+# We construct a factorial function with separate base and recursive cases.
+# Compare factorial.mu.
+#
+# This isn't a very tasteful example, just a basic demonstration of
+# possibilities.
+
+def factorial n:num -> result:num [
+  local-scope
+  load-inputs
+  <factorial-cases>
+]
+
+after <factorial-cases> [
+  # if n=0 return 1
+  return-unless n, 1
+]
+
+after <factorial-cases> [
+  # return n * factorial(n - 1)
+  {
+    break-unless n
+    x:num <- subtract n, 1
+    subresult:num <- factorial x
+    result <- multiply subresult, n
+    return result
+  }
+]
+
+def main [
+  1:num <- factorial 5
+  # trailing space in next line is to help with syntax highlighting
+  $print [result: ], 1:num, [ 
+]
+]
diff --git a/archive/2.vm/termbox/COPYING b/archive/2.vm/termbox/COPYING
new file mode 100644
index 00000000..e9bb4eac
--- /dev/null
+++ b/archive/2.vm/termbox/COPYING
@@ -0,0 +1,19 @@
+Copyright (C) 2010-2013 nsf <no.smile.face@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/archive/2.vm/termbox/Readme b/archive/2.vm/termbox/Readme
new file mode 100644
index 00000000..d97cae4e
--- /dev/null
+++ b/archive/2.vm/termbox/Readme
@@ -0,0 +1,2 @@
+Fork of https://github.com/nsf/termbox as of 2015-06-05
+git hash 252bef01264a2aeef22ebe5bae0b5893a58947f3
diff --git a/archive/2.vm/termbox/bytebuffer.inl b/archive/2.vm/termbox/bytebuffer.inl
new file mode 100644
index 00000000..aae8f073
--- /dev/null
+++ b/archive/2.vm/termbox/bytebuffer.inl
@@ -0,0 +1,79 @@
+struct bytebuffer {
+  char *buf;
+  int len;
+  int cap;
+};
+
+static void bytebuffer_reserve(struct bytebuffer *b, int cap) {
+  if (b->cap >= cap) {
+    return;
+  }
+
+  // prefer doubling capacity
+  if (b->cap * 2 >= cap) {
+    cap = b->cap * 2;
+  }
+
+  char *newbuf = malloc(cap);
+  if (b->len > 0) {
+    // copy what was there, b->len > 0 assumes b->buf != null
+    memcpy(newbuf, b->buf, b->len);
+  }
+  if (b->buf) {
+    // in case there was an allocated buffer, free it
+    free(b->buf);
+  }
+  b->buf = newbuf;
+  b->cap = cap;
+}
+
+static void bytebuffer_init(struct bytebuffer *b, int cap) {
+  b->cap = 0;
+  b->len = 0;
+  b->buf = 0;
+
+  if (cap > 0) {
+    b->cap = cap;
+    b->buf = malloc(cap); // just assume malloc works always
+  }
+}
+
+static void bytebuffer_free(struct bytebuffer *b) {
+  if (b->buf)
+    free(b->buf);
+}
+
+static void bytebuffer_clear(struct bytebuffer *b) {
+  b->len = 0;
+}
+
+static void bytebuffer_append(struct bytebuffer *b, const char *data, int len) {
+  bytebuffer_reserve(b, b->len + len);
+  memcpy(b->buf + b->len, data, len);
+  b->len += len;
+}
+
+static void bytebuffer_puts(struct bytebuffer *b, const char *str) {
+  bytebuffer_append(b, str, strlen(str));
+}
+
+static void bytebuffer_resize(struct bytebuffer *b, int len) {
+  bytebuffer_reserve(b, len);
+  b->len = len;
+}
+
+static void bytebuffer_flush(struct bytebuffer *b, int fd) {
+  int yyy = write(fd, b->buf, b->len);
+  (void) yyy;
+  bytebuffer_clear(b);
+}
+
+static void bytebuffer_truncate(struct bytebuffer *b, int n) {
+  if (n <= 0)
+    return;
+  if (n > b->len)
+    n = b->len;
+  const int nmove = b->len - n;
+  memmove(b->buf, b->buf+n, nmove);
+  b->len -= n;
+}
diff --git a/archive/2.vm/termbox/input.inl b/archive/2.vm/termbox/input.inl
new file mode 100644
index 00000000..83b4bb8c
--- /dev/null
+++ b/archive/2.vm/termbox/input.inl
@@ -0,0 +1,185 @@
+// if s1 starts with s2 returns true, else false
+// len is the length of s1
+// s2 should be null-terminated
+static bool starts_with(const char *s1, int len, const char *s2)
+{
+  int n = 0;
+  while (*s2 && n < len) {
+    if (*s1++ != *s2++)
+      return false;
+    n++;
+  }
+  return *s2 == 0;
+}
+
+#define FOO(...) { \
+  FILE* f = fopen("log", "a+"); \
+  fprintf(f, __VA_ARGS__); \
+  fclose(f); \
+}
+
+// convert escape sequence to event, and return consumed bytes on success (failure == 0)
+static int parse_escape_seq(struct tb_event *event, const char *buf, int len)
+{
+  static int parse_attempts = 0;
+  static const int MAX_PARSE_ATTEMPTS = 2;
+
+//?   int x = 0;
+//?   FOO("-- %d\n", len);
+//?   for (x = 0; x < len; ++x) {
+//?     FOO("%d\n", (unsigned char)buf[x]);
+//?   }
+  if (len >= 6 && starts_with(buf, len, "\033[M")) {
+
+    switch (buf[3] & 3) {
+    case 0:
+      if (buf[3] == 0x60)
+        event->key = TB_KEY_MOUSE_WHEEL_UP;
+      else
+        event->key = TB_KEY_MOUSE_LEFT;
+      break;
+    case 1:
+      if (buf[3] == 0x61)
+        event->key = TB_KEY_MOUSE_WHEEL_DOWN;
+      else
+        event->key = TB_KEY_MOUSE_MIDDLE;
+      break;
+    case 2:
+      event->key = TB_KEY_MOUSE_RIGHT;
+      break;
+    case 3:
+      event->key = TB_KEY_MOUSE_RELEASE;
+      break;
+    default:
+      parse_attempts = 0;
+      return -6;
+    }
+    event->type = TB_EVENT_MOUSE; // TB_EVENT_KEY by default
+
+    // the coord is 1,1 for upper left
+    event->x = (uint8_t)buf[4] - 1 - 32;
+    event->y = (uint8_t)buf[5] - 1 - 32;
+
+    parse_attempts = 0;
+    return 6;
+  }
+
+  // it's pretty simple here, find 'starts_with' match and return
+  // success, else return failure
+  int i;
+  for (i = 0; keys[i]; i++) {
+    if (starts_with(buf, len, keys[i])) {
+      event->ch = 0;
+      event->key = 0xFFFF-i;
+      parse_attempts = 0;
+      return strlen(keys[i]);
+    }
+  }
+
+  if (starts_with(buf, len, "\033[200~")) {
+    event->ch = 0;
+    event->key = TB_KEY_START_PASTE;
+    parse_attempts = 0;
+    return strlen("\033[200~");
+  }
+  if (starts_with(buf, len, "\033[201~")) {
+    event->ch = 0;
+    event->key = TB_KEY_END_PASTE;
+    parse_attempts = 0;
+    return strlen("\033[201~");
+  }
+  if (starts_with(buf, len, "\033[1;5A")) {
+    event->ch = 0;
+    event->key = TB_KEY_CTRL_ARROW_UP;
+    parse_attempts = 0;
+    return strlen("\033[1;5A");
+  }
+  if (starts_with(buf, len, "\033[1;5B")) {
+    event->ch = 0;
+    event->key = TB_KEY_CTRL_ARROW_DOWN;
+    parse_attempts = 0;
+    return strlen("\033[1;5B");
+  }
+  if (starts_with(buf, len, "\033[1;5C")) {
+    event->ch = 0;
+    event->key = TB_KEY_CTRL_ARROW_RIGHT;
+    parse_attempts = 0;
+    return strlen("\033[1;5C");
+  }
+  if (starts_with(buf, len, "\033[1;5D")) {
+    event->ch = 0;
+    event->key = TB_KEY_CTRL_ARROW_LEFT;
+    parse_attempts = 0;
+    return strlen("\033[1;5D");
+  }
+  if (starts_with(buf, len, "\033[Z")) {
+    event->ch = 0;
+    event->key = TB_KEY_SHIFT_TAB;
+    parse_attempts = 0;
+    return strlen("\033[Z");
+  }
+
+  // no escape sequence recognized? wait a bit in case our buffer is incomplete
+  ++parse_attempts;
+  if (parse_attempts < MAX_PARSE_ATTEMPTS) return 0;
+  // still nothing? give up and consume just the esc
+  event->ch = 0;
+  event->key = TB_KEY_ESC;
+  parse_attempts = 0;
+  return 1;
+}
+
+static bool extract_event(struct tb_event *event, struct bytebuffer *inbuf)
+{
+  const char *buf = inbuf->buf;
+  const int len = inbuf->len;
+  if (len == 0)
+    return false;
+
+//?   int x = 0;
+//?   FOO("== %d\n", len);
+//?   for (x = 0; x < len; ++x) {
+//?     FOO("%x\n", (unsigned char)buf[x]);
+//?   }
+  if (buf[0] == '\033') {
+    int n = parse_escape_seq(event, buf, len);
+    if (n == 0) return false;
+//?     FOO("parsed: %u %u %u %u\n", n, (unsigned int)event->type, (unsigned int)event->key, event->ch);
+    bool success = true;
+    if (n < 0) {
+      success = false;
+      n = -n;
+    }
+    bytebuffer_truncate(inbuf, n);
+    return success;
+  }
+
+  // if we're here, this is not an escape sequence and not an alt sequence
+  // so, it's a FUNCTIONAL KEY or a UNICODE character
+
+  // first of all check if it's a functional key
+  if ((unsigned char)buf[0] <= TB_KEY_SPACE ||
+      (unsigned char)buf[0] == TB_KEY_BACKSPACE2)
+  {
+    // fill event, pop buffer, return success */
+    event->ch = 0;
+    event->key = (uint16_t)buf[0];
+    bytebuffer_truncate(inbuf, 1);
+    return true;
+  }
+
+  // feh... we got utf8 here
+
+  // check if there is all bytes
+  if (len >= tb_utf8_char_length(buf[0])) {
+    /* everything ok, fill event, pop buffer, return success */
+    tb_utf8_char_to_unicode(&event->ch, buf);
+    event->key = 0;
+    bytebuffer_truncate(inbuf, tb_utf8_char_length(buf[0]));
+    return true;
+  }
+
+  // event isn't recognized, perhaps there is not enough bytes in utf8
+  // sequence
+  return false;
+}
diff --git a/archive/2.vm/termbox/output.inl b/archive/2.vm/termbox/output.inl
new file mode 100644
index 00000000..d87049ef
--- /dev/null
+++ b/archive/2.vm/termbox/output.inl
@@ -0,0 +1,320 @@
+enum {
+  T_ENTER_CA,
+  T_EXIT_CA,
+  T_SHOW_CURSOR,
+  T_HIDE_CURSOR,
+  T_CLEAR_SCREEN,
+  T_SGR0,
+  T_UNDERLINE,
+  T_BOLD,
+  T_BLINK,
+  T_REVERSE,
+  T_ENTER_KEYPAD,
+  T_EXIT_KEYPAD,
+  T_ENTER_MOUSE,
+  T_EXIT_MOUSE,
+  T_ENTER_BRACKETED_PASTE,
+  T_EXIT_BRACKETED_PASTE,
+  T_FUNCS_NUM,
+};
+
+#define EUNSUPPORTED_TERM -1
+
+// rxvt-256color
+static const char *rxvt_256color_keys[] = {
+  "\033[11~", "\033[12~", "\033[13~", "\033[14~", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[7~", "\033[8~", "\033[5~", "\033[6~", "\033[A", "\033[B", "\033[D", "\033[C", 0
+};
+static const char *rxvt_256color_funcs[] = {
+  "\0337\033[?47h", "\033[2J\033[?47l\0338", "\033[?25h", "\033[?25l", "\033[H\033[2J", "\033[m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "\033=", "\033>", "\033[?1000h", "\033[?1000l", "\033[?2004h", "\033[?2004l",
+};
+
+// Eterm
+static const char *eterm_keys[] = {
+  "\033[11~", "\033[12~", "\033[13~", "\033[14~", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[7~", "\033[8~", "\033[5~", "\033[6~", "\033[A", "\033[B", "\033[D", "\033[C", 0
+};
+static const char *eterm_funcs[] = {
+  "\0337\033[?47h", "\033[2J\033[?47l\0338", "\033[?25h", "\033[?25l", "\033[H\033[2J", "\033[m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "", "", "", "", "", "",
+};
+
+// screen
+static const char *screen_keys[] = {
+  "\033OP", "\033OQ", "\033OR", "\033OS", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[1~", "\033[4~", "\033[5~", "\033[6~", "\033OA", "\033OB", "\033OD", "\033OC", 0
+};
+static const char *screen_funcs[] = {
+  "\033[?1049h", "\033[?1049l", "\033[34h\033[?25h", "\033[?25l", "\033[H\033[J", "\033[m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "\033[?1h\033=", "\033[?1l\033>", "\033[?1000h", "\033[?1000l", "\033[?2004h", "\033[?2004l",
+};
+
+// rxvt-unicode
+static const char *rxvt_unicode_keys[] = {
+  "\033[11~", "\033[12~", "\033[13~", "\033[14~", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[7~", "\033[8~", "\033[5~", "\033[6~", "\033[A", "\033[B", "\033[D", "\033[C", 0
+};
+static const char *rxvt_unicode_funcs[] = {
+  "\033[?1049h", "\033[r\033[?1049l", "\033[?25h", "\033[?25l", "\033[H\033[2J", "\033[m\033(B", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "\033=", "\033>", "\033[?1000h", "\033[?1000l", "\033[?2004h", "\033[?2004l",
+};
+
+// linux
+static const char *linux_keys[] = {
+  "\033[[A", "\033[[B", "\033[[C", "\033[[D", "\033[[E", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[1~", "\033[4~", "\033[5~", "\033[6~", "\033[A", "\033[B", "\033[D", "\033[C", 0
+};
+static const char *linux_funcs[] = {
+  "", "", "\033[?25h\033[?0c", "\033[?25l\033[?1c", "\033[H\033[J", "\033[0;10m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "", "", "", "", "", "",
+};
+
+// xterm
+static const char *xterm_keys[] = {
+  "\033OP", "\033OQ", "\033OR", "\033OS", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033OH", "\033OF", "\033[5~", "\033[6~", "\033OA", "\033OB", "\033OD", "\033OC", 0
+};
+static const char *xterm_funcs[] = {
+  "\033[?1049h", "\033[?1049l", "\033[?12l\033[?25h", "\033[?25l", "\033[H\033[2J", "\033(B\033[m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "\033[?1h\033=", "\033[?1l\033>", "\033[?1000h", "\033[?1000l", "\033[?2004h", "\033[?2004l",
+};
+
+static struct term {
+  const char *name;
+  const char **keys;
+  const char **funcs;
+} terms[] = {
+  {"rxvt-256color", rxvt_256color_keys, rxvt_256color_funcs},
+  {"Eterm", eterm_keys, eterm_funcs},
+  {"screen", screen_keys, screen_funcs},
+  {"rxvt-unicode", rxvt_unicode_keys, rxvt_unicode_funcs},
+  {"linux", linux_keys, linux_funcs},
+  {"xterm", xterm_keys, xterm_funcs},
+  {0, 0, 0},
+};
+
+static bool init_from_terminfo = false;
+static const char **keys;
+static const char **funcs;
+
+static int try_compatible(const char *term, const char *name,
+        const char **tkeys, const char **tfuncs)
+{
+  if (strstr(term, name)) {
+    keys = tkeys;
+    funcs = tfuncs;
+    return 0;
+  }
+
+  return EUNSUPPORTED_TERM;
+}
+
+static int init_term_builtin(void)
+{
+  int i;
+  const char *term = getenv("TERM");
+
+  if (term) {
+    for (i = 0; terms[i].name; i++) {
+      if (!strcmp(terms[i].name, term)) {
+        keys = terms[i].keys;
+        funcs = terms[i].funcs;
+        return 0;
+      }
+    }
+
+    /* let's do some heuristic, maybe it's a compatible terminal */
+    if (try_compatible(term, "xterm", xterm_keys, xterm_funcs) == 0)
+      return 0;
+    if (try_compatible(term, "rxvt", rxvt_unicode_keys, rxvt_unicode_funcs) == 0)
+      return 0;
+    if (try_compatible(term, "linux", linux_keys, linux_funcs) == 0)
+      return 0;
+    if (try_compatible(term, "Eterm", eterm_keys, eterm_funcs) == 0)
+      return 0;
+    if (try_compatible(term, "screen", screen_keys, screen_funcs) == 0)
+      return 0;
+    /* let's assume that 'cygwin' is xterm compatible */
+    if (try_compatible(term, "cygwin", xterm_keys, xterm_funcs) == 0)
+      return 0;
+  }
+
+  return EUNSUPPORTED_TERM;
+}
+
+//----------------------------------------------------------------------
+// terminfo
+//----------------------------------------------------------------------
+
+static char *read_file(const char *file) {
+  FILE *f = fopen(file, "rb");
+  if (!f)
+    return 0;
+
+  struct stat st;
+  if (fstat(fileno(f), &st) != 0) {
+    fclose(f);
+    return 0;
+  }
+
+  char *data = malloc(st.st_size);
+  if (!data) {
+    fclose(f);
+    return 0;
+  }
+
+  if (fread(data, 1, st.st_size, f) != (size_t)st.st_size) {
+    fclose(f);
+    free(data);
+    return 0;
+  }
+
+  fclose(f);
+  return data;
+}
+
+static char *terminfo_try_path(const char *path, const char *term) {
+  char tmp[4096];
+  // snprintf guarantee for older compilers
+  assert(sizeof(tmp) > sizeof(path)+sizeof("/x/")+sizeof(term)+1);
+  snprintf(tmp, sizeof(tmp), "%s/%c/%s", path, term[0], term);
+  char *data = read_file(tmp);
+  if (data) {
+    return data;
+  }
+
+  // fallback to darwin specific dirs structure
+  // snprintf guarantee above still applies
+  snprintf(tmp, sizeof(tmp), "%s/%x/%s", path, term[0], term);
+  return read_file(tmp);
+}
+
+void string_copy(char* dest, const char* src, int dest_capacity) {
+  strncpy(dest, src, dest_capacity);
+  dest[dest_capacity-1] = '\0';
+}
+
+void string_append(char* dest, const char* src, int dest_capacity) {
+  strncat(dest, src, dest_capacity);
+  dest[dest_capacity-1] = '\0';
+}
+
+static char *load_terminfo(void) {
+  char tmp[4096];
+  const char *term = getenv("TERM");
+  if (!term) {
+    return 0;
+  }
+
+  // if TERMINFO is set, no other directory should be searched
+  const char *terminfo = getenv("TERMINFO");
+  if (terminfo) {
+    return terminfo_try_path(terminfo, term);
+  }
+
+  // next, consider ~/.terminfo
+  const char *home = getenv("HOME");
+  if (home) {
+    // snprintf guarantee for older compilers
+    assert(sizeof(tmp) > sizeof(home)+sizeof("/.terminfo")+1);
+    string_copy(tmp, home, sizeof(tmp));
+    string_append(tmp, "/.terminfo", sizeof(tmp));
+    char *data = terminfo_try_path(tmp, term);
+    if (data)
+      return data;
+  }
+
+  // next, TERMINFO_DIRS
+  const char *dirs = getenv("TERMINFO_DIRS");
+  if (dirs) {
+    // snprintf guarantee for older compilers
+    assert(sizeof(tmp) > sizeof(dirs));
+    strncpy(tmp, dirs, sizeof(tmp));
+    char *dir = strtok(tmp, ":");
+    while (dir) {
+      const char *cdir = dir;
+      if (strcmp(cdir, "") == 0) {
+        cdir = "/usr/share/terminfo";
+      }
+      char *data = terminfo_try_path(cdir, term);
+      if (data)
+        return data;
+      dir = strtok(0, ":");
+    }
+  }
+
+  // fallback to /usr/share/terminfo
+  return terminfo_try_path("/usr/share/terminfo", term);
+}
+
+#define TI_MAGIC 0432
+#define TI_HEADER_LENGTH 12
+#define TB_KEYS_NUM 22
+
+static const char *terminfo_copy_string(char *data, int str, int table) {
+  const int16_t off = *(int16_t*)(data + str);
+  const char *src = data + table + off;
+  int len = strlen(src);
+  char *dst = malloc(len+1);
+  string_copy(dst, src, len+1);
+  return dst;
+}
+
+static const int16_t ti_funcs[] = {
+  28, 40, 16, 13, 5, 39, 36, 27, 26, 34, 89, 88,
+};
+
+static const int16_t ti_keys[] = {
+  66, 68 /* apparently not a typo; 67 is F10 for whatever reason */, 69,
+  70, 71, 72, 73, 74, 75, 67, 216, 217, 77, 59, 76, 164, 82, 81, 87, 61,
+  79, 83,
+};
+
+static int init_term(void) {
+  int i;
+  char *data = load_terminfo();
+  if (!data) {
+    init_from_terminfo = false;
+    return init_term_builtin();
+  }
+
+  int16_t *header = (int16_t*)data;
+  if ((header[1] + header[2]) % 2) {
+    // old quirk to align everything on word boundaries
+    header[2] += 1;
+  }
+
+  const int str_offset = TI_HEADER_LENGTH +
+    header[1] + header[2] + 2 * header[3];
+  const int table_offset = str_offset + 2 * header[4];
+
+  keys = malloc(sizeof(const char*) * (TB_KEYS_NUM+1));
+  for (i = 0; i < TB_KEYS_NUM; i++) {
+    keys[i] = terminfo_copy_string(data,
+      str_offset + 2 * ti_keys[i], table_offset);
+  }
+  keys[TB_KEYS_NUM] = 0;
+
+  funcs = malloc(sizeof(const char*) * T_FUNCS_NUM);
+  // the last four entries are reserved for mouse, bracketed paste. because the table offset is
+  // not there, the two entries have to fill in manually
+  for (i = 0; i < T_FUNCS_NUM-4; i++) {
+    funcs[i] = terminfo_copy_string(data,
+      str_offset + 2 * ti_funcs[i], table_offset);
+  }
+
+  funcs[T_FUNCS_NUM-4] = "\033[?1000h";
+  funcs[T_FUNCS_NUM-3] = "\033[?1000l";
+  funcs[T_FUNCS_NUM-2] = "\033[?2004h";
+  funcs[T_FUNCS_NUM-1] = "\033[?2004l";
+
+  init_from_terminfo = true;
+  free(data);
+  return 0;
+}
+
+static void shutdown_term(void) {
+  if (init_from_terminfo) {
+    int i;
+    for (i = 0; i < TB_KEYS_NUM; i++) {
+      free((void*)keys[i]);
+    }
+    // the last four entries are reserved for mouse, bracketed paste. because the table offset
+    // is not there, the two entries have to fill in manually and do not
+    // need to be freed.
+    for (i = 0; i < T_FUNCS_NUM-4; i++) {
+      free((void*)funcs[i]);
+    }
+    free(keys);
+    free(funcs);
+  }
+}
diff --git a/archive/2.vm/termbox/termbox.c b/archive/2.vm/termbox/termbox.c
new file mode 100644
index 00000000..c97f03d5
--- /dev/null
+++ b/archive/2.vm/termbox/termbox.c
@@ -0,0 +1,397 @@
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+#include <sys/stat.h>
+#include <termios.h>
+#include <unistd.h>
+#include <wchar.h>
+/* hack: we can't define _XOPEN_SOURCE because that causes OpenBSD to not
+ * include SIGWINCH. But then this prototype is not included on Linux,
+ * triggering a warning. */
+extern int wcwidth (wchar_t);
+
+#include "termbox.h"
+
+#include "bytebuffer.inl"
+#include "output.inl"
+#include "input.inl"
+
+#define LAST_COORD_INIT -1
+
+static struct termios orig_tios;
+
+static struct bytebuffer output_buffer;
+static struct bytebuffer input_buffer;
+
+static int termw = -1;
+static int termh = -1;
+
+static int inout;
+static int winch_fds[2];
+
+static int cursor_x = 0;
+static int cursor_y = 0;
+
+static uint16_t background = TB_BLACK;
+static uint16_t foreground = TB_WHITE;
+
+static void update_size(void);
+static void update_term_size(void);
+static void send_attr(uint16_t fg, uint16_t bg);
+static void send_clear(void);
+static void sigwinch_handler(int xxx);
+static int wait_fill_event(struct tb_event *event, struct timeval *timeout);
+
+/* may happen in a different thread */
+static volatile int buffer_size_change_request;
+
+/* -------------------------------------------------------- */
+
+int tb_init(void)
+{
+  inout = open("/dev/tty", O_RDWR);
+  if (inout == -1) {
+    return TB_EFAILED_TO_OPEN_TTY;
+  }
+
+  if (init_term() < 0) {
+    close(inout);
+    return TB_EUNSUPPORTED_TERMINAL;
+  }
+
+  if (pipe(winch_fds) < 0) {
+    close(inout);
+    return TB_EPIPE_TRAP_ERROR;
+  }
+
+  struct sigaction sa;
+  memset(&sa, 0, sizeof(sa));
+  sa.sa_handler = sigwinch_handler;
+  sa.sa_flags = 0;
+  sigaction(SIGWINCH, &sa, 0);
+
+  tcgetattr(inout, &orig_tios);
+
+  struct termios tios;
+  memcpy(&tios, &orig_tios, sizeof(tios));
+
+  tios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
+                           | INLCR | IGNCR | ICRNL | IXON);
+  tios.c_oflag &= ~OPOST;
+  tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
+  tios.c_cflag &= ~(CSIZE | PARENB);
+  tios.c_cflag |= CS8;
+  tios.c_cc[VMIN] = 0;
+  tios.c_cc[VTIME] = 0;
+  tcsetattr(inout, TCSAFLUSH, &tios);
+
+  bytebuffer_init(&input_buffer, 128);
+  bytebuffer_init(&output_buffer, 32 * 1024);
+
+  bytebuffer_puts(&output_buffer, funcs[T_ENTER_KEYPAD]);
+  bytebuffer_puts(&output_buffer, funcs[T_ENTER_MOUSE]);
+  bytebuffer_puts(&output_buffer, funcs[T_ENTER_BRACKETED_PASTE]);
+  bytebuffer_flush(&output_buffer, inout);
+
+  update_term_size();
+  return 0;
+}
+
+void tb_shutdown(void)
+{
+  if (termw == -1) return;
+
+  bytebuffer_puts(&output_buffer, funcs[T_SGR0]);
+  bytebuffer_puts(&output_buffer, funcs[T_EXIT_KEYPAD]);
+  bytebuffer_puts(&output_buffer, funcs[T_EXIT_MOUSE]);
+  bytebuffer_puts(&output_buffer, funcs[T_EXIT_BRACKETED_PASTE]);
+  bytebuffer_flush(&output_buffer, inout);
+  tcsetattr(inout, TCSAFLUSH, &orig_tios);
+
+  shutdown_term();
+  close(inout);
+  close(winch_fds[0]);
+  close(winch_fds[1]);
+
+  bytebuffer_free(&output_buffer);
+  bytebuffer_free(&input_buffer);
+  termw = termh = -1;
+}
+
+int tb_is_active(void)
+{
+  return termw != -1;
+}
+
+void tb_print(uint32_t ch, uint16_t fg, uint16_t bg)
+{
+  assert(termw != -1);
+  send_attr(fg, bg);
+  if (ch == 0) {
+    // replace 0 with whitespace
+    bytebuffer_puts(&output_buffer, " ");
+  }
+  else {
+    char buf[7];
+    int bw = tb_utf8_unicode_to_char(buf, ch);
+    buf[bw] = '\0';
+    bytebuffer_puts(&output_buffer, buf);
+  }
+  bytebuffer_flush(&output_buffer, inout);
+}
+
+int tb_poll_event(struct tb_event *event)
+{
+  assert(termw != -1);
+  return wait_fill_event(event, 0);
+}
+
+int tb_peek_event(struct tb_event *event, int timeout)
+{
+  struct timeval tv;
+  tv.tv_sec = timeout / 1000;
+  tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000;
+  assert(termw != -1);
+  return wait_fill_event(event, &tv);
+}
+
+int tb_width(void)
+{
+  assert(termw != -1);
+  return termw;
+}
+
+int tb_height(void)
+{
+  assert(termw != -1);
+  return termh;
+}
+
+void tb_clear(void)
+{
+  assert(termw != -1);
+  if (buffer_size_change_request) {
+    update_size();
+    buffer_size_change_request = 0;
+  }
+  send_clear();
+}
+
+void tb_set_clear_attributes(uint16_t fg, uint16_t bg)
+{
+  assert(termw != -1);
+  foreground = fg;
+  background = bg;
+}
+
+/* -------------------------------------------------------- */
+
+static int convertnum(uint32_t num, char* buf) {
+  int i, l = 0;
+  int ch;
+  do {
+    buf[l++] = '0' + (num % 10);
+    num /= 10;
+  } while (num);
+  for(i = 0; i < l / 2; i++) {
+    ch = buf[i];
+    buf[i] = buf[l - 1 - i];
+    buf[l - 1 - i] = ch;
+  }
+  return l;
+}
+
+#define WRITE_LITERAL(X) bytebuffer_append(&output_buffer, (X), sizeof(X)-1)
+#define WRITE_INT(X) bytebuffer_append(&output_buffer, buf, convertnum((X), buf))
+
+void tb_set_cursor(int x, int y) {
+  char buf[32];
+  WRITE_LITERAL("\033[");
+  WRITE_INT(y+1);
+  WRITE_LITERAL(";");
+  WRITE_INT(x+1);
+  WRITE_LITERAL("H");
+  bytebuffer_flush(&output_buffer, inout);
+}
+
+static void get_term_size(int *w, int *h)
+{
+  struct winsize sz;
+  memset(&sz, 0, sizeof(sz));
+
+  ioctl(inout, TIOCGWINSZ, &sz);
+
+  if (w) *w = sz.ws_col;
+  if (h) *h = sz.ws_row;
+}
+
+static void update_term_size(void)
+{
+  struct winsize sz;
+  memset(&sz, 0, sizeof(sz));
+
+  ioctl(inout, TIOCGWINSZ, &sz);
+
+  termw = sz.ws_col;
+  termh = sz.ws_row;
+}
+
+static void send_attr(uint16_t fg, uint16_t bg)
+{
+#define LAST_ATTR_INIT 0xFFFF
+  static uint16_t lastfg = LAST_ATTR_INIT, lastbg = LAST_ATTR_INIT;
+  if (fg != lastfg || bg != lastbg) {
+    bytebuffer_puts(&output_buffer, funcs[T_SGR0]);
+
+    uint16_t fgcol = fg & 0xFF;
+    uint16_t bgcol = bg & 0xFF;
+
+    if (fg & TB_BOLD)
+      bytebuffer_puts(&output_buffer, funcs[T_BOLD]);
+    if (bg & TB_BOLD)
+      bytebuffer_puts(&output_buffer, funcs[T_BLINK]);
+    if (fg & TB_UNDERLINE)
+      bytebuffer_puts(&output_buffer, funcs[T_UNDERLINE]);
+    if ((fg & TB_REVERSE) || (bg & TB_REVERSE))
+      bytebuffer_puts(&output_buffer, funcs[T_REVERSE]);
+    char buf[32];
+    WRITE_LITERAL("\033[38;5;");
+    WRITE_INT(fgcol);
+    WRITE_LITERAL("m");
+    WRITE_LITERAL("\033[48;5;");
+    WRITE_INT(bgcol);
+    WRITE_LITERAL("m");
+    bytebuffer_flush(&output_buffer, inout);
+    lastfg = fg;
+    lastbg = bg;
+  }
+}
+
+const char* to_unicode(uint32_t c)
+{
+  static char buf[7];
+  int bw = tb_utf8_unicode_to_char(buf, c);
+  buf[bw] = '\0';
+  return buf;
+}
+
+static void send_clear(void)
+{
+  send_attr(foreground, background);
+  bytebuffer_puts(&output_buffer, funcs[T_CLEAR_SCREEN]);
+  tb_set_cursor(cursor_x, cursor_y);
+  bytebuffer_flush(&output_buffer, inout);
+}
+
+static void sigwinch_handler(int xxx)
+{
+  (void) xxx;
+  const int zzz = 1;
+  int yyy = write(winch_fds[1], &zzz, sizeof(int));
+  (void) yyy;
+}
+
+static void update_size(void)
+{
+  update_term_size();
+  send_clear();
+}
+
+static int read_up_to(int n) {
+  assert(n > 0);
+  const int prevlen = input_buffer.len;
+  bytebuffer_resize(&input_buffer, prevlen + n);
+
+  int read_n = 0;
+  while (read_n <= n) {
+    ssize_t r = 0;
+    if (read_n < n) {
+      r = read(inout, input_buffer.buf + prevlen + read_n, n - read_n);
+    }
+#ifdef __CYGWIN__
+    // While linux man for tty says when VMIN == 0 && VTIME == 0, read
+    // should return 0 when there is nothing to read, cygwin's read returns
+    // -1. Not sure why and if it's correct to ignore it, but let's pretend
+    // it's zero.
+    if (r < 0) r = 0;
+#endif
+    if (r < 0) {
+      // EAGAIN / EWOULDBLOCK shouldn't occur here
+      assert(errno != EAGAIN && errno != EWOULDBLOCK);
+      return -1;
+    } else if (r > 0) {
+      read_n += r;
+    } else {
+      bytebuffer_resize(&input_buffer, prevlen + read_n);
+      return read_n;
+    }
+  }
+  assert(!"unreachable");
+  return 0;
+}
+
+int tb_event_ready(void)
+{
+  return input_buffer.len > 0;
+}
+
+static int wait_fill_event(struct tb_event *event, struct timeval *timeout)
+{
+  // ;-)
+#define ENOUGH_DATA_FOR_PARSING 64
+  fd_set events;
+  memset(event, 0, sizeof(struct tb_event));
+
+  // try to extract event from input buffer, return on success
+  event->type = TB_EVENT_KEY;
+  if (extract_event(event, &input_buffer))
+    return event->type;
+
+  // it looks like input buffer is incomplete, let's try the short path,
+  // but first make sure there is enough space
+  int n = read_up_to(ENOUGH_DATA_FOR_PARSING);
+  if (n < 0)
+    return -1;
+  if (n > 0 && extract_event(event, &input_buffer))
+    return event->type;
+
+  // n == 0, or not enough data, let's go to select
+  while (1) {
+    FD_ZERO(&events);
+    FD_SET(inout, &events);
+    FD_SET(winch_fds[0], &events);
+    int maxfd = (winch_fds[0] > inout) ? winch_fds[0] : inout;
+    int result = select(maxfd+1, &events, 0, 0, timeout);
+    if (!result)
+      return 0;
+
+    if (FD_ISSET(inout, &events)) {
+      event->type = TB_EVENT_KEY;
+      n = read_up_to(ENOUGH_DATA_FOR_PARSING);
+      if (n < 0)
+        return -1;
+
+      if (n == 0)
+        continue;
+
+      if (extract_event(event, &input_buffer))
+        return event->type;
+    }
+    if (FD_ISSET(winch_fds[0], &events)) {
+      event->type = TB_EVENT_RESIZE;
+      int zzz = 0;
+      int yyy = read(winch_fds[0], &zzz, sizeof(int));
+      (void) yyy;
+      buffer_size_change_request = 1;
+      get_term_size(&event->w, &event->h);
+      return TB_EVENT_RESIZE;
+    }
+  }
+}
diff --git a/archive/2.vm/termbox/termbox.h b/archive/2.vm/termbox/termbox.h
new file mode 100644
index 00000000..43b326cb
--- /dev/null
+++ b/archive/2.vm/termbox/termbox.h
@@ -0,0 +1,190 @@
+#pragma once
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*** 1. Controlling the screen. */
+
+/* Names for some foreground/background colors. */
+#define TB_BLACK 232
+#define TB_WHITE 255
+
+/* Some attributes of screen cells that can be combined with colors using
+ * bitwise-OR. */
+#define TB_BOLD      0x0100
+#define TB_UNDERLINE 0x0200
+#define TB_REVERSE   0x0400
+
+/* Initialize screen and keyboard. */
+int tb_init(void);
+/* Possible error codes returned by tb_init() */
+#define TB_EUNSUPPORTED_TERMINAL -1
+#define TB_EFAILED_TO_OPEN_TTY   -2
+/* Termbox uses unix pipes in order to deliver a message from a signal handler
+ * (SIGWINCH) to the main event reading loop. */
+#define TB_EPIPE_TRAP_ERROR      -3
+
+/* Restore terminal mode. */
+void tb_shutdown(void);
+
+int tb_is_active(void);
+
+/* Size of the screen. Return negative values before tb_init() or after
+ * tb_shutdown() */
+int tb_width(void);
+int tb_height(void);
+
+/* Clear the screen using either TB_DEFAULT or the color/attributes set by
+ * tb_set_clear_attributes(). */
+void tb_clear(void);
+void tb_set_clear_attributes(uint16_t fg, uint16_t bg);
+
+/* Move the cursor. Upper-left character is (0, 0). */
+void tb_set_cursor(int cx, int cy);
+
+/* Modify the screen at the cursor. */
+void tb_print(uint32_t ch, uint16_t fg, uint16_t bg);
+
+/*** 2. Controlling keyboard events. */
+
+struct tb_event {
+  uint8_t type;
+  /* fields for type TB_EVENT_KEY. At most one of 'key' and 'ch' will be set at
+   * any time. */
+  uint16_t key;
+  uint32_t ch;
+  /* fields for type TB_EVENT_RESIZE */
+  int32_t w;
+  int32_t h;
+  /* fields for type TB_EVENT_MOUSE */
+  int32_t x;
+  int32_t y;
+};
+
+/* Possible values for tb_event.type. */
+#define TB_EVENT_KEY    1
+#define TB_EVENT_RESIZE 2
+#define TB_EVENT_MOUSE  3
+
+/* Possible values for tb_event.key. */
+#define TB_KEY_F1               (0xFFFF-0)
+#define TB_KEY_F2               (0xFFFF-1)
+#define TB_KEY_F3               (0xFFFF-2)
+#define TB_KEY_F4               (0xFFFF-3)
+#define TB_KEY_F5               (0xFFFF-4)
+#define TB_KEY_F6               (0xFFFF-5)
+#define TB_KEY_F7               (0xFFFF-6)
+#define TB_KEY_F8               (0xFFFF-7)
+#define TB_KEY_F9               (0xFFFF-8)
+#define TB_KEY_F10              (0xFFFF-9)
+#define TB_KEY_F11              (0xFFFF-10)
+#define TB_KEY_F12              (0xFFFF-11)
+#define TB_KEY_INSERT           (0xFFFF-12)
+#define TB_KEY_DELETE           (0xFFFF-13)
+#define TB_KEY_HOME             (0xFFFF-14)
+#define TB_KEY_END              (0xFFFF-15)
+#define TB_KEY_PGUP             (0xFFFF-16)
+#define TB_KEY_PGDN             (0xFFFF-17)
+#define TB_KEY_ARROW_UP         (0xFFFF-18)
+#define TB_KEY_ARROW_DOWN       (0xFFFF-19)
+#define TB_KEY_ARROW_LEFT       (0xFFFF-20)
+#define TB_KEY_ARROW_RIGHT      (0xFFFF-21)
+#define TB_KEY_MOUSE_LEFT       (0xFFFF-22)
+#define TB_KEY_MOUSE_RIGHT      (0xFFFF-23)
+#define TB_KEY_MOUSE_MIDDLE     (0xFFFF-24)
+#define TB_KEY_MOUSE_RELEASE    (0xFFFF-25)
+#define TB_KEY_MOUSE_WHEEL_UP   (0xFFFF-26)
+#define TB_KEY_MOUSE_WHEEL_DOWN (0xFFFF-27)
+#define TB_KEY_START_PASTE      (0xFFFF-28)
+#define TB_KEY_END_PASTE        (0xFFFF-29)
+#define TB_KEY_CTRL_ARROW_UP    (0xFFFF-30)
+#define TB_KEY_CTRL_ARROW_DOWN  (0xFFFF-31)
+#define TB_KEY_CTRL_ARROW_LEFT  (0xFFFF-32)
+#define TB_KEY_CTRL_ARROW_RIGHT (0xFFFF-33)
+#define TB_KEY_SHIFT_TAB        (0xFFFF-34)
+
+/* Names for some of the possible values for tb_event.ch. */
+/* These are all ASCII code points below SPACE character and a BACKSPACE key. */
+#define TB_KEY_CTRL_TILDE       0x00
+#define TB_KEY_CTRL_2           0x00 /* clash with 'CTRL_TILDE' */
+#define TB_KEY_CTRL_A           0x01
+#define TB_KEY_CTRL_B           0x02
+#define TB_KEY_CTRL_C           0x03
+#define TB_KEY_CTRL_D           0x04
+#define TB_KEY_CTRL_E           0x05
+#define TB_KEY_CTRL_F           0x06
+#define TB_KEY_CTRL_G           0x07
+#define TB_KEY_BACKSPACE        0x08
+#define TB_KEY_CTRL_H           0x08 /* clash with 'CTRL_BACKSPACE' */
+#define TB_KEY_TAB              0x09
+#define TB_KEY_CTRL_I           0x09 /* clash with 'TAB' */
+#define TB_KEY_CTRL_J           0x0A
+#define TB_KEY_CTRL_K           0x0B
+#define TB_KEY_CTRL_L           0x0C
+#define TB_KEY_ENTER            0x0D
+#define TB_KEY_CTRL_M           0x0D /* clash with 'ENTER' */
+#define TB_KEY_CTRL_N           0x0E
+#define TB_KEY_CTRL_O           0x0F
+#define TB_KEY_CTRL_P           0x10
+#define TB_KEY_CTRL_Q           0x11
+#define TB_KEY_CTRL_R           0x12
+#define TB_KEY_CTRL_S           0x13
+#define TB_KEY_CTRL_T           0x14
+#define TB_KEY_CTRL_U           0x15
+#define TB_KEY_CTRL_V           0x16
+#define TB_KEY_CTRL_W           0x17
+#define TB_KEY_CTRL_X           0x18
+#define TB_KEY_CTRL_Y           0x19
+#define TB_KEY_CTRL_Z           0x1A
+#define TB_KEY_ESC              0x1B
+#define TB_KEY_CTRL_LSQ_BRACKET 0x1B /* clash with 'ESC' */
+#define TB_KEY_CTRL_3           0x1B /* clash with 'ESC' */
+#define TB_KEY_CTRL_4           0x1C
+#define TB_KEY_CTRL_BACKSLASH   0x1C /* clash with 'CTRL_4' */
+#define TB_KEY_CTRL_5           0x1D
+#define TB_KEY_CTRL_RSQ_BRACKET 0x1D /* clash with 'CTRL_5' */
+#define TB_KEY_CTRL_6           0x1E
+#define TB_KEY_CTRL_7           0x1F
+#define TB_KEY_CTRL_SLASH       0x1F /* clash with 'CTRL_7' */
+#define TB_KEY_CTRL_UNDERSCORE  0x1F /* clash with 'CTRL_7' */
+#define TB_KEY_SPACE            0x20
+#define TB_KEY_BACKSPACE2       0x7F
+#define TB_KEY_CTRL_8           0x7F /* clash with 'DELETE' */
+/* These are non-existing ones.
+ *
+ * #define TB_KEY_CTRL_1 clash with '1'
+ * #define TB_KEY_CTRL_9 clash with '9'
+ * #define TB_KEY_CTRL_0 clash with '0'
+ */
+/* Some aliases */
+#define TB_KEY_NEWLINE TB_KEY_CTRL_J
+#define TB_KEY_CARRIAGE_RETURN TB_KEY_CTRL_M
+
+/* Wait for an event up to 'timeout' milliseconds and fill the 'event'
+ * structure with it, when the event is available. Returns the type of the
+ * event (one of TB_EVENT_* constants) or -1 if there was an error or 0 in case
+ * there were no event during 'timeout' period.
+ */
+int tb_peek_event(struct tb_event *event, int timeout);
+
+/* Wait for an event forever and fill the 'event' structure with it, when the
+ * event is available. Returns the type of the event (one of TB_EVENT_*
+ * constants) or -1 if there was an error.
+ */
+int tb_poll_event(struct tb_event *event);
+
+int tb_event_ready(void);
+
+/*** 3. Utility utf8 functions. */
+#define TB_EOF -1
+int tb_utf8_char_length(char c);
+int tb_utf8_char_to_unicode(uint32_t *out, const char *c);
+int tb_utf8_unicode_to_char(char *out, uint32_t c);
+const char* to_unicode(uint32_t c);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/archive/2.vm/termbox/utf8.c b/archive/2.vm/termbox/utf8.c
new file mode 100644
index 00000000..26c0c27b
--- /dev/null
+++ b/archive/2.vm/termbox/utf8.c
@@ -0,0 +1,79 @@
+#include "termbox.h"
+
+static const unsigned char utf8_length[256] = {
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+  3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,6,6,1,1
+};
+
+static const unsigned char utf8_mask[6] = {
+  0x7F,
+  0x1F,
+  0x0F,
+  0x07,
+  0x03,
+  0x01
+};
+
+int tb_utf8_char_length(char c)
+{
+  return utf8_length[(unsigned char)c];
+}
+
+int tb_utf8_char_to_unicode(uint32_t *out, const char *c)
+{
+  if (*c == 0)
+    return TB_EOF;
+
+  int i;
+  unsigned char len = tb_utf8_char_length(*c);
+  unsigned char mask = utf8_mask[len-1];
+  uint32_t result = c[0] & mask;
+  for (i = 1; i < len; ++i) {
+    result <<= 6;
+    result |= c[i] & 0x3f;
+  }
+
+  *out = result;
+  return (int)len;
+}
+
+int tb_utf8_unicode_to_char(char *out, uint32_t c)
+{
+  int len = 0;
+  int first;
+  int i;
+
+  if (c < 0x80) {
+    first = 0;
+    len = 1;
+  } else if (c < 0x800) {
+    first = 0xc0;
+    len = 2;
+  } else if (c < 0x10000) {
+    first = 0xe0;
+    len = 3;
+  } else if (c < 0x200000) {
+    first = 0xf0;
+    len = 4;
+  } else if (c < 0x4000000) {
+    first = 0xf8;
+    len = 5;
+  } else {
+    first = 0xfc;
+    len = 6;
+  }
+
+  for (i = len - 1; i > 0; --i) {
+    out[i] = (c & 0x3f) | 0x80;
+    c >>= 6;
+  }
+  out[0] = c | first;
+
+  return len;
+}
diff --git a/archive/2.vm/test_layers b/archive/2.vm/test_layers
new file mode 100755
index 00000000..47cb01b0
--- /dev/null
+++ b/archive/2.vm/test_layers
@@ -0,0 +1,88 @@
+#!/bin/bash
+# Repeatedly stop building until successive layers, and run all tests built,
+# while checking for undefined behavior using both UBSan and Valgrind.
+#
+# Requires Linux.
+#
+# Usage:
+#   Test all layers:
+#     test_layers
+#   Test non-app layers after x:
+#     test_layers x
+#   Test layers after x and until y (inclusive):
+#     test_layers x y
+#   Test all layers for a specific app:
+#     test_layers app
+set -e
+
+test "$BUILD" || export BUILD=build3
+
+if [[ $1 == one-off ]]
+then
+  ./$BUILD
+  ./mu_bin test || exit 1
+  exit 0
+fi
+
+# Core layers atop Valgrind
+for f in [0-9]*
+do
+  if [[ $f < $1 ]]; then continue; fi
+  if [[ $2 && $f > $2 ]]; then exit 0; fi
+  echo "=== $f"
+  ./clean top-level  # preserve subsidiary tools like tangle and cleave
+  ./$BUILD --until $f || exit 1
+  # valgrind requires Linux
+  valgrind --leak-check=yes --num-callers=40 -q --error-exitcode=1 ./mu_bin test || exit 1
+  # run on Mac OS without valgrind, and with a hacky fix for the coarser clock
+#?   ./mu_bin test || exit 1
+#?   sleep 1
+done
+
+# Layers for Mu apps without Valgrind
+./clean
+./$BUILD
+
+if [[ ! $1 || $1 == chessboard ]]
+then
+  echo "=== chessboard"
+  ./mu_bin test chessboard.mu || exit 1
+fi
+
+# slices of edit/ for Travis CI
+if [[ ! $1 || $1 == edit ]]
+then
+  echo "=== edit: until 001"
+  ./mu_bin test edit/001* || exit 1
+  echo "=== edit: until 002"
+  ./mu_bin test edit/00[1-2]* || exit 1
+  echo "=== edit: until 003"
+  ./mu_bin test edit/00[1-3]* || exit 1
+fi
+if [[ ! $1 || $1 == edit2 ]]
+then
+  echo "=== edit: until 004"
+  ./mu_bin test edit/00[1-4]* || exit 1
+  echo "=== edit: until 005"
+  ./mu_bin test edit/00[1-5]* || exit 1
+  echo "=== edit: until 006"
+  ./mu_bin test edit/00[1-6]* || exit 1
+fi
+if [[ ! $1 || $1 == edit3 ]]
+then
+  echo "=== edit: until 007"
+  ./mu_bin test edit/00[1-7]* || exit 1
+  echo "=== edit: until 008"
+  ./mu_bin test edit/00[1-8]* || exit 1
+  echo "=== edit: until 009"
+  ./mu_bin test edit/00* || exit 1
+fi
+if [[ ! $1 || $1 == edit4 ]]
+then
+  echo "=== edit: until 010"
+  ./mu_bin test edit/00* edit/010* || exit 1
+  echo "=== edit: until 011"
+  ./mu_bin test edit/00* edit/01[01]* || exit 1
+  echo "=== edit: until 012"
+  ./mu_bin test edit/00* edit/01[0-2]* || exit 1
+fi
diff --git a/archive/2.vm/vimrc.vim b/archive/2.vm/vimrc.vim
new file mode 100644
index 00000000..c63a8941
--- /dev/null
+++ b/archive/2.vm/vimrc.vim
@@ -0,0 +1,31 @@
+" Highlighting literate directives in C++ sources.
+function! HighlightTangledFile()
+  " Tangled comments only make sense in the sources and are stripped out of
+  " the generated .cc file. They're highlighted same as regular comments.
+  syntax match tangledComment /\/\/:.*/ | highlight link tangledComment Comment
+  syntax match tangledSalientComment /\/\/::.*/ | highlight link tangledSalientComment SalientComment
+  set comments-=://
+  set comments-=n://
+  set comments+=n://:,n://
+
+  " Inside tangle scenarios.
+  syntax region tangleDirective start=+:(+ skip=+".*"+ end=+)+
+  highlight link tangleDirective Delimiter
+  syntax match traceContains /^+.*/
+  highlight traceContains ctermfg=22
+  syntax match traceAbsent /^-.*/
+  highlight traceAbsent ctermfg=darkred
+  syntax match tangleScenarioSetup /^\s*% .*/ | highlight link tangleScenarioSetup SpecialChar
+  highlight Special ctermfg=160
+endfunction
+augroup LocalVimrc
+  autocmd BufRead,BufNewFile *.cc call HighlightTangledFile()
+  autocmd BufRead,BufNewFile *.mu set ft=mu
+augroup END
+
+" Scenarios considered:
+"   opening or starting vim with a new or existing file without an extension (should interpret as C++)
+"   opening or starting vim with a new or existing file with a .mu extension
+"   starting vim or opening a buffer without a file name (ok to do nothing)
+"   opening a second file in a new or existing window (shouldn't mess up existing highlighting)
+"   reloading an existing file (shouldn't mess up existing highlighting)
diff --git a/archive/2.vm/x.mu b/archive/2.vm/x.mu
new file mode 100644
index 00000000..f53a86ba
--- /dev/null
+++ b/archive/2.vm/x.mu
@@ -0,0 +1,8 @@
+# example program: add two numbers
+
+def main [
+  11:num <- copy 1
+  12:num <- copy 3
+  13:num <- add 11:num, 12:num
+  $dump-memory
+]
diff --git a/archive/3.transect/Readme b/archive/3.transect/Readme
new file mode 100644
index 00000000..821ccde0
--- /dev/null
+++ b/archive/3.transect/Readme
@@ -0,0 +1,6 @@
+Abortive series of attempts at building a bootstrappable low-level language.
+Type-checked, but feasible to implement in some sort of notation for machine
+code (like SubX).
+
+The latest version is in compiler10. But it doesn't account for checking
+how programs allocate registers.
diff --git a/archive/3.transect/build b/archive/3.transect/build
new file mode 100755
index 00000000..150c6b4d
--- /dev/null
+++ b/archive/3.transect/build
@@ -0,0 +1,109 @@
+#!/bin/sh
+# returns 0 on successful build or nothing to build
+# non-zero exit status only on error during building
+set -e  # stop immediately on error
+
+# [0-9]*.cc -> transect.cc -> transect_bin
+# (layers)   |              |
+#          tangle          $CXX
+
+# can also be called with a layer to only build until
+#   $ ./build --until 050
+UNTIL_LAYER=${2:-zzz}
+
+# there's two mechanisms for fast builds here:
+# - if a command is quick to run, always run it but update the result only on any change
+# - otherwise run it only if the output is 'older_than' the inputs
+#
+# avoid combining both mechanisms for a single file
+# otherwise you'll see spurious messages about files being updated
+# risk: a file may unnecessarily update without changes, causing unnecessary work downstream
+
+test "$CXX" || export CXX=c++
+test "$CC" || export CC=cc
+test "$CFLAGS" || export CFLAGS="-g -O3"
+export CFLAGS="$CFLAGS -Wall -Wextra -ftrapv -fno-strict-aliasing"
+
+# return 1 if $1 is older than _any_ of the remaining args
+older_than() {
+  local target=$1
+  shift
+  if [ ! -e $target ]
+  then
+#?     echo "$target doesn't exist"
+    echo "updating $target" >&2
+    return 0  # success
+  fi
+  local f
+  for f in $*
+  do
+    if [ $f -nt $target ]
+    then
+      echo "updating $target" >&2
+      return 0  # success
+    fi
+  done
+  return 1  # failure
+}
+
+# redirect to $1, unless it's already identical
+update() {
+  if [ ! -e $1 ]
+  then
+    cat > $1
+  else
+    cat > $1.tmp
+    diff -q $1 $1.tmp >/dev/null  &&  rm $1.tmp  ||  mv $1.tmp $1
+  fi
+}
+
+update_cp() {
+  if [ ! -e $2/$1 ]
+  then
+    cp $1 $2
+  elif [ $1 -nt $2/$1 ]
+  then
+    cp $1 $2
+  fi
+}
+
+noisy_cd() {
+  cd $1
+  echo "-- `pwd`" >&2
+}
+
+older_than ../enumerate/enumerate ../enumerate/enumerate.cc && {
+  $CXX $CFLAGS ../enumerate/enumerate.cc -o ../enumerate/enumerate
+}
+
+older_than ../tangle/tangle ../tangle/*.cc && {
+  noisy_cd ../tangle
+    {
+      grep -h "^struct .* {" [0-9]*.cc  |sed 's/\(struct *[^ ]*\).*/\1;/'
+      grep -h "^typedef " [0-9]*.cc
+    }  |update type_list
+    grep -h "^[^ #].*) {" [0-9]*.cc  |sed 's/ {.*/;/'  |update function_list
+    ls [0-9]*.cc  |grep -v "\.test\.cc$"  |sed 's/.*/#include "&"/'  |update file_list
+    ls [0-9]*.test.cc  |sed 's/.*/#include "&"/'  |update test_file_list
+    grep -h "^[[:space:]]*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {$/\1,/'  |update test_list
+    grep -h "^\s*void test_" [0-9]*.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update test_name_list
+    $CXX $CFLAGS boot.cc -o tangle
+    ./tangle test
+  noisy_cd ../transect  # no effect; just to show us returning to the parent directory
+}
+
+LAYERS=$(../enumerate/enumerate --until $UNTIL_LAYER  |grep '.cc$')
+older_than transect.cc $LAYERS ../enumerate/enumerate ../tangle/tangle && {
+  # no update here; rely on 'update' calls downstream
+  ../tangle/tangle $LAYERS  > transect.cc
+}
+
+grep -h "^[^[:space:]#].*) {$" transect.cc  |grep -v ":.*("  |sed 's/ {.*/;/'  |update function_list
+grep -h "^\s*void test_" transect.cc  |sed 's/^\s*void \(.*\)() {.*/\1,/'  |update test_list
+grep -h "^\s*void test_" transect.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update test_name_list
+
+older_than transect_bin transect.cc *_list && {
+  $CXX $CFLAGS transect.cc -o transect_bin
+}
+
+exit 0
diff --git a/archive/3.transect/build_and_test_until b/archive/3.transect/build_and_test_until
new file mode 100755
index 00000000..385558be
--- /dev/null
+++ b/archive/3.transect/build_and_test_until
@@ -0,0 +1,18 @@
+#!/bin/sh
+# Run tests for just a subset of layers.
+#
+# Usage:
+#   build_and_test_until [file prefix] [test name]
+# Provide the second arg to run just a single test.
+set -e
+
+# clean previous builds if they were building until a different layer
+touch .until
+PREV_UNTIL=`cat .until`
+if [ "$PREV_UNTIL" != $1 ]
+then
+  ./clean top-level
+  echo $1 > .until
+fi
+
+./build --until $1  &&  ./transect_bin test $2
diff --git a/archive/3.transect/clean b/archive/3.transect/clean
new file mode 100755
index 00000000..2c49d7d7
--- /dev/null
+++ b/archive/3.transect/clean
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -e
+
+set -v
+rm -rf transect.cc transect_bin* *_list
+test $# -gt 0 && exit 0  # convenience: 'clean top-level' to leave subsidiary tools alone
+rm -rf ../enumerate/enumerate ../tangle/tangle ../tangle/*_list ../*/*.dSYM
+rm -rf .until
diff --git a/archive/3.transect/compiler10 b/archive/3.transect/compiler10
new file mode 100644
index 00000000..ce0e487a
--- /dev/null
+++ b/archive/3.transect/compiler10
@@ -0,0 +1,304 @@
+=== Goal
+
+A memory-safe language with a simple translator to x86 that can be feasibly written without itself needing a translator/compiler.
+
+Memory-safe: it should be impossible to:
+  a) create a pointer out of arbitrary data, or
+  b) to access heap memory after it's been freed.
+
+Simple: do all the work in a 2-pass translator:
+  Pass 1: check each statement's types in isolation.
+  Pass 2: emit code for each statement in isolation.
+
+=== Language summary
+
+Program organization is going to be fairly conventional and in the spirit of C: programs will consist of a series of type, global and function declarations. More details below. Functions will consist of a list of statements, each containing a single operation. Since we try to map directly to x86 instructions, combinations of operations and operands will not be orthogonal. You won't be able to operate at once on two memory locations, for example, since no single x86 instruction can do that.
+
+Statement operands will be tagged with where they lie. This mostly follows C: local variables are on the stack, and variables not on the stack are in the global segment. The one addition is that you can lay out (only word-size) variables on registers. This is kinda like C's `register` keyword, but not quite: if you don't place a variable on a register, you are *guaranteed* it won't be allocated a register. Programmers do register allocation in this language.
+
+The other memorable feature of the language is two kinds of pointers: a 'ref' is a fat pointer manually allocated on the heap, and an 'address' is a far more ephemeral thing described below.
+
+--- Ref
+
+Refs are used to manage heap allocations. They are fat pointers that augment the address of a payload with an allocation id. On x86 a ref requires 8 bytes: 4 for the address, and 4 for the alloc id. Refs can only ever point to the start of a heap allocation. Never within a heap allocation, and *certainly* never to the stack or global segment.
+
+How alloc ids work: Every heap allocation allocates an additional word of space for an alloc id in the payload, and stores a unique alloc id in the payload as well as the pointer returned to the caller. Reclaiming an allocation resets the payload's alloc id. As long as alloc ids are always unique, and as long as refs can never point to within a heap allocation, we can be guaranteed that a stale pointer whose payload has been reclaimed will end up with a mismatch between pointer alloc id and payload alloc id.
+
+  x <- alloc   # x's alloc id and *x's alloc id will be the same, say A
+  y <- copy x  # y also has alloc id A
+  free x       # x's alloc id is now 0, as is *x's alloc id
+  ..*y..       # y's alloc id is A, but *y's alloc id is 0, so we can signal an error
+  z <- alloc   # say z reuses the same address, but now with a new alloc id A'
+  ..*y..       # y's alloc id is A, but *y's alloc id is A', so we can signal an error
+
+--- Address
+
+Since our statements are really simple, many operations may take multiple statements. To stitch a more complex computation like `A[i].f = 34` across multiple statements, we need addresses.
+
+Addresses can be used to manage any memory address. They can point inside objects, on the stack, heap or global segment. Since they are so powerful we greatly restrict their use. Addresses can only be stored in a register, never in memory on the stack or global segment. Since user-defined types will usually not fit on a register, we forbid addresses in any user-defined types. Since an address may point inside a heap allocation that can be freed, and since `free` will be a function call, addresses will not persist across function calls. Analyzing control flow to find intervening function calls can be complex, so addresses will not persist across basic block boundaries.
+
+The key open question with this language: can we find *clear* rules of address use that *don't complicate* programs, and that keep the type system *sound*?
+
+=== Language syntax
+
+The type system basically follows Hindley-Milner with product and (tagged) sum types. In addition we have address and ref types. Type declarations have the following syntax:
+
+  # product type
+  type foo [
+    x : int
+    y : (ref int)
+    z : bar
+  ]
+
+  # sum type
+  choice bar [
+    x : int
+    y : point
+  ]
+
+Functions have a header and a series of statements in the body:
+
+  fn f a : int b : int -> b : int [
+    ...
+  ]
+
+Statements have the following format:
+
+  io1, io2, ... <- operation i1, i2, ...
+
+i1, i2 operands on the right hand side are immutable. io1, io2 are in-out operands. They're written to, and may also be read.
+
+Two example programs:
+
+  i) Factorial:
+
+    fn factorial n : int -> result/EAX : int [
+      result/EAX <- copy 1
+      {
+        compare n, 1
+        break-if <=
+        var tmp/EBX : int
+        tmp/EBX <- copy n
+        tmp/EBX <- subtract 1
+        var tmp2/EAX : int
+        tmp2/EAX <- call factorial, tmp/EBX
+        result/EAX <- multiply tmp2/EAX, n
+      }
+      return result/EAX
+    ]
+
+  ii) Writing to a global variable:
+
+    var x : char
+
+    fn main [
+      call read, 0/stdin, x, 1/size
+      result/EAX <- call write, 1/stdout, x, 1/size
+      call exit, result/EAX
+    ]
+
+One thing to note: variables refer to addresses (not to be confused with the `address` type) just like in Assembly. We'll uniformly use '*' to indicate getting at the value in an address. This will also provide a consistent hint of the addressing mode.
+
+=== Compilation strategy
+
+--- User-defined statements
+
+User-defined functions will be called with the same syntax as primitives. They'll translate to a sequence of push instructions (one per operand, both in and in-out), a call instruction, and a sequence of pop instructions, either to a black hole (in operands) or a location (in-out operands). This follows the standard Unix calling convention:
+
+  push EBP
+  copy ESP to EBP
+  push arg 1
+  push arg 2
+  ...
+  call
+  pop arg n
+  ...
+  pop arg 1
+  copy EBP to ESP
+  pop ESP
+
+Implication: each function argument needs to be something push/pop can accept. It can't be an address, so arrays and structs will either have to be passed by value, necessitating copies, or allocated on the heap. We may end up allocating members of structs in separate heap allocations just so we can pass them piecemeal to helper functions. (Mu has explored this trade-off in the past.)
+
+--- Primitive statements
+
+Operands may be:
+  in code (literals)
+  in registers
+  on the stack
+  on the global segment
+
+Operands are always scalar. Variables on the stack or global segment are immutable references.
+
+  - Variables on the stack are stored at addresses like *(EBP+n)
+  - Global variables are stored at addresses like *disp32, where disp32 is a statically known constant
+
+  #define local(n)  1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n/disp8
+  #define disp32(N) 0/mod 5/rm32/include-disp32 N/disp32
+
+Since the language will not be orthogonal, compilation proceeds by pattern matching over a statement along with knowledge about the types of its operands, as well as where they're stored (register/stack/global). We now enumerate mappings for various categories of statements, based on the type and location of their operands.
+
+Many statements will end up encoding to the exact same x86 instructions. But the types differ, and they get type-checked differently along the way.
+
+A. x : int <- add y
+
+  Requires y to be scalar (32 bits). Result will always be an int. No pointer arithmetic.
+
+  reg <- add literal    => 81 0/subop 3/mod                                                                                           ...(0)
+  reg <- add reg        => 01 3/mod                                                                                                   ...(1)
+  reg <- add stack      => 03 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n/disp8 reg/r32                                        ...(2)
+  reg <- add global     => 03 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                       ...(3)
+  stack <- add literal  => 81 0/subop 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n/disp8 literal/imm32                          ...(4)
+  stack <- add reg      => 01 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n/disp8 reg/r32                                        ...(5)
+  stack <- add stack    => disallowed
+  stack <- add global   => disallowed
+  global <- add literal => 81 0/subop 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32                                         ...(6)
+  global <- add reg     => 01 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                       ...(7)
+  global <- add stack   => disallowed
+  global <- add global  => disallowed
+
+Similarly for sub, and, or, xor and even copy. Replace the opcodes above with corresponding ones from this table:
+
+                            add             sub           and           or            xor         copy/mov
+  reg <- op literal         81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
+  reg <- op reg             01 or 03        29 or 2b      21 or 23      09 or 0b      31 or 33    89 or 8b
+  reg <- op stack           03              2b            23            0b            33          8b
+  reg <- op global          03              2b            23            0b            33          8b
+  stack <- op literal       81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
+  stack <- op reg           01              29            21            09            31          89
+  global <- op literal      81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
+  global <- op reg          01              29            21            09            31          89
+
+B. x/reg : int <- mul y
+
+  Requires y to be scalar.
+  x must be in a register. Multiplies can't write to memory.
+
+  reg <- mul literal    => 69                                                                                                         ...(8)
+  reg <- mul reg        => 0f af 3/mod                                                                                                ...(9)
+  reg <- mul stack      => 0f af 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n/disp8 reg/r32                                     ...(10)
+  reg <- mul global     => 0f af 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                    ...(11)
+
+C. x/EAX/quotient : int, y/EDX/remainder : int <- idiv z     # divide EAX by z; store results in EAX and EDX
+
+  Requires source x and z to both be scalar.
+  x must be in EAX and y must be in EDX. Divides can't write anywhere else.
+
+  First clear EDX (we don't support ints larger than 32 bits):
+  31/xor 3/mod 2/rm32/EDX 2/r32/EDX
+
+  then:
+  EAX, EDX <- idiv literal  => disallowed
+  EAX, EDX <- idiv reg      => f7 7/subop 3/mod                                                                                       ...(12)
+  EAX, EDX <- idiv stack    => f7 7/subop 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n/disp8                                    ...(13)
+  EAX, EDX <- idiv global   => f7 7/subop 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                           ...(14)
+
+D. x : int <- not (weird syntax, but we'll ignore that)
+
+  Requires x to be an int.
+
+  reg <- not                => f7 3/mod                                                                                               ...(15)
+  stack <- not              => f7 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n/disp8                                            ...(16)
+  global <- not             => f7 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                   ...(17)
+
+E. x : (address t) <- get o : T, %f
+
+  (Assumes T.f has type t.)
+
+  o can't be on a register since it's a non-primitive (likely larger than a word)
+  f is a literal
+  x must be in a register (by definition for an address)
+
+  reg1 <- get reg2, literal       => 8d/lea 1/mod reg2/rm32 literal/disp8 reg1/r32                                                    ...(18)
+  reg <- get stack, literal       => 8d/lea 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n+literal/disp8 reg/r32                  ...(19)
+    (simplifying assumption: stack frames can't be larger than 256 bytes)
+  reg <- get global, literal      => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32                                ...(20)
+
+F. x : (offset T) <- index i : int, %size(T)
+
+  This statement is used to translate an array index (denominated in the type of array elements) into an offset (denominated in bytes). It's just a multiply but with a new type for the result so that we can keep the type system sound.
+
+  Since index statements translate to multiplies, 'x' must be a register.
+  The %size(T) argument is statically known, so will always be a literal.
+
+  reg1 <- index reg2, literal       => 69/mul 3/mod reg2/rm32 literal/imm32 -> reg1/r32
+                                    or 68/mul 3/mod reg2/rm32 literal/imm8 -> reg1/r32                                                ...(21)
+  reg1 <- index stack, literal      => 69/mul 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n/disp8 literal/imm32 -> reg1/r32      ...(22)
+  reg1 <- index global, literal     => 69/mul 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32 -> reg1/r32                     ...(23)
+
+G. x : (address T) <- advance a : (array T), idx : (offset T)
+
+  reg <- advance a/reg, idx/reg   => 8d/lea 0/mod 4/rm32/SIB a/base idx/index 0/scale reg/r32                                         ...(24)
+  reg <- advance stack, literal   => 8d/lea 1/mod 4/rm32/SIB 5/base/EBP 4/index/none 0/scale n+literal/disp8 reg/r32                  ...(25)
+  reg <- advance stack, reg2      => 8d/lea 1/mod 4/rm32/SIB 5/base/EBP reg2/index 0/scale n/disp8 reg/r32                            ...(26)
+  reg <- advance global, literal  => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32                                ...(27)
+
+=== Example
+
+Putting it all together: code generation for `a[i].y = 4` where a is an array of 2-d points with x, y coordinates.
+
+If a is allocated on the stack, say of type (array point 6):
+
+  offset/EAX : (offset point) <- index i, 8  # (22)
+  tmp/EBX : (address point) <- advance a : (array point 6), offset/EAX  # (26)
+  tmp2/ECX : (address number) <- get tmp/EBX : (address point), 4/y  # (18)
+  *tmp2/ECX <- copy 4  # (5 for copy/mov with 0 disp8)
+
+=== More complex statements
+
+A couple of statement types expand to multiple instructions:
+  Function calls. We've already seen these above.
+  Bounds checking against array length in 'advance'
+  Dereferencing 'ref' types (see type list up top). Requires an alloc id check.
+
+G'. Bounds checking the 'advance' statement begins with a few extra instructions. For example:
+
+  x/EAX : (address T) <- advance a : (array T), literal
+
+Suppose array 'a' lies on the stack starting at EBP+4. Its length will be at EBP+4, and the actual contents of the array will start from EBP+8.
+
+ compare *(EBP+4), literal
+ jump-if-greater panic          # rudimentary error handling
+
+Now we're ready to perform the actual 'lea':
+
+  lea EBP+8 + literal, reg      # line 25 above
+
+H. Dereferencing a 'ref' needs to be its own statement, yielding an address. This statement has two valid forms:
+
+  reg : (address T) <- deref stack : (ref T)
+  reg : (address T) <- deref global : (ref T)
+
+Since refs need 8 bytes they can't be in a register. And of course the output is an address so it must be in a register.
+
+Compiling 'deref' will take a few instructions. Consider the following example where 's' is on the stack, say starting at EBP+4:
+
+  EDX : (address T) <- deref s : (ref T)
+
+The alloc id of 's' is at *(EBP+4) and the actual address is at *(EBP+8). The above statement will compile down to the following:
+
+  EDX/s <- copy *(EBP+8)         # the address stored in s
+  EDX/alloc-id <- copy *EDX      # alloc id of payload *s
+  compare EDX, *(EBP+4)          # compare with alloc id of pointer
+  jump-unless-equal panic        # rudimentary error handling
+  # compute *(EBP+8) + 4
+  EDX <- copy *(EBP+8)           # recompute the address in s because we can't save the value anywhere)
+  EDX <- add EDX, 4              # skip alloc id this time
+
+Subtleties:
+  a) if the alloc id of the payload is 0, then the payload is reclaimed
+  b) looking up the payload's alloc id *could* cause a segfault. What to do?
+
+=== More speculative ideas
+
+Initialize data segment with special extensible syntax for literals. All literals except numbers and strings start with %. Global variable declarations would now look like:
+
+  var s : (array character) = "abc"  # exception to the '%' convention
+  var p : point = %point(3, 4)
+
+=== Credits
+
+Forth
+C
+Rust
+Lisp
+qhasm
diff --git a/archive/3.transect/compiler2 b/archive/3.transect/compiler2
new file mode 100644
index 00000000..5c06cc4f
--- /dev/null
+++ b/archive/3.transect/compiler2
@@ -0,0 +1,27 @@
+to dereference a heap allocation
+  copy handle to stack
+  perform lookup to stack
+
+lookup x in *(ESP+4) of type (handle T)
+
+  reg <- copy *(ESP+5) : (address T stack)
+  payload alloc id <- copy *reg
+  address alloc id <- copy *(ESP+4)
+  compare payload alloc id, address alloc id
+  jump if not equal to print stack trace and panic
+  address <- add reg, 1
+
+types:
+
+  address T reg
+  address T stack
+  address T heap
+  address T global
+
+copy down this spectrum is not permitted, but up is.
+
+addresses aren't allowed in types, globals and on the heap. Only handles.
+addresses are only for temporary manipulations.
+
+
+*(address T) <- copy T
diff --git a/archive/3.transect/compiler3 b/archive/3.transect/compiler3
new file mode 100644
index 00000000..6bc6bf85
--- /dev/null
+++ b/archive/3.transect/compiler3
@@ -0,0 +1,73 @@
+== Goal
+
+A memory-safe language with a simple translator to x86 that can be feasibly written in x86.
+
+== Definitions of terms
+
+Memory-safe: it should be impossible to:
+  a) create a pointer out of arbitrary data, or
+  b) to access heap memory after it's been freed.
+
+Simple: do all the work in a 2-pass translator:
+  Pass 1: check each instruction's types in isolation.
+  Pass 2: emit code for each instruction in isolation.
+
+== Implications
+
+=> Each instruction matches a pattern and yields a template to emit.
+=> There's a 1-to-1 mapping between instructions in the source language and x86 machine code.
+  Zero runtime.
+=> Programmers have to decide how to use registers.
+=> Translator can't insert any instructions that write to registers. (We don't know if a register is in use.)
+
+== Lessons from Mu
+
+1. For easy bounds checking, never advance pointers to arrays or heap allocations. No pointer arithmetic.
+2. Store the array length with the array.
+3. Store an allocation id with heap allocations. Allocation id goes monotonically up, never gets reused. When it wraps around to zero the program panics.
+4. Heap pointers also carry around allocation id.
+5. When dereferencing a heap pointer, first ensure its alloc id matches the alloc id of the payload. This ensures some other copy of the pointer didn't get freed (and potentially reused)
+
+== Problem 1
+
+How to index into an array?
+
+  The array has a length that needs to be checked.
+  Its elements have a type T.
+  The base will be in memory, either on the stack or the heap.
+  The index may be in the register, stack or heap.
+
+That's too much work to do in a single instruction.
+
+So arrays have to take multiple steps. And we have to guard against the steps
+being misused in unsafe ways.
+
+To index into an array with elements of type T, starting with the size of the
+array in bytes:
+
+  step 1: get the offset the index is at
+    <reg offset> : (index T) <- index <reg/mem idx> : int, <literal> : (size T)
+  step 2: convert the array to address-of-element
+    <reg x> : (address T) <- advance <reg/mem A> : (array T), <reg offset> : (index T)
+    implicitly compares the offset with the size, panic if greater
+    =>
+      compare <reg offset> : (index T), <reg/mem> : (array T)
+      jge panic
+  step 3: use the address to the element
+    ...
+
+(index T) is a special type. You can do only two things with it:
+  - pass it to the advance instruction
+  - convert it to a number (but no converting back)
+
+(address T) is a short-term pointer. You can't store addresses in structs, you
+can't define global variables of that type, and you can't pass the type to the
+memory allocator to save to the heap. Only place you can store an (address T)
+is on the stack or a register.
+
+[But you can still be holding an address in a long-lived stack frame after
+it's been freed?!]
+
+== Problem 2
+
+How to dereference a heap allocation?
diff --git a/archive/3.transect/compiler4 b/archive/3.transect/compiler4
new file mode 100644
index 00000000..8dfb8ccd
--- /dev/null
+++ b/archive/3.transect/compiler4
@@ -0,0 +1,84 @@
+== Goal
+
+A memory-safe language with a simple translator to x86 that can be feasibly written in x86.
+
+== Definitions of terms
+
+Memory-safe: it should be impossible to:
+  a) create a pointer out of arbitrary data, or
+  b) to access heap memory after it's been freed.
+
+Simple: do all the work in a 2-pass translator:
+  Pass 1: check each instruction's types in isolation.
+  Pass 2: emit code for each instruction in isolation.
+
+== Implications
+
+=> Each instruction matches a pattern and yields a template to emit.
+=> There's a 1-to-1 mapping between instructions in the source language and x86 machine code.
+  Zero runtime.
+=> Programmers have to decide how to use registers.
+=> Translator can't insert any instructions that write to registers. (We don't know if a register is in use.)
+
+== Lessons from Mu
+
+1. For easy bounds checking, never advance pointers to arrays or heap allocations. No pointer arithmetic.
+2. Store the array length with the array.
+3. Store an allocation id with heap allocations. Allocation id goes monotonically up, never gets reused. When it wraps around to zero the program panics.
+4. Heap pointers also carry around allocation id.
+5. When dereferencing a heap pointer, first ensure its alloc id matches the alloc id of the payload. This ensures some other copy of the pointer didn't get freed (and potentially reused)
+
+== Problem 1
+
+How to index into an array?
+
+  The array has a length that needs to be checked.
+  Its elements have a type T.
+  The base will be in memory, either on the stack or the heap.
+  The index may be in the register, stack or heap.
+
+That's too much work to do in a single instruction.
+
+So arrays have to take multiple steps. And we have to guard against the steps
+being misused in unsafe ways.
+
+To index into an array with elements of type T, starting with the size of the
+array in bytes:
+
+  step 1: get the offset the index is at
+    <reg offset> : (index T) <- index <reg/mem idx> : int, <literal> : (size T)
+  step 2: convert the array to address-of-element
+    <reg x> : (address T) <- advance <reg/mem A> : (array T), <reg offset> : (index T)
+    implicitly compares the offset with the size, panic if greater
+    =>
+      compare <reg offset> : (index T), <reg/mem> : (array T)
+      jge panic
+  step 3: use the address to the element
+    ...
+
+(index T) is a special type. You can do only two things with it:
+  - pass it to the advance instruction
+  - convert it to a number (but no converting back)
+
+(address T) is a short-term pointer. You can't store addresses in structs, you
+can't define global variables of that type, and you can't pass the type to the
+memory allocator to save to the heap. You also can't store addresses in the
+stack, because you may encounter a free before you end the function.
+
+Maybe we'll also forbid any sort of copy of address types. Only place you can
+store an (address T) is the register you saved to. To copy you need a handle
+to a heap allocation.
+
+Still not entirely protected against temporal issues. But pretty close.
+
+== Problem 2
+
+How to dereference a heap allocation?
+
+== List of types
+
+int 
+char
+(address _)   X  
+(array _)
+(handle _)
diff --git a/archive/3.transect/compiler5 b/archive/3.transect/compiler5
new file mode 100644
index 00000000..aeb857f4
--- /dev/null
+++ b/archive/3.transect/compiler5
@@ -0,0 +1,32 @@
+== Goal
+
+A memory-safe language with a simple translator to x86 that can be feasibly written in x86.
+
+== Definitions of terms
+
+Memory-safe: it should be impossible to:
+  a) create a pointer out of arbitrary data, or
+  b) to access heap memory after it's been freed.
+
+Simple: do all the work in a 2-pass translator:
+  Pass 1: check each instruction's types in isolation.
+  Pass 2: emit code for each instruction in isolation.
+
+== types
+
+int
+char
+(address _ t), t ∋ {stack, heap, global}
+(array _ t), t ∋ {stack, heap, global}
+
+stack addresses can't be copied to heap or global
+heap addresses can't be copied [1]
+global addresses you're free to use anywhere
+
+[1] (address _ heap) can't be copied or stored, can't be part of a type or
+choice. Only thing you can do with it is access it from the register you wrote
+it to. And even that not past a call instruction. Important detail: `free()`
+is a call. So an address to something on the heap can never be invalid if the
+program type-checks.
+
+<reg x> : (address T m) <- advance <reg/mem> : (array T m), <reg offset> : (index T)
diff --git a/archive/3.transect/compiler6 b/archive/3.transect/compiler6
new file mode 100644
index 00000000..48a7030f
--- /dev/null
+++ b/archive/3.transect/compiler6
@@ -0,0 +1,36 @@
+== Goal
+
+A memory-safe language with a simple translator to x86 that can be feasibly written in x86.
+
+== Definitions of terms
+
+Memory-safe: it should be impossible to:
+  a) create a pointer out of arbitrary data, or
+  b) to access heap memory after it's been freed.
+
+Simple: do all the work in a 2-pass translator:
+  Pass 1: check each instruction's types in isolation.
+  Pass 2: emit code for each instruction in isolation.
+
+== types
+
+int
+char
+(address _)
+(array _ n)
+(ref _)
+
+addresses can't be saved to stack or global,
+      or included in compound types
+      or used across a call (to eliminate possibility of free)
+
+<reg x> : (address T) <- advance <reg/mem> : (array T), <reg offset> : (index T)
+
+arrays require a size
+(ref array _) may not include a size
+
+== open questions
+Is argv an address?
+Global variables are easiest to map to addresses.
+Ideally we'd represent 'indirect' as a '*' and we could just count to make
+sure that an instruction never has more than one '*'.
diff --git a/archive/3.transect/compiler7 b/archive/3.transect/compiler7
new file mode 100644
index 00000000..cf7d454f
--- /dev/null
+++ b/archive/3.transect/compiler7
@@ -0,0 +1,46 @@
+== Goal
+
+A memory-safe language with a simple translator to x86 that can be feasibly written in x86.
+
+== Definitions of terms
+
+Memory-safe: it should be impossible to:
+  a) create a pointer out of arbitrary data, or
+  b) to access heap memory after it's been freed.
+
+Simple: do all the work in a 2-pass translator:
+  Pass 1: check each instruction's types in isolation.
+  Pass 2: emit code for each instruction in isolation.
+
+== types
+
+int
+char
+(address _ stack|heap|global)
+(array _ n)  # on stack or global
+(ref _)
+(ref array _)  # by definition always on the heap
+
+addresses to global can be saved and manipulated as usual
+addresses on stack can't be saved to heap
+addresses on heap can't be saved to global (use ref)
+
+addresses to stack or heap can't be included in compound types
+  or used across a call
+  or used across a label
+
+<reg x> : (address T stack|global) <- advance <reg/mem> : (array T), <reg offset> : (index T)
+<reg x> : (address T heap) <- advance *<mem> : (ref array T), <reg offset> : (index T)
+
+arrays require a size
+(ref array _) may not include a size
+
+Arguments of type 'address' are required to be on the stack or global. Can't
+be on the heap.
+
+So we need duplication for address and ref arguments?
+
+Argv has type (array (address (array char) global))
+
+variables on stack, heap and global are references. The name points at the
+address. Use '*' to get at the value.
diff --git a/archive/3.transect/compiler8 b/archive/3.transect/compiler8
new file mode 100644
index 00000000..b3b35271
--- /dev/null
+++ b/archive/3.transect/compiler8
@@ -0,0 +1,53 @@
+== Goal
+
+A memory-safe language with a simple translator to x86 that can be feasibly written in x86.
+
+== Definitions of terms
+
+Memory-safe: it should be impossible to:
+  a) create a pointer out of arbitrary data, or
+  b) to access heap memory after it's been freed.
+
+Simple: do all the work in a 2-pass translator:
+  Pass 1: check each instruction's types in isolation.
+  Pass 2: emit code for each instruction in isolation.
+
+== types
+
+int
+char
+(address _)
+(array _ n)
+(ref _)
+
+== implications
+
+addresses can't be saved to stack or global,
+      or included in compound types
+      or used across a call (to eliminate possibility of free)
+
+<reg x> : (address T) <- advance <reg/mem> : (array T), <reg offset> : (index T)
+
+arrays require a size
+(ref array _) may not include a size
+
+argv has type (array (ref array char))
+
+variables on stack, heap and global are references. The name points at the
+address. Use '*' to get at the value.
+
+instructions performing lookups write to register, so that we can reuse the register for temporaries
+instructions performing lookups can't read from the register they write to.
+  But most instructions read from the register they write to?! (in-out params)
+
+== open questions
+
+If bounds checks can take multiple instructions, why not perform array
+indexing in a single statement in the language?
+
+But we want addresses as intermediate points to combine instructions with.
+
+Maybe disallow addresses to function calls, but allow addresses to non-heap
+structures to be used spanning function calls and labels.
+
+That's just for ergonomics. Doesn't add new capability.
diff --git a/archive/3.transect/compiler9 b/archive/3.transect/compiler9
new file mode 100644
index 00000000..26becf48
--- /dev/null
+++ b/archive/3.transect/compiler9
@@ -0,0 +1,254 @@
+=== Goal
+
+A memory-safe language with a simple translator to x86 that can be feasibly
+written without itself needing a translator.
+
+Memory-safe: it should be impossible to:
+  a) create a pointer out of arbitrary data, or
+  b) to access heap memory after it's been freed.
+
+Simple: do all the work in a 2-pass translator:
+  Pass 1: check each instruction's types in isolation.
+  Pass 2: emit code for each instruction in isolation.
+
+=== Overview of the language
+
+A program consists of a series of type, function and global variable declarations.
+(Also constants and tests, but let's focus on these.)
+
+Type declarations basically follow Hindley-Milner with product and (tagged) sum
+types. Types are written in s-expression form. There's a `ref` type that's a
+type-safe fat pointer, with an alloc id that gets incremented after each
+allocation. Memory allocation and reclamation is manual. Dereferencing a ref
+after its underlying memory is reclaimed (pointer alloc id no longer matches
+payload alloc id) is guaranteed to immediately kill the program (like a
+segfault).
+
+  # product type
+  type foo [
+    x : int
+    y : (ref int)
+    z : bar
+  ]
+
+  # sum type
+  choice bar [
+    x : int
+    y : point
+  ]
+
+Functions have a header and a series of instructions in the body:
+
+  fn f a : int -> b : int [
+    ...
+  ]
+
+Instructions have the following format:
+
+  io1, io2, ... <- operation i1, i2, ...
+
+i1, i2 operands on the right hand side are immutable. io1, io2 are in-out
+operands. They're written to, and may also be read.
+
+User-defined functions will be called with the same syntax. They'll translate
+to a sequence of push instructions (one per operand, both in and in-out), a
+call instruction, and a sequence of pop instructions, either to a black hole
+(in operands) or a location (in-out operands). This follows the standard Unix
+calling convention. Each operand needs to be something push/pop can accept.
+
+Primitive operations depend on the underlying processor. We'd like each primitive
+operation supported by the language to map to a single instruction in the ISA.
+Sometimes we have to violate that (see below), but we definitely won't be
+writing to any temporary locations behind the scenes. The language affords
+control over registers, and tracking unused registers gets complex, and
+besides we may have no unused registers at a specific point. Instructions only
+modify their operands.
+
+In most ISAs, instructions operate on at most a word of data at a time. They
+also tend to not have more than 2-3 operands, and not modify more than 2
+locations in memory.
+
+Since the number of reads from memory is limited, we break up complex high-level
+operations using a special type called `address`. Addresses are strictly
+short-term entities. They can't be stored in a compound type, and they can't
+be passed into or returned from a user-defined function. They also can't be
+used after a function call (because it could free the underlying memory) or
+label (because it gets complex to check control flow, and we want to translate
+each instruction simply and in isolation).
+
+=== Compilation to 32-bit x86
+
+Values can be stored:
+  in code (literals)
+  in registers
+  on the stack
+  on the global segment
+
+Variables on the stack are stored at *(ESP+n)
+Global variables are stored at *disp32, where disp32 is statically known
+
+Address variables have to be in a register.
+  - You need them in a register to do a lookup, and
+  - Saving them to even the stack increases the complexity of checks needed on
+    function calls or labels.
+
+Compilation proceeds by pattern matching over an instruction along with
+knowledge about the types of its operands, as well as where they're stored
+(register/stack/global). We now enumerate mappings for various categories of
+instructions, based on the type and location of their operands.
+
+Where types of operands aren't mentioned below, all operands of an instruction
+should have the same (word-length) type.
+
+Lots of special cases because of limitations of the x86 ISA. Beware.
+
+A. x : int <- add y
+
+  Requires y to be scalar. Result will always be an int. No pointer arithmetic.
+
+  reg <- add literal    => 81 0/subop 3/mod                                                                                           ...(0)
+  reg <- add reg        => 01 3/mod                                                                                                   ...(1)
+  reg <- add stack      => 03 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                        ...(2)
+  reg <- add global     => 03 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                       ...(3)
+  stack <- add literal  => 81 0/subop 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 literal/imm32                          ...(4)
+  stack <- add reg      => 01 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                        ...(5)
+  stack <- add stack    => disallowed
+  stack <- add global   => disallowed
+  global <- add literal => 81 0/subop 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32                                         ...(6)
+  global <- add reg     => 01 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                       ...(7)
+  global <- add stack   => disallowed
+  global <- add global  => disallowed
+
+Similarly for sub, and, or, xor and even copy. Replace the opcodes above with corresponding ones from this table:
+
+                            add             sub           and           or            xor         copy/mov
+  reg <- op literal         81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
+  reg <- op reg             01 or 03        29 or 2b      21 or 23      09 or 0b      31 or 33    89 or 8b
+  reg <- op stack           03              2b            23            0b            33          8b
+  reg <- op global          03              2b            23            0b            33          8b
+  stack <- op literal       81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
+  stack <- op reg           01              29            21            09            31          89
+  global <- op literal      81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
+  global <- op reg          01              29            21            09            31          89
+
+B. x/reg : int <- mul y
+
+  Requires both y to be scalar.
+  x must be in a register. Multiplies can't write to memory.
+
+  reg <- mul literal    => 69                                                                                                         ...(8)
+  reg <- mul reg        => 0f af 3/mod                                                                                                ...(9)
+  reg <- mul stack      => 0f af 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                     ...(10)
+  reg <- mul global     => 0f af 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                    ...(11)
+
+C. x/EAX/quotient : int, y/EDX/remainder : int <- idiv z     # divide EAX by z; store the result in EAX and EDX
+
+  Requires source x and z to both be scalar.
+  x must be in EAX and y must be in EDX. Divides can't write anywhere else.
+
+  First clear EDX (we don't support ints larger than 32 bits):
+  31/xor 3/mod 2/rm32/EDX 2/r32/EDX
+
+  then:
+  EAX, EDX <- idiv literal  => disallowed
+  EAX, EDX <- idiv reg      => f7 7/subop 3/mod                                                                                       ...(12)
+  EAX, EDX <- idiv stack    => f7 7/subop 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8                                    ...(13)
+  EAX, EDX <- idiv global   => f7 7/subop 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                           ...(14)
+
+D. x : int <- not
+
+  Requires x to be an int.
+
+  reg <- not                => f7 3/mod                                                                                               ...(15)
+  stack <- not              => f7 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8                                            ...(16)
+  global <- not             => f7 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                   ...(17)
+
+E. x : (address t) <- get o : T, %f
+
+  (Assumes T.f has type t.)
+
+  o can't be on a register since it's a non-primitive (likely larger than a word)
+  f is a literal
+  x must be in a register (by definition for an address)
+
+  below '*' works on either address or ref types
+
+  For raw stack values we want to read *(ESP+n)
+  For raw global values we want to read *disp32
+  For address stack values we want to read *(ESP+n)+
+    *(ESP+n) contains an address
+    so we want to compute *(ESP+n) + literal
+
+  reg1 <- get reg2, literal       => 8d/lea 1/mod reg2/rm32 literal/disp8 reg1/r32                                                    ...(18)
+  reg <- get stack, literal       => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n+literal/disp8 reg/r32                  ...(19)
+    (simplifying assumption: stack frames can't be larger than 256 bytes)
+  reg <- get global, literal      => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32                                ...(20)
+
+F. x : (offset T) <- index i : int, %size(T)
+
+  reg1 <- index reg2, literal       => 69/mul 3/mod reg2/rm32 literal/imm32 -> reg1/r32
+                                    or 68/mul 3/mod reg2/rm32 literal/imm8 -> reg1/r32                                                ...(21)
+  reg1 <- index stack, literal      => 69/mul 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 literal/imm32 -> reg1/r32      ...(22)
+  reg1 <- index global, literal     => 69/mul 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32 -> reg1/r32                     ...(23)
+
+  optimization: avoid multiply if literal is a power of 2
+    use SIB byte if literal is 2, 4 or 8
+    or left shift
+
+G. x : (address T) <- advance o : (array T), idx : (offset T)
+
+  reg <- advance a/reg, idx/reg   => 8d/lea 0/mod 4/rm32/SIB a/base idx/index 0/scale reg/r32                                         ...(24)
+  reg <- advance stack, literal   => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n+literal/disp8 reg/r32                  ...(25)
+  reg <- advance stack, reg2      => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP reg2/index 0/scale n/disp8 reg/r32                            ...(26)
+  reg <- advance global, literal  => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32                                ...(27)
+
+  also instructions for runtime bounds checking
+
+=== Example
+
+Putting it all together: code generation for `a[i].y = 4` where a is an array
+of 2-d points with x, y coordinates.
+
+If a is allocated on the stack, say of type (array point 6) at (ESP+4):
+
+  offset/EAX : (offset point) <- index i, 8  # (22)
+  tmp/EBX : (address point) <- advance a : (array point 6), offset/EAX  # (26)
+  tmp2/ECX : (address number) <- get tmp/EBX : (address point), 4/y  # (18)
+  *tmp2/ECX <- copy 4  # (5 for copy/mov with 0 disp8)
+
+Many instructions, particularly variants of 'get' and 'advance' -- end up encoding the exact same instructions.
+But the types differ, and the type-checker checks them differently.
+
+=== Advanced checks
+
+Couple of items require inserting mapping to multiple instructions:
+  bounds checking against array length in 'advance'
+  dereferencing 'ref' types (see type list up top)
+
+A. Dereferencing a ref
+
+    tmp/EDX <- advance *s, tmp0/EDI
+      => compare (ESP+4), *(ESP+8)  ; '*' from compiler2
+         jump-unless-equal panic
+         EDX <- add ESP, 8
+         EDX <- copy *EDX
+         EDX <- add EDX, 4
+         EDX <- 8d/lea EDX + result
+
+=== More speculative ideas
+
+Initialize data segment with special extensible syntax for literals. All
+literals except numbers and strings start with %.
+
+  %size(type) => compiler replaces with size of type
+  %point(3, 4) => two words
+
+and so on.
+
+=== Credits
+
+Forth
+C
+Rust
+Lisp
+qhasm
diff --git a/archive/3.transect/ex3.k2 b/archive/3.transect/ex3.k2
new file mode 100644
index 00000000..63151396
--- /dev/null
+++ b/archive/3.transect/ex3.k2
@@ -0,0 +1,22 @@
+# add the first 10 numbers, and return the result in the exit code
+
+fn main [
+  var result/EBX : int
+  result/EBX <- copy 0
+  var counter/ECX : int
+  counter/ECX <- copy 1
+  {
+    compare counter/ECX, 10
+    break-if >
+    result/EBX <- add counter/ECX
+    counter/ECX <- add 1
+    loop
+  }
+  call exit, 1
+]
+
+fn exit x : int [
+  code/EBX <- copy x
+  code/EAX <- copy 1/exit
+  syscall
+]
diff --git a/archive/3.transect/ex4.k2 b/archive/3.transect/ex4.k2
new file mode 100644
index 00000000..9a7ddee7
--- /dev/null
+++ b/archive/3.transect/ex4.k2
@@ -0,0 +1,34 @@
+## read a character from stdin, save it to a global, write it to stdout
+
+# variables are always references
+#   read their address with their names: x (can't write to their address)
+#   read/write their contents with a lookup: *x
+var x : char
+
+fn main [
+  call read 0/stdin, x, 1/size
+  result/EAX <- call write 1/stdout, x, 1/size
+  call exit, result/EAX
+]
+
+fn read fd : int, x : (address array byte), size : int [
+  EBX <- copy fd
+  ECX <- copy x
+  EDX <- copy size
+  EAX <- copy 3/read
+  syscall
+]
+
+fn write fd : int, x : (address array byte), size : int [
+  EBX <- copy fd
+  ECX <- copy x
+  EDX <- copy size
+  EAX <- copy 4/write
+  syscall
+]
+
+fn exit x : int [
+  code/EBX <- copy x
+  code/EAX <- copy 1/exit
+  syscall
+]
diff --git a/archive/3.transect/ex5.k2 b/archive/3.transect/ex5.k2
new file mode 100644
index 00000000..0d7148fe
--- /dev/null
+++ b/archive/3.transect/ex5.k2
@@ -0,0 +1,30 @@
+# read a character from stdin, save it to a local on the stack, write it to stdout
+
+fn main [
+  var x : char
+  call read 0/stdin, x, 1/size
+  result/EBX <- call write 1/stdout, x, 1/size
+  call exit-EBX
+]
+
+fn read fd : int, x : (address array byte), size : int [
+  EBX <- copy fd
+  ECX <- copy x
+  EDX <- copy size
+  EAX <- copy 3/read
+  syscall
+]
+
+fn write fd : int, x : (address array byte), size : int [
+  EBX <- copy fd
+  ECX <- copy x
+  EDX <- copy size
+  EAX <- copy 4/write
+  syscall
+]
+
+# like exit, but assumes the code is already in EBX
+fn exit-EBX [
+  code/EAX <- copy 1/exit
+  syscall
+]
diff --git a/archive/3.transect/ex6.k2 b/archive/3.transect/ex6.k2
new file mode 100644
index 00000000..cd68f861
--- /dev/null
+++ b/archive/3.transect/ex6.k2
@@ -0,0 +1,23 @@
+## print out a (global variable) string to stdout
+
+var size : int = 14
+var x : (array character) = "hello, world!"
+
+fn main [
+  call write 1/stdout, x, size
+  call exit, 0
+]
+
+fn write fd : int, x : (address array byte), size : int [
+  EBX <- copy fd
+  ECX <- copy x
+  EDX <- copy size
+  EAX <- copy 4/write
+  syscall
+]
+
+fn exit x : int [
+  code/EBX <- copy x
+  code/EAX <- copy 1/exit
+  syscall
+]
diff --git a/archive/3.transect/ex7.k2 b/archive/3.transect/ex7.k2
new file mode 100644
index 00000000..3ede2c5e
--- /dev/null
+++ b/archive/3.transect/ex7.k2
@@ -0,0 +1,64 @@
+# example showing file syscalls
+#
+# Create a file, open it for writing, write a character to it, close it, open
+# it for reading, read a character from it, close it, delete it, and return
+# the character read.
+
+var stream : int = 0
+var a : char = 97
+var b : char = 0
+var filename : (array char) = ".foo"
+
+fn main [
+  call create, filename
+  *stream <- call open, filename, 1/wronly
+  call write, *stream, a, 1
+  call close, *stream
+  stream <- call open, filename, 0/rdonly
+  call read, *stream, b, 1
+  call close, *stream
+  call unlink, filename
+  var result/EBX : char
+  result/EBX <- copy b  # TODO: copy char to int?
+  call exit-EBX
+]
+
+fn read fd : int, x : (address array byte), size : int [
+  EBX <- copy fd
+  ECX <- copy x
+  EDX <- copy size
+  EAX <- copy 3/read
+  syscall
+]
+
+fn write fd : int, x : (address array byte), size : int [
+  EBX <- copy fd
+  ECX <- copy x
+  EDX <- copy size
+  EAX <- copy 4/write
+  syscall
+]
+
+fn open name : (address array byte) [
+  EBX <- copy name
+  EAX <- copy 5/open
+  syscall
+]
+
+fn close name : (address array byte) [
+  EBX <- copy name
+  EAX <- copy 6/close
+  syscall
+]
+
+fn unlink name : (address array byte) [
+  EBX <- copy name
+  EAX <- copy 10/unlink
+  syscall
+]
+
+# like exit, but assumes the code is already in EBX
+fn exit-EBX [
+  code/EAX <- copy 1/exit
+  syscall
+]
diff --git a/archive/3.transect/ex8.k2 b/archive/3.transect/ex8.k2
new file mode 100644
index 00000000..dfef03b0
--- /dev/null
+++ b/archive/3.transect/ex8.k2
@@ -0,0 +1,36 @@
+# Example reading commandline arguments: compute length of first arg.
+
+fn main argc : int, argv : (array (ref array char)) -> [
+  var tmp : (index char)
+  tmp <- index 1, %size(ref array char)
+  var tmp2 : (address (ref array char))
+  tmp2 <- advance argv, tmp
+  var s/EBX : (ref array char)
+  s/EBX <- copy *tmp2
+  var result/EAX : int
+  result/EAX <- ascii_length s/EBX
+  call exit, result/EAX
+]
+
+fn ascii_length s : (ref array char) -> result : int [
+  var result/EBX : int
+  result/EBX <- copy 0
+  {
+    var tmp0/EDI : (offset char)
+    tmp0/EDI <- index result/EBX, %size(char)
+    var tmp/EDX : (address char)
+    tmp/EDX <- advance *s, tmp0/EDI
+    var c/ECX : char
+    c/ECX <- copy *tmp
+    compare c/ECX, 0
+    break-if-equal
+    loop
+  }
+  return result/EBX
+]
+
+fn exit x : int [
+  code/EBX <- copy x
+  code/EAX <- copy 1/exit
+  syscall
+]
diff --git a/archive/3.transect/factorial.k2 b/archive/3.transect/factorial.k2
new file mode 100644
index 00000000..79269773
--- /dev/null
+++ b/archive/3.transect/factorial.k2
@@ -0,0 +1,16 @@
+# compute the factorial of 5, and return the result in the exit code
+
+fn factorial n : int -> result/EAX : int [
+  result/EAX <- copy 1
+  {
+    compare n, 1
+    break-if <=
+    var tmp/EBX : int
+    tmp/EBX <- copy n
+    tmp/EBX <- subtract 1
+    var tmp2/EAX : int
+    tmp2/EAX <- call factorial, tmp/EBX
+    result/EAX <- multiply tmp2/EAX, n
+  }
+  return result/EAX
+]
diff --git a/archive/3.transect/vimrc.vim b/archive/3.transect/vimrc.vim
new file mode 100644
index 00000000..d8b70fbc
--- /dev/null
+++ b/archive/3.transect/vimrc.vim
@@ -0,0 +1,36 @@
+" Highlighting literate directives in C++ sources.
+function! HighlightTangledFile()
+  " Tangled comments only make sense in the sources and are stripped out of
+  " the generated .cc file. They're highlighted same as regular comments.
+  syntax match tangledComment /\/\/:.*/ | highlight link tangledComment Comment
+  syntax match tangledSalientComment /\/\/::.*/ | highlight link tangledSalientComment SalientComment
+  set comments-=://
+  set comments-=n://
+  set comments+=n://:,n://
+
+  " Inside tangle scenarios.
+  syntax region tangleDirective start=+:(+ skip=+".*"+ end=+)+
+  highlight link tangleDirective Delimiter
+  syntax match traceContains /^+.*/
+  highlight traceContains ctermfg=darkgreen
+  syntax match traceAbsent /^-.*/
+  highlight traceAbsent ctermfg=darkred
+  syntax match tangleScenarioSetup /^\s*% .*/ | highlight link tangleScenarioSetup SpecialChar
+
+  " 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:#
+endfunction
+augroup LocalVimrc
+  autocmd BufRead,BufNewFile *.cc call HighlightTangledFile()
+augroup END
+
+" Scenarios considered:
+"   opening or starting vim with a new or existing file without an extension (should interpret as C++)
+"   starting vim or opening a buffer without a file name (ok to do nothing)
+"   opening a second file in a new or existing window (shouldn't mess up existing highlighting)
+"   reloading an existing file (shouldn't mess up existing highlighting)