about summary refs log tree commit diff stats
path: root/baremetal/shell
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-02-20 22:14:06 -0800
committerKartik K. Agaram <vc@akkartik.com>2021-02-20 22:14:09 -0800
commit4a37291e2d68cb4c9a49dfb816882b963a1cfe90 (patch)
treedef5aa0f5a7060b548f9f2cbcd1635cc10b7f283 /baremetal/shell
parentbcb2190ec370b2009fd3ff6faf690758e4cd8e69 (diff)
downloadmu-4a37291e2d68cb4c9a49dfb816882b963a1cfe90.tar.gz
7759 - changing course; delete the postfix shell
It might be too ambitious for an initial Mu system, and I also want to
watch my novelty budget. I also have great doubts about the ability of
this live-updating postfix system to scale to interesting programs. Conditionals,
loops, multi-line functions, all this requires further work.

Instead, I'm going to recenter around Mu's original goals:
  - saying no to most features
  - encouraging/teaching testing
  - traces as a unifying metaphor

In particular, instead of a live-updating system, the new debug loop will
be:
  - generate a trace
  - browse the trace
  - modify the program
  - generate a trace
  - ...

The only persistence we'll need here is a way to track what the programmer
has drilled into in the trace. That might have some commonalities with
the old system of expanded words.
Diffstat (limited to 'baremetal/shell')
-rw-r--r--baremetal/shell/eval.mu1054
-rw-r--r--baremetal/shell/eval.mu.debug1070
-rw-r--r--baremetal/shell/line.mu275
-rw-r--r--baremetal/shell/value-stack.mu241
-rw-r--r--baremetal/shell/word.mu718
5 files changed, 0 insertions, 3358 deletions
diff --git a/baremetal/shell/eval.mu b/baremetal/shell/eval.mu
deleted file mode 100644
index 0dc4aee9..00000000
--- a/baremetal/shell/eval.mu
+++ /dev/null
@@ -1,1054 +0,0 @@
-# evaluator (and parser) for the Mu shell language
-# inputs:
-#   a list of lines, each a list of words, each an editable gap-buffer
-#   end: a word to stop at
-# output:
-#   a stack of values to render that summarizes the result of evaluation until 'end'
-
-# Key features of the language:
-#   words matching '=___' create bindings
-#   line boundaries clear the stack (but not bindings)
-#   { and } for grouping words
-#   break and loop for control flow within groups
-#   -> for conditionally skipping the next word or group
-
-# Example: Pushing numbers from 1 to n on the stack
-#
-#   3 =n
-#   { n 1 <     -> break n n 1- =n loop }
-#
-# Stack as we evaluate each word in the second line:
-#     3 1 false          3 3 2  3   1
-#       3                  3 3      2
-#                                   3
-
-# Rules beyond simple postfix:
-#   If the final word is `->`, clear stack
-#   If the final word is `break`, pop top of stack
-#
-#   `{` and `}` don't affect evaluation
-#   If the final word is `{` or `}`, clear stack (to suppress rendering it)
-#
-#   If `->` in middle and top of stack is falsy, skip next word or group
-#
-#   If `break` in middle executes, skip to next containing `}`
-#     If no containing `}`, clear stack (incomplete)
-#
-#   If `loop` in middle executes, skip to previous containing `{`
-#     If no containing `}`, clear stack (error)
-
-fn evaluate _in: (addr line), end: (addr word), out: (addr value-stack) {
-  clear-value-stack out
-  var line/eax: (addr line) <- copy _in
-  var curr-ah/eax: (addr handle word) <- get line, data
-  var curr/eax: (addr word) <- lookup *curr-ah
-  evaluate-sub curr, end, out, 1/top-level
-}
-
-fn evaluate-sub _curr: (addr word), end: (addr word), out: (addr value-stack), top-level?: boolean {
-  var curr/ecx: (addr word) <- copy _curr
-  var curr-stream-storage: (stream byte 0x10)
-  var curr-stream/edi: (addr stream byte) <- address curr-stream-storage
-  $evaluate-sub:loop: {
-    # safety net (should never hit)
-    compare curr, 0
-    break-if-=
-    # pull next word in for parsing
-    emit-word curr, curr-stream
-#?     {
-#?       clear-screen 0/screen
-#?       dump-stack out
-#?       var foo/eax: int <- render-word 0/screen, curr, 0/x, 0/y, 0/no-cursor
-#?       {
-#?         var key/eax: byte <- read-key 0/keyboard
-#?         compare key, 0
-#?         loop-if-=
-#?       }
-#?     }
-    $evaluate-sub:process-word: {
-      ### if curr-stream is an operator, perform it
-      {
-        var is-add?/eax: boolean <- stream-data-equal? curr-stream, "+"
-        compare is-add?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- add b
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-sub?/eax: boolean <- stream-data-equal? curr-stream, "-"
-        compare is-sub?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- subtract b
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-mul?/eax: boolean <- stream-data-equal? curr-stream, "*"
-        compare is-mul?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- multiply b
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-div?/eax: boolean <- stream-data-equal? curr-stream, "/"
-        compare is-div?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- divide b
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-sqrt?/eax: boolean <- stream-data-equal? curr-stream, "sqrt"
-        compare is-sqrt?, 0/false
-        break-if-=
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- square-root a
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-lesser?/eax: boolean <- stream-data-equal? curr-stream, "<"
-        compare is-lesser?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        compare a, b
-        {
-          break-if-float<
-          push-boolean-to-value-stack out, 0/false
-          break $evaluate-sub:process-word
-        }
-        push-boolean-to-value-stack out, 1/true
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-greater?/eax: boolean <- stream-data-equal? curr-stream, ">"
-        compare is-greater?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        compare a, b
-        {
-          break-if-float>
-          push-boolean-to-value-stack out, 0/false
-          break $evaluate-sub:process-word
-        }
-        push-boolean-to-value-stack out, 1/true
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-equal?/eax: boolean <- stream-data-equal? curr-stream, "=="  # TODO support non-numbers
-        compare is-equal?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        compare a, b
-        {
-          break-if-=
-          push-boolean-to-value-stack out, 0/false
-          break $evaluate-sub:process-word
-        }
-        push-boolean-to-value-stack out, 1/true
-        break $evaluate-sub:process-word
-      }
-      ## control flow
-      {
-        var is-conditional?/eax: boolean <- stream-data-equal? curr-stream, "->"
-        compare is-conditional?, 0/false
-        break-if-=
-        var a/eax: boolean <- pop-boolean-from-value-stack out
-        compare a, 0/false
-        {
-          break-if-!=
-          # if a is false, skip one word
-          var next-word: (handle word)
-          var next-word-ah/eax: (addr handle word) <- address next-word
-          skip-word curr, end, next-word-ah
-          var _curr/eax: (addr word) <- lookup *next-word-ah
-          curr <- copy _curr
-        }
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-group-start?/eax: boolean <- stream-data-equal? curr-stream, "{"
-        compare is-group-start?, 0/false
-        break-if-=
-        # if top-level? and this is the final word, clear the stack
-        compare top-level?, 0/false
-        break-if-= $evaluate-sub:process-word
-        compare curr, end
-        break-if-!= $evaluate-sub:process-word
-        clear-value-stack out
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-group-end?/eax: boolean <- stream-data-equal? curr-stream, "}"
-        compare is-group-end?, 0/false
-        break-if-=
-        # if top-level? and this is the final word, clear the stack
-        compare top-level?, 0/false
-        break-if-= $evaluate-sub:process-word
-        compare curr, end
-        break-if-!= $evaluate-sub:process-word
-        clear-value-stack out
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-break?/eax: boolean <- stream-data-equal? curr-stream, "break"
-        compare is-break?, 0/false
-        break-if-=
-        # if curr == end, clear stack and break
-        # (TODO: move this into skip-rest-of-group)
-        compare curr, end
-        {
-          break-if-!=
-          clear-value-stack out
-          break $evaluate-sub:loop
-        }
-        # scan ahead to containing '}'
-        var next-word: (handle word)
-        var next-word-ah/eax: (addr handle word) <- address next-word
-        skip-rest-of-group curr, end, next-word-ah
-        var _curr/eax: (addr word) <- lookup *next-word-ah
-        curr <- copy _curr
-        # if '}' isn't found before end, we're rendering a word that isn't executed
-        # skip everything else and clear stack
-        var close-found?/eax: boolean <- word-equal? curr, "}"
-        compare close-found?, 0/false
-        {
-          break-if-!=
-          clear-value-stack out
-        }
-        loop $evaluate-sub:loop
-      }
-      {
-        var is-loop?/eax: boolean <- stream-data-equal? curr-stream, "loop"
-        compare is-loop?, 0/false
-        break-if-=
-        # scan back to containing '{'
-        var open-word: (handle word)
-        var open-word-ah/edx: (addr handle word) <- address open-word
-        scan-to-start-of-group curr, end, open-word-ah
-        # scan ahead to the containing '}'; record that as next word to eval at
-        var close-word: (handle word)
-        var close-word-ah/ebx: (addr handle word) <- address close-word
-        skip-rest-of-group curr, end, close-word-ah
-        var _curr/eax: (addr word) <- lookup *close-word-ah
-        curr <- copy _curr
-        # now eval until getting there
-        # TODO: can 'curr' be after 'end' at this point?
-        var open/eax: (addr word) <- lookup *open-word-ah
-        evaluate-sub open, curr, out, 0/nested
-        loop $evaluate-sub:loop
-      }
-      ## TEMPORARY HACKS; we're trying to avoid turning this into Forth
-      {
-        var is-dup?/eax: boolean <- stream-data-equal? curr-stream, "dup"
-        compare is-dup?, 0/false
-        break-if-=
-        # read src-val from out
-        var out2/esi: (addr value-stack) <- copy out
-        var top-addr/ecx: (addr int) <- get out2, top
-        compare *top-addr, 0
-        break-if-<=
-        var data-ah/eax: (addr handle array value) <- get out2, data
-        var data/eax: (addr array value) <- lookup *data-ah
-        var top/ecx: int <- copy *top-addr
-        top <- decrement
-        var offset/edx: (offset value) <- compute-offset data, top
-        var src-val/edx: (addr value) <- index data, offset
-        # push a copy of it
-        top <- increment
-        var offset/ebx: (offset value) <- compute-offset data, top
-        var target-val/ebx: (addr value) <- index data, offset
-        copy-object src-val, target-val
-        # commit
-        var top-addr/ecx: (addr int) <- get out2, top
-        increment *top-addr
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-swap?/eax: boolean <- stream-data-equal? curr-stream, "swap"
-        compare is-swap?, 0/false
-        break-if-=
-        # read top-val from out
-        var out2/esi: (addr value-stack) <- copy out
-        var top-addr/ecx: (addr int) <- get out2, top
-        compare *top-addr, 0
-        break-if-<=
-        var data-ah/eax: (addr handle array value) <- get out2, data
-        var data/eax: (addr array value) <- lookup *data-ah
-        var top/ecx: int <- copy *top-addr
-        top <- decrement
-        var offset/edx: (offset value) <- compute-offset data, top
-        var top-val/edx: (addr value) <- index data, offset
-        # read next val from out
-        top <- decrement
-        var offset/ebx: (offset value) <- compute-offset data, top
-        var pen-top-val/ebx: (addr value) <- index data, offset
-        # swap
-        var tmp: value
-        var tmp-a/eax: (addr value) <- address tmp
-        copy-object top-val, tmp-a
-        copy-object pen-top-val, top-val
-        copy-object tmp-a, pen-top-val
-        break $evaluate-sub:process-word
-      }
-      ### if the word starts with a quote and ends with a quote, turn it into a string
-      {
-        rewind-stream curr-stream
-        var start/eax: byte <- stream-first curr-stream
-        compare start, 0x22/double-quote
-        break-if-!=
-        var end/eax: byte <- stream-final curr-stream
-        compare end, 0x22/double-quote
-        break-if-!=
-        var h: (handle array byte)
-        var s/eax: (addr handle array byte) <- address h
-        unquote-stream-to-array curr-stream, s  # leak
-        push-string-to-value-stack out, *s
-        break $evaluate-sub:process-word
-      }
-      ### if the word starts with a '[' and ends with a ']', turn it into an array
-      {
-        rewind-stream curr-stream
-        var start/eax: byte <- stream-first curr-stream
-        compare start, 0x5b/open-bracket
-        break-if-!=
-        var end/eax: byte <- stream-final curr-stream
-        compare end, 0x5d/close-bracket
-        break-if-!=
-        # wastefully create a new input string to strip quotes
-        var h: (handle array value)
-        var input-ah/eax: (addr handle array byte) <- address h
-        unquote-stream-to-array curr-stream, input-ah  # leak
-        # wastefully parse input into int-array
-        # TODO: support parsing arrays of other types
-        var input/eax: (addr array byte) <- lookup *input-ah
-        var h2: (handle array int)
-        var int-array-ah/esi: (addr handle array int) <- address h2
-        parse-array-of-decimal-ints input, int-array-ah  # leak
-        var _int-array/eax: (addr array int) <- lookup *int-array-ah
-        var int-array/esi: (addr array int) <- copy _int-array
-        var len/ebx: int <- length int-array
-        # push value-array of same size as int-array
-        var h3: (handle array value)
-        var value-array-ah/eax: (addr handle array value) <- address h3
-        populate value-array-ah, len
-        push-array-to-value-stack out, *value-array-ah
-        # copy int-array into value-array
-        var _value-array/eax: (addr array value) <- lookup *value-array-ah
-        var value-array/edi: (addr array value) <- copy _value-array
-        var i/eax: int <- copy 0
-        {
-          compare i, len
-          break-if->=
-          var src-addr/ecx: (addr int) <- index int-array, i
-          var src/ecx: int <- copy *src-addr
-          var src-f/xmm0: float <- convert src
-          var dest-offset/edx: (offset value) <- compute-offset value-array, i
-          var dest-val/edx: (addr value) <- index value-array, dest-offset
-          var dest/edx: (addr float) <- get dest-val, number-data
-          copy-to *dest, src-f
-          i <- increment
-          loop
-        }
-        break $evaluate-sub:process-word
-      }
-      ### otherwise assume it's a literal number and push it (can't parse floats yet)
-      {
-        var n/eax: int <- parse-decimal-int-from-stream curr-stream
-        var n-f/xmm0: float <- convert n
-        push-number-to-value-stack out, n-f
-      }
-    }
-    # termination check
-    compare curr, end
-    break-if-=
-    # update
-    var next-word-ah/edx: (addr handle word) <- get curr, next
-    var _curr/eax: (addr word) <- lookup *next-word-ah
-    curr <- copy _curr
-    #
-    loop
-  }
-}
-
-fn skip-word _curr: (addr word), end: (addr word), out: (addr handle word) {
-  var curr/eax: (addr word) <- copy _curr
-  var bracket-count/ecx: int <- copy 0
-  var result-ah/esi: (addr handle word) <- get curr, next
-  {
-    var result-val/eax: (addr word) <- lookup *result-ah
-    compare result-val, end
-    break-if-=
-    {
-      var open?/eax: boolean <- word-equal? result-val, "{"
-      compare open?, 0/false
-      break-if-=
-      bracket-count <- increment
-    }
-    {
-      var close?/eax: boolean <- word-equal? result-val, "}"
-      compare close?, 0/false
-      break-if-=
-      bracket-count <- decrement
-      compare bracket-count, 0
-      {
-        break-if->=
-        abort "'->' cannot be final word in a {} group"  # TODO: error-handling
-      }
-    }
-    compare bracket-count, 0
-    break-if-=
-    result-ah <- get result-val, next
-    loop
-  }
-  copy-object result-ah, out
-}
-
-# find next "}" from curr
-# if you hit 'end' before "}", return null
-fn skip-rest-of-group _curr: (addr word), end: (addr word), out: (addr handle word) {
-  var curr/eax: (addr word) <- copy _curr
-  var bracket-count/ecx: int <- copy 0
-  var result-ah/esi: (addr handle word) <- get curr, next
-  $skip-rest-of-group:loop: {
-    var result-val/eax: (addr word) <- lookup *result-ah
-    compare result-val, 0
-    break-if-=
-    {
-      var open?/eax: boolean <- word-equal? result-val, "{"
-      compare open?, 0/false
-      break-if-=
-      bracket-count <- increment
-    }
-    {
-      var close?/eax: boolean <- word-equal? result-val, "}"
-      compare close?, 0/false
-      break-if-=
-      compare bracket-count, 0
-      break-if-= $skip-rest-of-group:loop
-      bracket-count <- decrement
-    }
-    compare result-val, end
-    {
-      break-if-!=
-      clear-object out
-      return
-    }
-    result-ah <- get result-val, next
-    loop
-  }
-  copy-object result-ah, out
-}
-
-fn scan-to-start-of-group _curr: (addr word), end: (addr word), out: (addr handle word) {
-  var curr/eax: (addr word) <- copy _curr
-  var bracket-count/ecx: int <- copy 0
-  var result-ah/esi: (addr handle word) <- get curr, prev
-  $scan-to-start-of-group:loop: {
-    var result-val/eax: (addr word) <- lookup *result-ah
-    compare result-val, 0
-    break-if-=
-    compare result-val, end  # not sure what error-detection should happen here
-    break-if-=
-    {
-      var open?/eax: boolean <- word-equal? result-val, "{"
-      compare open?, 0/false
-      break-if-=
-      compare bracket-count, 0
-      break-if-= $scan-to-start-of-group:loop
-      bracket-count <- increment
-    }
-    {
-      var close?/eax: boolean <- word-equal? result-val, "}"
-      compare close?, 0/false
-      break-if-=
-      bracket-count <- decrement
-    }
-    result-ah <- get result-val, prev
-    loop
-  }
-  copy-object result-ah, out
-}
-
-fn test-eval-arithmetic {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 1 +", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-arithmetic stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 2, "F - test-eval-arithmetic result"
-}
-
-fn test-eval-string {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "\"abc\"", in  # TODO support spaces within strings
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-string stack size"
-  var out-data-ah/eax: (addr handle array value) <- get out, data
-  var out-data/eax: (addr array value) <- lookup *out-data-ah
-  var v/eax: (addr value) <- index out-data, 0
-  var type/ecx: (addr int) <- get v, type
-  check-ints-equal *type, 1/text, "F - test-eval-string type"
-  var text-ah/eax: (addr handle array byte) <- get v, text-data
-  var text/eax: (addr array byte) <- lookup *text-ah
-  check-strings-equal text, "abc", "F - test-eval-string result"
-}
-
-fn test-eval-compare-lesser {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 <", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-compare-lesser stack size"
-  var result/eax: boolean <- pop-boolean-from-value-stack out
-  check result, "F - test-eval-compare-lesser result"
-}
-
-fn test-eval-compare-greater {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "2 1 >", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-compare-greater stack size"
-  var result/eax: boolean <- pop-boolean-from-value-stack out
-  check result, "F - test-eval-compare-greater result"
-}
-
-fn test-eval-compare-equal-fails {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 ==", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-compare-equal-fails stack size"
-  var result/eax: boolean <- pop-boolean-from-value-stack out
-  check-not result, "F - test-eval-compare-equal-fails result"
-}
-
-fn test-eval-compare-equal {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "2 2 ==", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-compare-equal stack size"
-  var result/eax: boolean <- pop-boolean-from-value-stack out
-  check result, "F - test-eval-compare-equal result"
-}
-
-fn test-eval-conditional {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 < -> 3", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-conditional stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 3, "F - test-eval-conditional result"
-}
-
-# if top of stack is false, `->` skips one word
-fn test-eval-conditional-skipped {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 > -> 3", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-conditional-skipped stack size"
-}
-
-# curlies have no effect in isolation
-fn test-eval-group {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "{ 1 } 1 +", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-group stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 2, "F - test-eval-group result"
-}
-
-fn test-eval-group-open-at-end {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 1 + {", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-group-open-at-end stack size"
-}
-
-fn test-eval-group-close-at-end {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "{ 1 1 + }", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-group-close-at-end stack size"
-}
-
-fn test-eval-conditional-skips-group {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 > -> { 3 } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # out contains just the final sentinel '9'
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-conditional-skips-group stack size"
-}
-
-fn test-eval-conditional-skips-nested-group {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 > -> { { 3 } 4 } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # out contains just the final sentinel '9'
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-conditional-skips-nested-group stack size"
-}
-
-# TODO: test error-handling on:
-#   1 2 > -> }
-
-# incomplete group rendering at 'break'
-fn test-eval-break-incomplete {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "3 { 4 break", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # break clears stack when final word
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-break-incomplete stack size"
-}
-
-# incomplete group rendering after 'break'
-fn test-eval-break-incomplete-2 {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "3 { 4 break 5", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # break clears stack when final word
-#?   dump-stack out
-  var len/eax: int <- value-stack-length out
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
-  check-ints-equal len, 0, "F - test-eval-break-incomplete-2 stack size"
-}
-
-# complete group rendering at 'break'
-fn test-eval-break-incomplete-3 {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "{ 3 break 4 } 5", in
-  # end = 'break'
-  var w-ah/edx: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  skip-one-word w-ah, end-ah
-  skip-one-word end-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # break clears stack when final word
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-break-incomplete-3 stack size"
-}
-
-# { 1 break 2 } 3   => empty
-#           ^
-#
-# { 1 break 2 } 3   => empty
-#             ^
-#
-# { 1 break 2 } 3   => 1 3
-#               ^
-
-# break skips to next containing `}`
-fn test-eval-break {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "3 { 4 break 5 } +", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # result is 3+4, not 4+5
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-break stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 7, "F - test-eval-break result"
-}
-
-fn test-eval-break-nested {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "3 { 4 break { 5 } 6 } +", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # result is 3+4, skipping remaining numbers
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-break-nested stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 7, "F - test-eval-break-nested result"
-}
-
-#? 1 2 3 4 6 5 { <       -> break loop }
-#?  1 2 3 4 6 5   false            2
-#?    1 2 3 4 6   4                1
-#?      1 2 3 4   3
-#?        1 2 3   2
-#?          1 2   1
-#?            1
-
-#? 1 2 3 4 { 3 ==     -> return loop }
-#?  1 2 3 4   3 false            2      => 3
-#?    1 2 3   4 3                1
-#?      1 2   3 2
-#?        1   2 1
-#?            1
-
-# loop skips to previous containing `{` and continues evaluating until control
-# leaves the group
-fn test-eval-loop {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 4 3 { < -> break loop } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # evaluation order: 1 2 4 3 { < -> loop { < -> break 9
-  # stack contents: 9
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-loop stack size"
-}
-
-fn test-eval-loop-2 {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 4 3 { 4 == -> break loop } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # evaluation order: 1 2 4 3 { 4 == -> loop { 4 == -> break 9
-  # stack contents: 1 2 9
-#?   dump-stack out
-  var len/eax: int <- value-stack-length out
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
-  check-ints-equal len, 3, "F - test-eval-loop-2 stack size"
-}
-
-fn test-eval-loop-conditional {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 3 { 3 == -> loop } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # evaluation order: 1 2 3 { 3 == -> loop { 3 == -> 9
-  # stack contents: 1 9
-#?   dump-stack out
-  var len/eax: int <- value-stack-length out
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
-  check-ints-equal len, 2, "F - test-eval-loop-2-conditional stack size"
-}
-
-fn test-eval-loop-with-words-after-in-group {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 3 { 3 == -> loop 37 } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # evaluation order: 1 2 3 { 3 == -> loop { 3 == -> 37 } 9
-  # stack contents: 1 37 9
-#?   dump-stack out
-  var len/eax: int <- value-stack-length out
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
-  check-ints-equal len, 3, "F - test-eval-loop-with-words-after-in-group stack size"
-}
diff --git a/baremetal/shell/eval.mu.debug b/baremetal/shell/eval.mu.debug
deleted file mode 100644
index 2fcf6ec5..00000000
--- a/baremetal/shell/eval.mu.debug
+++ /dev/null
@@ -1,1070 +0,0 @@
-# evaluator (and parser) for the Mu shell language
-# inputs:
-#   a list of lines, each a list of words, each an editable gap-buffer
-#   end: a word to stop at
-# output:
-#   a stack of values to render that summarizes the result of evaluation until 'end'
-
-# Key features of the language:
-#   words matching '=___' create bindings
-#   line boundaries clear the stack (but not bindings)
-#   { and } for grouping words
-#   break and loop for control flow within groups
-#   -> for conditionally skipping the next word or group
-
-# Example: Pushing numbers from 1 to n on the stack
-#
-#   3 =n
-#   { n 1 <     -> break n n 1- =n loop }
-#
-# Stack as we evaluate each word in the second line:
-#     3 1 false          3 3 2  3   1
-#       3                  3 3      2
-#                                   3
-
-# Rules beyond simple postfix:
-#   If the final word is `->`, clear stack
-#   If the final word is `break`, pop top of stack
-#
-#   `{` and `}` don't affect evaluation
-#   If the final word is `{` or `}`, clear stack (to suppress rendering it)
-#
-#   If `->` in middle and top of stack is falsy, skip next word or group
-#
-#   If `break` in middle executes, skip to next containing `}`
-#     If no containing `}`, clear stack (incomplete)
-#
-#   If `loop` in middle executes, skip to previous containing `{`
-#     If no containing `}`, clear stack (error)
-
-fn evaluate _in: (addr line), end: (addr word), out: (addr value-stack) {
-  clear-value-stack out
-  var line/eax: (addr line) <- copy _in
-  var curr-ah/eax: (addr handle word) <- get line, data
-  var curr/eax: (addr word) <- lookup *curr-ah
-  evaluate-sub curr, end, out, 1/top-level
-}
-
-fn evaluate-sub _curr: (addr word), end: (addr word), out: (addr value-stack), top-level?: boolean {
-  var curr/ecx: (addr word) <- copy _curr
-  var curr-stream-storage: (stream byte 0x10)
-  var curr-stream/edi: (addr stream byte) <- address curr-stream-storage
-  $evaluate-sub:loop: {
-    # safety net (should never hit)
-    compare curr, 0
-    break-if-=
-    # pull next word in for parsing
-    emit-word curr, curr-stream
-#?     {
-#?       clear-screen 0/screen
-#?       dump-stack out
-#?       var foo/eax: int <- render-word 0/screen, curr, 0/x, 0/y, 0/no-cursor
-#?       {
-#?         var key/eax: byte <- read-key 0/keyboard
-#?         compare key, 0
-#?         loop-if-=
-#?       }
-#?     }
-    $evaluate-sub:process-word: {
-      ### if curr-stream is an operator, perform it
-      {
-        var is-add?/eax: boolean <- stream-data-equal? curr-stream, "+"
-        compare is-add?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- add b
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-sub?/eax: boolean <- stream-data-equal? curr-stream, "-"
-        compare is-sub?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- subtract b
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-mul?/eax: boolean <- stream-data-equal? curr-stream, "*"
-        compare is-mul?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- multiply b
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-div?/eax: boolean <- stream-data-equal? curr-stream, "/"
-        compare is-div?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- divide b
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-sqrt?/eax: boolean <- stream-data-equal? curr-stream, "sqrt"
-        compare is-sqrt?, 0/false
-        break-if-=
-        var a/xmm0: float <- pop-number-from-value-stack out
-        a <- square-root a
-        push-number-to-value-stack out, a
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-lesser?/eax: boolean <- stream-data-equal? curr-stream, "<"
-        compare is-lesser?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        compare a, b
-        {
-          break-if-float<
-          push-boolean-to-value-stack out, 0/false
-          break $evaluate-sub:process-word
-        }
-        push-boolean-to-value-stack out, 1/true
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-greater?/eax: boolean <- stream-data-equal? curr-stream, ">"
-        compare is-greater?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        compare a, b
-        {
-          break-if-float>
-          push-boolean-to-value-stack out, 0/false
-          break $evaluate-sub:process-word
-        }
-        push-boolean-to-value-stack out, 1/true
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-equal?/eax: boolean <- stream-data-equal? curr-stream, "=="  # TODO support non-numbers
-        compare is-equal?, 0/false
-        break-if-=
-        var _b/xmm0: float <- pop-number-from-value-stack out
-        var b/xmm1: float <- copy _b
-        var a/xmm0: float <- pop-number-from-value-stack out
-        compare a, b
-        {
-          break-if-=
-          push-boolean-to-value-stack out, 0/false
-          break $evaluate-sub:process-word
-        }
-        push-boolean-to-value-stack out, 1/true
-        break $evaluate-sub:process-word
-      }
-      ## control flow
-      {
-        var is-conditional?/eax: boolean <- stream-data-equal? curr-stream, "->"
-        compare is-conditional?, 0/false
-        break-if-=
-        var a/eax: boolean <- pop-boolean-from-value-stack out
-        compare a, 0/false
-        {
-          break-if-!=
-          # if a is false, skip one word
-          var next-word: (handle word)
-          var next-word-ah/eax: (addr handle word) <- address next-word
-          skip-word curr, end, next-word-ah
-          var _curr/eax: (addr word) <- lookup *next-word-ah
-          curr <- copy _curr
-        }
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-group-start?/eax: boolean <- stream-data-equal? curr-stream, "{"
-        compare is-group-start?, 0/false
-        break-if-=
-        # if top-level? and this is the final word, clear the stack
-        compare top-level?, 0/false
-        break-if-= $evaluate-sub:process-word
-        compare curr, end
-        break-if-!= $evaluate-sub:process-word
-        clear-value-stack out
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-group-end?/eax: boolean <- stream-data-equal? curr-stream, "}"
-        compare is-group-end?, 0/false
-        break-if-=
-        # if top-level? and this is the final word, clear the stack
-        compare top-level?, 0/false
-        break-if-= $evaluate-sub:process-word
-        compare curr, end
-        break-if-!= $evaluate-sub:process-word
-        clear-value-stack out
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-break?/eax: boolean <- stream-data-equal? curr-stream, "break"
-        compare is-break?, 0/false
-        break-if-=
-        # if curr == end, clear stack and break
-        compare curr, end
-        {
-          break-if-!=
-          clear-value-stack out
-          break $evaluate-sub:loop
-        }
-        # scan ahead to containing '}'
-        # if it isn't found before end, skip everything else
-        var next-word: (handle word)
-        var next-word-ah/eax: (addr handle word) <- address next-word
-        skip-rest-of-group curr, end, next-word-ah
-#?   {
-#?     var dummy/eax: int <- render-words 0/screen, next-word-ah, 0/x, 5/y, 0/no-cursor
-#?   }
-        var _curr/eax: (addr word) <- lookup *next-word-ah
-        curr <- copy _curr
-        var close-found?/eax: boolean <- word-equal? curr, "}"
-        compare close-found?, 0/false
-        {
-          break-if-!=
-          clear-value-stack out
-        }
-        loop $evaluate-sub:loop
-      }
-      {
-        var is-loop?/eax: boolean <- stream-data-equal? curr-stream, "loop"
-        compare is-loop?, 0/false
-        break-if-=
-        # scan back to containing '{'
-        var open-word: (handle word)
-        var open-word-ah/edx: (addr handle word) <- address open-word
-        scan-to-start-of-group curr, end, open-word-ah
-        # scan ahead to the containing '}'; record that as next word to eval at
-        var close-word: (handle word)
-        var close-word-ah/ebx: (addr handle word) <- address close-word
-        skip-rest-of-group curr, end, close-word-ah
-        var _curr/eax: (addr word) <- lookup *close-word-ah
-        curr <- copy _curr
-        # now eval until getting there
-        # TODO: can 'curr' be after 'end' at this point?
-        var open/eax: (addr word) <- lookup *open-word-ah
-        evaluate-sub open, curr, out, 0/nested
-        loop $evaluate-sub:loop
-      }
-      ## TEMPORARY HACKS; we're trying to avoid turning this into Forth
-      {
-        var is-dup?/eax: boolean <- stream-data-equal? curr-stream, "dup"
-        compare is-dup?, 0/false
-        break-if-=
-        # read src-val from out
-        var out2/esi: (addr value-stack) <- copy out
-        var top-addr/ecx: (addr int) <- get out2, top
-        compare *top-addr, 0
-        break-if-<=
-        var data-ah/eax: (addr handle array value) <- get out2, data
-        var data/eax: (addr array value) <- lookup *data-ah
-        var top/ecx: int <- copy *top-addr
-        top <- decrement
-        var offset/edx: (offset value) <- compute-offset data, top
-        var src-val/edx: (addr value) <- index data, offset
-        # push a copy of it
-        top <- increment
-        var offset/ebx: (offset value) <- compute-offset data, top
-        var target-val/ebx: (addr value) <- index data, offset
-        copy-object src-val, target-val
-        # commit
-        var top-addr/ecx: (addr int) <- get out2, top
-        increment *top-addr
-        break $evaluate-sub:process-word
-      }
-      {
-        var is-swap?/eax: boolean <- stream-data-equal? curr-stream, "swap"
-        compare is-swap?, 0/false
-        break-if-=
-        # read top-val from out
-        var out2/esi: (addr value-stack) <- copy out
-        var top-addr/ecx: (addr int) <- get out2, top
-        compare *top-addr, 0
-        break-if-<=
-        var data-ah/eax: (addr handle array value) <- get out2, data
-        var data/eax: (addr array value) <- lookup *data-ah
-        var top/ecx: int <- copy *top-addr
-        top <- decrement
-        var offset/edx: (offset value) <- compute-offset data, top
-        var top-val/edx: (addr value) <- index data, offset
-        # read next val from out
-        top <- decrement
-        var offset/ebx: (offset value) <- compute-offset data, top
-        var pen-top-val/ebx: (addr value) <- index data, offset
-        # swap
-        var tmp: value
-        var tmp-a/eax: (addr value) <- address tmp
-        copy-object top-val, tmp-a
-        copy-object pen-top-val, top-val
-        copy-object tmp-a, pen-top-val
-        break $evaluate-sub:process-word
-      }
-      ### if the word starts with a quote and ends with a quote, turn it into a string
-      {
-        rewind-stream curr-stream
-        var start/eax: byte <- stream-first curr-stream
-        compare start, 0x22/double-quote
-        break-if-!=
-        var end/eax: byte <- stream-final curr-stream
-        compare end, 0x22/double-quote
-        break-if-!=
-        var h: (handle array byte)
-        var s/eax: (addr handle array byte) <- address h
-        unquote-stream-to-array curr-stream, s  # leak
-        push-string-to-value-stack out, *s
-        break $evaluate-sub:process-word
-      }
-      ### if the word starts with a '[' and ends with a ']', turn it into an array
-      {
-        rewind-stream curr-stream
-        var start/eax: byte <- stream-first curr-stream
-        compare start, 0x5b/open-bracket
-        break-if-!=
-        var end/eax: byte <- stream-final curr-stream
-        compare end, 0x5d/close-bracket
-        break-if-!=
-        # wastefully create a new input string to strip quotes
-        var h: (handle array value)
-        var input-ah/eax: (addr handle array byte) <- address h
-        unquote-stream-to-array curr-stream, input-ah  # leak
-        # wastefully parse input into int-array
-        # TODO: support parsing arrays of other types
-        var input/eax: (addr array byte) <- lookup *input-ah
-        var h2: (handle array int)
-        var int-array-ah/esi: (addr handle array int) <- address h2
-        parse-array-of-decimal-ints input, int-array-ah  # leak
-        var _int-array/eax: (addr array int) <- lookup *int-array-ah
-        var int-array/esi: (addr array int) <- copy _int-array
-        var len/ebx: int <- length int-array
-        # push value-array of same size as int-array
-        var h3: (handle array value)
-        var value-array-ah/eax: (addr handle array value) <- address h3
-        populate value-array-ah, len
-        push-array-to-value-stack out, *value-array-ah
-        # copy int-array into value-array
-        var _value-array/eax: (addr array value) <- lookup *value-array-ah
-        var value-array/edi: (addr array value) <- copy _value-array
-        var i/eax: int <- copy 0
-        {
-          compare i, len
-          break-if->=
-          var src-addr/ecx: (addr int) <- index int-array, i
-          var src/ecx: int <- copy *src-addr
-          var src-f/xmm0: float <- convert src
-          var dest-offset/edx: (offset value) <- compute-offset value-array, i
-          var dest-val/edx: (addr value) <- index value-array, dest-offset
-          var dest/edx: (addr float) <- get dest-val, number-data
-          copy-to *dest, src-f
-          i <- increment
-          loop
-        }
-        break $evaluate-sub:process-word
-      }
-      ### otherwise assume it's a literal number and push it (can't parse floats yet)
-      {
-        var n/eax: int <- parse-decimal-int-from-stream curr-stream
-        var n-f/xmm0: float <- convert n
-        push-number-to-value-stack out, n-f
-      }
-    }
-    # termination check
-    compare curr, end
-    break-if-=
-    # update
-    var next-word-ah/edx: (addr handle word) <- get curr, next
-    var _curr/eax: (addr word) <- lookup *next-word-ah
-    curr <- copy _curr
-    #
-    loop
-  }
-}
-
-fn skip-word _curr: (addr word), end: (addr word), out: (addr handle word) {
-  var curr/eax: (addr word) <- copy _curr
-  var bracket-count/ecx: int <- copy 0
-  var result-ah/esi: (addr handle word) <- get curr, next
-  {
-    var result-val/eax: (addr word) <- lookup *result-ah
-    compare result-val, end
-    break-if-=
-    {
-      var open?/eax: boolean <- word-equal? result-val, "{"
-      compare open?, 0/false
-      break-if-=
-      bracket-count <- increment
-    }
-    {
-      var close?/eax: boolean <- word-equal? result-val, "}"
-      compare close?, 0/false
-      break-if-=
-      bracket-count <- decrement
-      compare bracket-count, 0
-      {
-        break-if->=
-        abort "'->' cannot be final word in a {} group"  # TODO: error-handling
-      }
-    }
-    compare bracket-count, 0
-    break-if-=
-    result-ah <- get result-val, next
-    loop
-  }
-  copy-object result-ah, out
-}
-
-# find next '}' from curr
-# if you hit 'end' first, return null
-fn skip-rest-of-group _curr: (addr word), end: (addr word), out: (addr handle word) {
-  var curr/eax: (addr word) <- copy _curr
-  var bracket-count/ecx: int <- copy 0
-  var y/edx: int <- copy 0x18
-  var result-ah/esi: (addr handle word) <- get curr, next
-  $skip-rest-of-group:loop: {
-    var result-val/eax: (addr word) <- lookup *result-ah
-    compare result-val, 0
-    break-if-=
-#?     {
-#?       var dummy/eax: int <- render-word 0/screen, result-val, 0/x, y, 0/false
-#?       var dummy/eax: byte <- read-key 0/keyboard
-#?       y <- increment
-#?     }
-    compare result-val, end
-    {
-      break-if-!=
-#?       {
-#?         var d1/eax: int <- copy 0
-#?         var d2/ecx: int <- copy 0
-#?         d1, d2 <- draw-text-wrapping-right-then-down-over-full-screen 0/screen, "clearing", 0/x, 0x10/y, 6/fg, 0/bg
-#?       }
-      clear-object out
-      return
-    }
-    {
-      var open?/eax: boolean <- word-equal? result-val, "{"
-      compare open?, 0/false
-      break-if-=
-      bracket-count <- increment
-    }
-    {
-      var close?/eax: boolean <- word-equal? result-val, "}"
-      compare close?, 0/false
-      break-if-=
-      compare bracket-count, 0
-      break-if-= $skip-rest-of-group:loop
-      bracket-count <- decrement
-    }
-    result-ah <- get result-val, next
-    loop
-  }
-  copy-object result-ah, out
-}
-
-fn scan-to-start-of-group _curr: (addr word), end: (addr word), out: (addr handle word) {
-  var curr/eax: (addr word) <- copy _curr
-  var bracket-count/ecx: int <- copy 0
-  var result-ah/esi: (addr handle word) <- get curr, prev
-  $scan-to-start-of-group:loop: {
-    var result-val/eax: (addr word) <- lookup *result-ah
-    compare result-val, 0
-    break-if-=
-    compare result-val, end  # not sure what error-detection should happen here
-    break-if-=
-    {
-      var open?/eax: boolean <- word-equal? result-val, "{"
-      compare open?, 0/false
-      break-if-=
-      compare bracket-count, 0
-      break-if-= $scan-to-start-of-group:loop
-      bracket-count <- increment
-    }
-    {
-      var close?/eax: boolean <- word-equal? result-val, "}"
-      compare close?, 0/false
-      break-if-=
-      bracket-count <- decrement
-    }
-    result-ah <- get result-val, prev
-    loop
-  }
-  copy-object result-ah, out
-}
-
-fn test-eval-arithmetic {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 1 +", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-arithmetic stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 2, "F - test-eval-arithmetic result"
-}
-
-fn test-eval-string {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "\"abc\"", in  # TODO support spaces within strings
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-string stack size"
-  var out-data-ah/eax: (addr handle array value) <- get out, data
-  var out-data/eax: (addr array value) <- lookup *out-data-ah
-  var v/eax: (addr value) <- index out-data, 0
-  var type/ecx: (addr int) <- get v, type
-  check-ints-equal *type, 1/text, "F - test-eval-string type"
-  var text-ah/eax: (addr handle array byte) <- get v, text-data
-  var text/eax: (addr array byte) <- lookup *text-ah
-  check-strings-equal text, "abc", "F - test-eval-string result"
-}
-
-fn test-eval-compare-lesser {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 <", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-compare-lesser stack size"
-  var result/eax: boolean <- pop-boolean-from-value-stack out
-  check result, "F - test-eval-compare-lesser result"
-}
-
-fn test-eval-compare-greater {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "2 1 >", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-compare-greater stack size"
-  var result/eax: boolean <- pop-boolean-from-value-stack out
-  check result, "F - test-eval-compare-greater result"
-}
-
-fn test-eval-compare-equal-fails {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 ==", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-compare-equal-fails stack size"
-  var result/eax: boolean <- pop-boolean-from-value-stack out
-  check-not result, "F - test-eval-compare-equal-fails result"
-}
-
-fn test-eval-compare-equal {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "2 2 ==", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-compare-equal stack size"
-  var result/eax: boolean <- pop-boolean-from-value-stack out
-  check result, "F - test-eval-compare-equal result"
-}
-
-fn test-eval-conditional {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 < -> 3", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-conditional stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 3, "F - test-eval-conditional result"
-}
-
-# if top of stack is false, `->` skips one word
-fn test-eval-conditional-skipped {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 > -> 3", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-conditional-skipped stack size"
-}
-
-# curlies have no effect in isolation
-fn test-eval-group {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "{ 1 } 1 +", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-group stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 2, "F - test-eval-group result"
-}
-
-fn test-eval-group-open-at-end {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 1 + {", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-group-open-at-end stack size"
-}
-
-fn test-eval-group-close-at-end {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "{ 1 1 + }", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  #
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-group-close-at-end stack size"
-}
-
-fn test-eval-conditional-skips-group {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 > -> { 3 } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # out contains just the final sentinel '9'
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-conditional-skips-group stack size"
-}
-
-fn test-eval-conditional-skips-nested-group {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 > -> { { 3 } 4 } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # out contains just the final sentinel '9'
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-conditional-skips-nested-group stack size"
-}
-
-# TODO: test error-handling on:
-#   1 2 > -> }
-
-# incomplete group rendering at 'break'
-fn test-eval-break-incomplete {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "3 { 4 break", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # break clears stack when final word
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-break-incomplete stack size"
-}
-
-# incomplete group rendering after 'break'
-fn test-eval-break-incomplete-2 {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "3 { 4 break 5", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # break clears stack when final word
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 0, "F - test-eval-break-incomplete-2 stack size"
-}
-
-# complete group rendering at 'break'
-fn test-eval-break-incomplete-3 {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "{ 3 break 4 } 5", in
-  # end = 'break'
-  var w-ah/edx: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  skip-one-word w-ah, end-ah
-  skip-one-word end-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-#?   {
-#?     var dummy/eax: int <- render-words 0/screen, w-ah, 0/x, 0/y, 0/no-cursor
-#?     var dummy/eax: int <- render-words 0/screen, end-ah, 0/x, 1/y, 0/no-cursor
-#?   }
-  evaluate in, end, out
-#?   dump-stack out
-  # break clears stack when final word
-  var len/eax: int <- value-stack-length out
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
-  check-ints-equal len, 0, "F - test-eval-break-incomplete-3 stack size"
-}
-
-# { 1 break 2 } 3   => empty
-#           ^
-#
-# { 1 break 2 } 3   => empty
-#             ^
-#
-# { 1 break 2 } 3   => 1 3
-#               ^
-
-# break skips to next containing `}`
-fn test-eval-break {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "3 { 4 break 5 } +", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # result is 3+4, not 4+5
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-break stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 7, "F - test-eval-break result"
-}
-
-fn test-eval-break-nested {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "3 { 4 break { 5 } 6 } +", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # result is 3+4, skipping remaining numbers
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-break-nested stack size"
-  var n/xmm0: float <- pop-number-from-value-stack out
-  var n2/eax: int <- convert n
-  check-ints-equal n2, 7, "F - test-eval-break-nested result"
-}
-
-#? 1 2 3 4 6 5 { <       -> break loop }
-#?  1 2 3 4 6 5   false            2
-#?    1 2 3 4 6   4                1
-#?      1 2 3 4   3
-#?        1 2 3   2
-#?          1 2   1
-#?            1
-
-#? 1 2 3 4 { 3 ==     -> return loop }
-#?  1 2 3 4   3 false            2      => 3
-#?    1 2 3   4 3                1
-#?      1 2   3 2
-#?        1   2 1
-#?            1
-
-# loop skips to previous containing `{` and continues evaluating until control
-# leaves the group
-fn test-eval-loop {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 4 3 { < -> break loop } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # evaluation order: 1 2 4 3 { < -> loop { < -> break 9
-  # stack contents: 9
-  var len/eax: int <- value-stack-length out
-  check-ints-equal len, 1, "F - test-eval-loop stack size"
-}
-
-fn test-eval-loop-2 {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 4 3 { 4 == -> break loop } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # evaluation order: 1 2 4 3 { 4 == -> loop { 4 == -> break 9
-  # stack contents: 1 2 9
-#?   dump-stack out
-  var len/eax: int <- value-stack-length out
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
-  check-ints-equal len, 3, "F - test-eval-loop-2 stack size"
-}
-
-fn test-eval-loop-conditional {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 3 { 3 == -> loop } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # evaluation order: 1 2 3 { 3 == -> loop { 3 == -> 9
-  # stack contents: 1 9
-#?   dump-stack out
-  var len/eax: int <- value-stack-length out
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
-  check-ints-equal len, 2, "F - test-eval-loop-2-conditional stack size"
-}
-
-fn test-eval-loop-with-words-after-in-group {
-  # in
-  var in-storage: line
-  var in/esi: (addr line) <- address in-storage
-  parse-line "1 2 3 { 3 == -> loop 37 } 9", in
-  # end
-  var w-ah/eax: (addr handle word) <- get in, data
-  var end-h: (handle word)
-  var end-ah/ecx: (addr handle word) <- address end-h
-  final-word w-ah, end-ah
-  var end/eax: (addr word) <- lookup *end-ah
-  # out
-  var out-storage: value-stack
-  var out/edi: (addr value-stack) <- address out-storage
-  initialize-value-stack out, 8
-  #
-  evaluate in, end, out
-  # evaluation order: 1 2 3 { 3 == -> loop { 3 == -> 37 } 9
-  # stack contents: 1 37 9
-#?   dump-stack out
-  var len/eax: int <- value-stack-length out
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
-  check-ints-equal len, 3, "F - test-eval-loop-with-words-after-in-group stack size"
-}
diff --git a/baremetal/shell/line.mu b/baremetal/shell/line.mu
deleted file mode 100644
index b67e3521..00000000
--- a/baremetal/shell/line.mu
+++ /dev/null
@@ -1,275 +0,0 @@
-type line {
-  name: (handle array byte)
-  data: (handle word)
-  cursor: (handle word)
-  next: (handle line)
-  prev: (handle line)
-}
-
-# initialize line with a single empty word
-fn initialize-line _line: (addr line) {
-  var line/esi: (addr line) <- copy _line
-  var word-ah/eax: (addr handle word) <- get line, data
-  allocate word-ah
-  var cursor-ah/ecx: (addr handle word) <- get line, cursor
-  copy-object word-ah, cursor-ah
-  var word/eax: (addr word) <- lookup *word-ah
-  initialize-word word
-}
-
-fn num-words-in-line _in: (addr line) -> _/eax: int {
-  var in/esi: (addr line) <- copy _in
-  var curr-ah/ecx: (addr handle word) <- get in, data
-  var result/edi: int <- copy 0
-  {
-    var curr/eax: (addr word) <- lookup *curr-ah
-    compare curr, 0
-    break-if-=
-    curr-ah <- get curr, next
-    result <- increment
-    loop
-  }
-  return result
-}
-
-fn line-list-length lines: (addr handle line) -> _/eax: int {
-  var curr-ah/esi: (addr handle line) <- copy lines
-  var result/edi: int <- copy 0
-  {
-    var curr/eax: (addr line) <- lookup *curr-ah
-    compare curr, 0
-    break-if-=
-    curr-ah <- get curr, next
-    result <- increment
-    loop
-  }
-  return result
-}
-
-fn render-line screen: (addr screen), _line: (addr line), x: int, y: int, render-cursor?: boolean -> _/eax: int {
-  var line/eax: (addr line) <- copy _line
-  var first-word-ah/esi: (addr handle word) <- get line, data
-  # cursor-word
-  var cursor-word/edi: int <- copy 0
-  compare render-cursor?, 0/false
-  {
-    break-if-=
-    var cursor-word-ah/eax: (addr handle word) <- get line, cursor
-    var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
-    cursor-word <- copy _cursor-word
-  }
-  #
-  var result/eax: int <- render-words screen, first-word-ah, x, y, cursor-word
-  return result
-}
-
-fn parse-line in: (addr array byte), _out: (addr line) {
-  var out/edi: (addr line) <- copy _out
-  initialize-line out
-  var dest/eax: (addr handle word) <- get out, data
-  parse-words in, dest
-}
-
-#? fn main {
-#?   # line = [aaa, bbb, ccc, ddd]
-#?   var line-storage: line
-#?   var w-ah/eax: (addr handle word) <- get line-storage, data
-#?   allocate-word-with w-ah, "aaa"
-#?   append-word-at-end-with w-ah, "bbb"
-#?   append-word-at-end-with w-ah, "ccc"
-#?   append-word-at-end-with w-ah, "ddd"
-#?   var cursor-ah/ecx: (addr handle word) <- get line-storage, cursor
-#?   var w/eax: (addr word) <- lookup *w-ah
-#?   var next-ah/eax: (addr handle word) <- get w, next
-#?   copy-object next-ah, cursor-ah
-#?   var line-addr/eax: (addr line) <- address line-storage
-#?   var dummy/eax: int <- render-line 0/screen, line-addr, 0/x, 0/y, 1/render-cursor
-#? }
-
-fn render-line-with-stack screen: (addr screen), _line: (addr line), x: int, y: int, render-cursor?: boolean -> _/eax: int, _/ecx: int {
-  var line/esi: (addr line) <- copy _line
-  # cursor-word
-  var cursor-word/edi: int <- copy 0
-  compare render-cursor?, 0/false
-  {
-    break-if-=
-    var cursor-word-ah/eax: (addr handle word) <- get line, cursor
-    var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
-    cursor-word <- copy _cursor-word
-  }
-  #
-  var curr-word-ah/eax: (addr handle word) <- get line, data
-  var _curr-word/eax: (addr word) <- lookup *curr-word-ah
-  var curr-word/edx: (addr word) <- copy _curr-word
-  var new-x/eax: int <- copy x  # increases each iteration
-  var new-y/ebx: int <- copy y  # compute max across all iterations
-  {
-    compare curr-word, 0
-    break-if-=
-    var curr-y/ecx: int <- copy 0
-    new-x, curr-y <- render-word-with-stack-and-cursor screen, line, curr-word, new-x, y, cursor-word
-    compare curr-y, new-y
-    {
-      break-if-<=
-      new-y <- copy curr-y
-    }
-    new-x <- add 1/inter-word-spacing
-    # update
-    var next-word-ah/eax: (addr handle word) <- get curr-word, next
-    var next-word/eax: (addr word) <- lookup *next-word-ah
-    curr-word <- copy next-word
-    loop
-  }
-  return new-x, new-y
-}
-
-fn render-word-with-stack-and-cursor screen: (addr screen), line: (addr line), curr-word: (addr word), x: int, y: int, _cursor-word-addr: int -> _/eax: int, _/ecx: int {
-  # print curr-word, with cursor if necessary
-  var render-cursor?/eax: boolean <- copy 0/false
-  var cursor-word-addr/ecx: int <- copy _cursor-word-addr
-  {
-    compare cursor-word-addr, curr-word
-    break-if-!=
-    render-cursor? <- copy 1/true
-  }
-  var new-x/eax: int <- render-word screen, curr-word, x, y, render-cursor?
-  add-to x, 1/word-stack-indent
-  var new-x-saved/edx: int <- copy new-x
-  add-to y, 2/word-stack-spacing
-  # compute stack until word
-  var stack-storage: value-stack
-  var stack/edi: (addr value-stack) <- address stack-storage
-  evaluate line, curr-word, stack
-  # render stack
-  var new-y/ecx: int <- copy 0
-  new-x, new-y <- render-value-stack screen, stack, x, y
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, new-x, 0xc/fg, 0/bg
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, new-y, 3/fg, 0/bg
-  compare new-x, new-x-saved
-  {
-    break-if->=
-    new-x <- copy new-x-saved
-  }
-#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, new-x, 7/fg, 0/bg
-  return new-x, new-y
-}
-
-fn test-render-line-with-stack-singleton {
-  var line-storage: line
-  var line/esi: (addr line) <- address line-storage
-  parse-line "1", line
-  # setup: screen
-  var screen-on-stack: screen
-  var screen/edi: (addr screen) <- address screen-on-stack
-  initialize-screen screen, 0x20, 4
-  #
-  var new-x/eax: int <- copy 0
-  var new-y/ecx: int <- copy 0
-  new-x, new-y <- render-line-with-stack screen, line, 0/x, 0/y, 0/no-cursor
-  check-screen-row screen, 0/y, "1   ", "F - test-render-line-with-stack-singleton/0"
-  check-screen-row screen, 1/y, "    ", "F - test-render-line-with-stack-singleton/1"
-                                # ___
-  check-screen-row screen, 2/y, "  1 ", "F - test-render-line-with-stack-singleton/2"
-  check-screen-row screen, 3/y, "    ", "F - test-render-line-with-stack-singleton/3"
-  # not bothering to test hash colors for numbers
-}
-
-fn test-render-line-with-stack {
-  var line-storage: line
-  var line/esi: (addr line) <- address line-storage
-  parse-line "1 2", line
-  # setup: screen
-  var screen-on-stack: screen
-  var screen/edi: (addr screen) <- address screen-on-stack
-  initialize-screen screen, 0x20, 8
-  #
-  var new-x/eax: int <- copy 0
-  var new-y/ecx: int <- copy 0
-  new-x, new-y <- render-line-with-stack screen, line, 0/x, 0/y, 0/no-cursor
-  check-screen-row screen, 0/y, "1    2   ", "F - test-render-line-with-stack/0"
-  check-screen-row screen, 1/y, "         ", "F - test-render-line-with-stack/1"
-                                # ___  ___
-  check-screen-row screen, 2/y, "  1    2 ", "F - test-render-line-with-stack/2"
-  check-screen-row screen, 3/y, "       1 ", "F - test-render-line-with-stack/3"
-  check-screen-row screen, 4/y, "         ", "F - test-render-line-with-stack/4"
-  # not bothering to test hash colors for numbers
-}
-
-# { } groups have no effect on the stack by default.
-fn test-render-line-with-stack-groups {
-  var line-storage: line
-  var line/esi: (addr line) <- address line-storage
-  parse-line "{ 1 2 }", line
-  # setup: screen
-  var screen-on-stack: screen
-  var screen/edi: (addr screen) <- address screen-on-stack
-  initialize-screen screen, 0x20, 8
-  #
-  var new-x/eax: int <- copy 0
-  var new-y/ecx: int <- copy 0
-  new-x, new-y <- render-line-with-stack screen, line, 0/x, 0/y, 0/no-cursor
-  check-screen-row screen, 0/y, "{  1    2    } ", "F - test-render-line-with-stack-groups/0"
-  check-screen-row screen, 1/y, "               ", "F - test-render-line-with-stack-groups/1"
-                                #    ___  ___
-  check-screen-row screen, 2/y, "     1    2    ", "F - test-render-line-with-stack-groups/2"
-  check-screen-row screen, 3/y, "          1    ", "F - test-render-line-with-stack-groups/3"
-  check-screen-row screen, 4/y, "               ", "F - test-render-line-with-stack-groups/4"
-}
-
-# break skips rest of the containing group
-#? fn test-render-line-with-break {
-#?   var line-storage: line
-#?   var line/esi: (addr line) <- address line-storage
-#?   parse-line "{ 1 break 2 }", line
-#?   # setup: screen
-#?   var screen-on-stack: screen
-#?   var screen/edi: (addr screen) <- address screen-on-stack
-#?   initialize-screen screen, 0x20, 8
-#?   #
-#?   var new-x/eax: int <- copy 0
-#?   var new-y/ecx: int <- copy 0
-#?   new-x, new-y <- render-line-with-stack screen, line, 0/x, 0/y, 0/no-cursor
-#?   check-screen-row screen, 0/y, "{  1    break  2    } ", "F - test-render-line-with-break/0"
-#?   check-screen-row screen, 1/y, "                      ", "F - test-render-line-with-break/1"
-#?                                 #    ___
-#?   check-screen-row screen, 2/y, "     1                ", "F - test-render-line-with-break/2"
-#? #?   check-screen-row screen, 3/y, "                      ", "F - test-render-line-with-break/3"
-#? }
-
-fn edit-line _self: (addr line), key: byte {
-  var self/esi: (addr line) <- copy _self
-  var cursor-word-ah/edx: (addr handle word) <- get self, cursor
-  var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
-  var cursor-word/ecx: (addr word) <- copy _cursor-word
-  compare key, 0x20/space
-  $edit-line:space: {
-    break-if-!=
-    append-word cursor-word-ah
-    var next-word-ah/eax: (addr handle word) <- get cursor-word, next
-    copy-object next-word-ah, cursor-word-ah
-    return
-  }
-  # otherwise insert key within current word
-  var g/edx: grapheme <- copy key
-  add-grapheme-to-word cursor-word, g
-  # silently ignore other hotkeys
-}
-
-fn main {
-  var line-storage: line
-  var line/esi: (addr line) <- address line-storage
-  initialize-line line
-  {
-    clear-screen 0/screen
-    var dummy1/eax: int <- copy 0
-    var dummy2/ecx: int <- copy 0
-    dummy1, dummy2 <- render-line-with-stack 0/screen, line, 2/x, 2/y, 1/show-cursor
-    {
-      var key/eax: byte <- read-key 0/keyboard
-      compare key, 0
-      loop-if-=
-      edit-line line, key
-    }
-    loop
-  }
-}
diff --git a/baremetal/shell/value-stack.mu b/baremetal/shell/value-stack.mu
deleted file mode 100644
index 8c9c2f47..00000000
--- a/baremetal/shell/value-stack.mu
+++ /dev/null
@@ -1,241 +0,0 @@
-# value stacks encode the result of a program at a single point in time
-# they are typically rendered vertically
-
-type value-stack {
-  data: (handle array value)
-  top: int
-}
-
-fn initialize-value-stack _self: (addr value-stack), n: int {
-  var self/esi: (addr value-stack) <- copy _self
-  var d/edi: (addr handle array value) <- get self, data
-  populate d, n
-  var top/eax: (addr int) <- get self, top
-  copy-to *top, 0
-}
-
-fn clear-value-stack _self: (addr value-stack) {
-  var self/esi: (addr value-stack) <- copy _self
-  var top/eax: (addr int) <- get self, top
-  copy-to *top, 0
-}
-
-fn push-number-to-value-stack _self: (addr value-stack), _val: float {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/ecx: (addr int) <- get self, top
-  var data-ah/edx: (addr handle array value) <- get self, data
-  var data/eax: (addr array value) <- lookup *data-ah
-  var top/edx: int <- copy *top-addr
-  var dest-offset/edx: (offset value) <- compute-offset data, top
-  var dest-addr/edx: (addr value) <- index data, dest-offset
-  var dest-addr2/eax: (addr float) <- get dest-addr, number-data
-  var val/xmm0: float <- copy _val
-  copy-to *dest-addr2, val
-  increment *top-addr
-  var type-addr/eax: (addr int) <- get dest-addr, type
-  copy-to *type-addr, 0/number
-}
-
-fn push-int-to-value-stack _self: (addr value-stack), _val: int {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/ecx: (addr int) <- get self, top
-  var data-ah/edx: (addr handle array value) <- get self, data
-  var data/eax: (addr array value) <- lookup *data-ah
-  var top/edx: int <- copy *top-addr
-  var dest-offset/edx: (offset value) <- compute-offset data, top
-  var dest-addr/edx: (addr value) <- index data, dest-offset
-  var dest-addr2/eax: (addr float) <- get dest-addr, number-data
-  var val/xmm0: float <- convert _val
-  copy-to *dest-addr2, val
-  increment *top-addr
-  var type-addr/eax: (addr int) <- get dest-addr, type
-  copy-to *type-addr, 0/number
-}
-
-fn push-string-to-value-stack _self: (addr value-stack), val: (handle array byte) {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/ecx: (addr int) <- get self, top
-  var data-ah/edx: (addr handle array value) <- get self, data
-  var data/eax: (addr array value) <- lookup *data-ah
-  var top/edx: int <- copy *top-addr
-  var dest-offset/edx: (offset value) <- compute-offset data, top
-  var dest-addr/edx: (addr value) <- index data, dest-offset
-  var dest-addr2/eax: (addr handle array byte) <- get dest-addr, text-data
-  copy-handle val, dest-addr2
-  var dest-addr3/eax: (addr int) <- get dest-addr, type
-  copy-to *dest-addr3, 1/string
-  increment *top-addr
-}
-
-fn push-array-to-value-stack _self: (addr value-stack), val: (handle array value) {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/ecx: (addr int) <- get self, top
-  var data-ah/edx: (addr handle array value) <- get self, data
-  var data/eax: (addr array value) <- lookup *data-ah
-  var top/edx: int <- copy *top-addr
-  var dest-offset/edx: (offset value) <- compute-offset data, top
-  var dest-addr/edx: (addr value) <- index data, dest-offset
-  var dest-addr2/eax: (addr handle array value) <- get dest-addr, array-data
-  copy-handle val, dest-addr2
-  # update type
-  var dest-addr3/eax: (addr int) <- get dest-addr, type
-  copy-to *dest-addr3, 2/array
-  increment *top-addr
-}
-
-fn push-boolean-to-value-stack _self: (addr value-stack), _val: boolean {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/ecx: (addr int) <- get self, top
-  var data-ah/edx: (addr handle array value) <- get self, data
-  var data/eax: (addr array value) <- lookup *data-ah
-  var top/edx: int <- copy *top-addr
-  var dest-offset/edx: (offset value) <- compute-offset data, top
-  var dest-addr/edx: (addr value) <- index data, dest-offset
-  var dest-addr2/eax: (addr boolean) <- get dest-addr, boolean-data
-  var val/esi: boolean <- copy _val
-  copy-to *dest-addr2, val
-  increment *top-addr
-  var type-addr/eax: (addr int) <- get dest-addr, type
-  copy-to *type-addr, 3/boolean
-}
-
-fn push-value-stack _self: (addr value-stack), val: (addr value) {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/ecx: (addr int) <- get self, top
-  var data-ah/edx: (addr handle array value) <- get self, data
-  var data/eax: (addr array value) <- lookup *data-ah
-  var top/edx: int <- copy *top-addr
-  var dest-offset/edx: (offset value) <- compute-offset data, top
-  var dest-addr/edx: (addr value) <- index data, dest-offset
-  copy-object val, dest-addr
-  increment *top-addr
-}
-
-fn pop-number-from-value-stack _self: (addr value-stack) -> _/xmm0: float {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/ecx: (addr int) <- get self, top
-  {
-    compare *top-addr, 0
-    break-if->
-    abort "pop number: empty stack"
-  }
-  decrement *top-addr
-  var data-ah/edx: (addr handle array value) <- get self, data
-  var data/eax: (addr array value) <- lookup *data-ah
-  var top/edx: int <- copy *top-addr
-  var dest-offset/edx: (offset value) <- compute-offset data, top
-  var result-addr/eax: (addr value) <- index data, dest-offset
-  var result-addr2/eax: (addr float) <- get result-addr, number-data
-  return *result-addr2
-}
-
-fn pop-boolean-from-value-stack _self: (addr value-stack) -> _/eax: boolean {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/ecx: (addr int) <- get self, top
-  {
-    compare *top-addr, 0
-    break-if->
-    abort "pop boolean: empty stack"
-  }
-  decrement *top-addr
-  var data-ah/edx: (addr handle array value) <- get self, data
-  var data/eax: (addr array value) <- lookup *data-ah
-  var top/edx: int <- copy *top-addr
-  var dest-offset/edx: (offset value) <- compute-offset data, top
-  var result-addr/eax: (addr value) <- index data, dest-offset
-  var result-addr2/eax: (addr boolean) <- get result-addr, boolean-data
-  return *result-addr2
-}
-
-fn value-stack-empty? _self: (addr value-stack) -> _/eax: boolean {
-  var self/esi: (addr value-stack) <- copy _self
-  var top/eax: (addr int) <- get self, top
-  compare *top, 0
-  {
-    break-if-!=
-    return 1/true
-  }
-  return 0/false
-}
-
-fn value-stack-length _self: (addr value-stack) -> _/eax: int {
-  var self/esi: (addr value-stack) <- copy _self
-  var top-addr/eax: (addr int) <- get self, top
-  return *top-addr
-}
-
-fn test-boolean {
-  var stack-storage: value-stack
-  var stack/esi: (addr value-stack) <- address stack-storage
-  push-boolean-to-value-stack stack, 0/false
-  var result/eax: boolean <- pop-boolean-from-value-stack stack
-  check-not result, "F - test-boolean/false"
-  push-boolean-to-value-stack stack, 1/true
-  var result/eax: boolean <- pop-boolean-from-value-stack stack
-  check result, "F - test-boolean/true"
-}
-
-fn dump-stack _self: (addr value-stack) {
-  var self/esi: (addr value-stack) <- copy _self
-  var data-ah/eax: (addr handle array value) <- get self, data
-  var _data/eax: (addr array value) <- lookup *data-ah
-  var data/edi: (addr array value) <- copy _data
-  var top-addr/ecx: (addr int) <- get self, top
-  var top/ecx: int <- copy *top-addr
-  top <- decrement
-  var y/edx: int <- copy 0xa
-  var dummy/eax: int <- draw-text-rightward-over-full-screen 0/screen, "==", 0/x, 9/y, 0xc/red, 0/bg
-  {
-    compare top, 0
-    break-if-<
-    var dest-offset/eax: (offset value) <- compute-offset data, top
-    var curr/eax: (addr value) <- index data, dest-offset
-    var dummy/eax: int <- render-value 0/screen, curr, 0/x, y, 0/no-color
-    top <- decrement
-    y <- increment
-    loop
-  }
-}
-
-fn render-value-stack screen: (addr screen), _self: (addr value-stack), x: int, y: int -> _/eax: int, _/ecx: int {
-  var self/ecx: (addr value-stack) <- copy _self
-  var data-ah/eax: (addr handle array value) <- get self, data
-  var _data/eax: (addr array value) <- lookup *data-ah
-  var data/edi: (addr array value) <- copy _data
-  var top-addr/eax: (addr int) <- get self, top
-  var curr-idx/ecx: int <- copy *top-addr
-  curr-idx <- decrement
-  var new-x/edx: int <- copy 0
-  {
-    compare curr-idx, 0
-    break-if-<
-    var dest-offset/eax: (offset value) <- compute-offset data, curr-idx
-    var curr/eax: (addr value) <- index data, dest-offset
-    var curr-x/eax: int <- render-value screen, curr, x, y, 1/top-level
-    {
-      compare curr-x, new-x
-      break-if-<=
-      new-x <- copy curr-x
-    }
-    curr-idx <- decrement
-    increment y
-    loop
-  }
-  return new-x, y
-}
-
-fn test-render-value-stack {
-  var stack-storage: value-stack
-  var stack/esi: (addr value-stack) <- address stack-storage
-  push-int-to-value-stack stack, 3
-  # setup: screen
-  var screen-on-stack: screen
-  var screen/edi: (addr screen) <- address screen-on-stack
-  initialize-screen screen, 0x20, 4
-  #
-  var final-x/eax: int <- copy 0
-  var final-y/ecx: int <- copy 0
-  final-x, final-y <- render-value-stack screen, stack, 0/x, 0/y
-  check-ints-equal final-y, 1, "F - test-render-value-stack y"
-  check-ints-equal final-x, 3, "F - test-render-value-stack x"
-}
diff --git a/baremetal/shell/word.mu b/baremetal/shell/word.mu
deleted file mode 100644
index ad57d040..00000000
--- a/baremetal/shell/word.mu
+++ /dev/null
@@ -1,718 +0,0 @@
-type word {
-  scalar-data: (handle gap-buffer)
-  next: (handle word)
-  prev: (handle word)
-}
-
-fn initialize-word _self: (addr word) {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  allocate data-ah
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  initialize-gap-buffer data
-}
-
-## some helpers for creating words. mostly for tests
-
-fn initialize-word-with _self: (addr word), s: (addr array byte) {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  allocate data-ah
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  initialize-gap-buffer-with data, s
-}
-
-fn allocate-word-with _out: (addr handle word), s: (addr array byte) {
-  var out/eax: (addr handle word) <- copy _out
-  allocate out
-  var out-addr/eax: (addr word) <- lookup *out
-  initialize-word-with out-addr, s
-}
-
-# just for tests for now
-# TODO: handle existing next
-# one implication of handles: append must take a handle
-fn append-word-with self-h: (handle word), s: (addr array byte) {
-  var self/eax: (addr word) <- lookup self-h
-  var next-ah/eax: (addr handle word) <- get self, next
-  allocate-word-with next-ah, s
-  var next/eax: (addr word) <- lookup *next-ah
-  var prev-ah/eax: (addr handle word) <- get next, prev
-  copy-handle self-h, prev-ah
-}
-
-# just for tests for now
-# TODO: handle existing prev
-fn prepend-word-with self-h: (handle word), s: (addr array byte) {
-  var self/eax: (addr word) <- lookup self-h
-  var prev-ah/eax: (addr handle word) <- get self, prev
-  allocate-word-with prev-ah, s
-  var prev/eax: (addr word) <- lookup *prev-ah
-  var next-ah/eax: (addr handle word) <- get prev, next
-  copy-handle self-h, next-ah
-}
-
-## real primitives
-
-fn move-word-contents _src-ah: (addr handle word), _dest-ah: (addr handle word) {
-  var dest-ah/eax: (addr handle word) <- copy _dest-ah
-  var _dest/eax: (addr word) <- lookup *dest-ah
-  var dest/edi: (addr word) <- copy _dest
-  var src-ah/eax: (addr handle word) <- copy _src-ah
-  var _src/eax: (addr word) <- lookup *src-ah
-  var src/esi: (addr word) <- copy _src
-  cursor-to-start src
-  var src-data-ah/eax: (addr handle gap-buffer) <- get src, scalar-data
-  var src-data/eax: (addr gap-buffer) <- lookup *src-data-ah
-  var src-stack/ecx: (addr grapheme-stack) <- get src-data, right
-  {
-    var done?/eax: boolean <- grapheme-stack-empty? src-stack
-    compare done?, 0/false
-    break-if-!=
-    var g/eax: grapheme <- pop-grapheme-stack src-stack
-    add-grapheme-to-word dest, g
-    loop
-  }
-}
-
-fn copy-word-contents-before-cursor _src-ah: (addr handle word), _dest-ah: (addr handle word) {
-  var dest-ah/eax: (addr handle word) <- copy _dest-ah
-  var _dest/eax: (addr word) <- lookup *dest-ah
-  var dest/edi: (addr word) <- copy _dest
-  var src-ah/eax: (addr handle word) <- copy _src-ah
-  var src/eax: (addr word) <- lookup *src-ah
-  var src-data-ah/eax: (addr handle gap-buffer) <- get src, scalar-data
-  var src-data/eax: (addr gap-buffer) <- lookup *src-data-ah
-  var src-stack/ecx: (addr grapheme-stack) <- get src-data, left
-  var src-stack-data-ah/eax: (addr handle array grapheme) <- get src-stack, data
-  var _src-stack-data/eax: (addr array grapheme) <- lookup *src-stack-data-ah
-  var src-stack-data/edx: (addr array grapheme) <- copy _src-stack-data
-  var top-addr/ecx: (addr int) <- get src-stack, top
-  var i/eax: int <- copy 0
-  {
-    compare i, *top-addr
-    break-if->=
-    var g/edx: (addr grapheme) <- index src-stack-data, i
-    add-grapheme-to-word dest, *g
-    i <- increment
-    loop
-  }
-}
-
-fn word-equal? _self: (addr word), s: (addr array byte) -> _/eax: boolean {
-  var self/esi: (addr word) <- copy _self
-  {
-    compare self, 0
-    break-if-!=
-    return 0/false
-  }
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: boolean <- gap-buffer-equal? data, s
-  return result
-}
-
-fn words-equal? _self: (addr word), _w: (addr word) -> _/eax: boolean {
-  var self/eax: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var _data/eax: (addr gap-buffer) <- lookup *data-ah
-  var data/ecx: (addr gap-buffer) <- copy _data
-  var w/eax: (addr word) <- copy _w
-  var w-data-ah/eax: (addr handle gap-buffer) <- get w, scalar-data
-  var w-data/eax: (addr gap-buffer) <- lookup *w-data-ah
-  var result/eax: boolean <- gap-buffers-equal? data, w-data
-  return result
-}
-
-fn word-length _self: (addr word) -> _/eax: int {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: int <- gap-buffer-length data
-  return result
-}
-
-fn skip-one-word _in: (addr handle word), out: (addr handle word) {
-  var in/eax: (addr handle word) <- copy _in
-  var curr/eax: (addr word) <- lookup *in
-  var next/eax: (addr handle word) <- get curr, next
-  copy-object next, out  # modify 'out' right at the end, just in case it's same as 'in'
-}
-
-fn final-word _in: (addr handle word), out: (addr handle word) {
-  var curr-h: (handle word)
-  var curr-ah/esi: (addr handle word) <- address curr-h
-  copy-object _in, curr-ah
-  var curr/eax: (addr word) <- copy 0
-  var next/edi: (addr handle word) <- copy 0
-  {
-    curr <- lookup *curr-ah
-    next <- get curr, next
-    curr <- lookup *next
-    compare curr, 0
-    break-if-=
-    copy-object next, curr-ah
-    loop
-  }
-  copy-object curr-ah, out  # modify 'out' right at the end, just in case it's same as 'in'
-}
-
-fn first-grapheme _self: (addr word) -> _/eax: grapheme {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: grapheme <- first-grapheme-in-gap-buffer data
-  return result
-}
-
-fn grapheme-before-cursor _self: (addr word) -> _/eax: grapheme {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: grapheme <- grapheme-before-cursor-in-gap-buffer data
-  return result
-}
-
-fn add-grapheme-to-word _self: (addr word), c: grapheme {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  add-grapheme-at-gap data, c
-}
-
-fn cursor-at-start? _self: (addr word) -> _/eax: boolean {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: boolean <- gap-at-start? data
-  return result
-}
-
-fn cursor-at-end? _self: (addr word) -> _/eax: boolean {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: boolean <- gap-at-end? data
-  return result
-}
-
-fn cursor-left _self: (addr word) {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var dummy/eax: grapheme <- gap-left data
-}
-
-fn cursor-right _self: (addr word) {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var dummy/eax: grapheme <- gap-right data
-}
-
-fn cursor-to-start _self: (addr word) {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  gap-to-start data
-}
-
-fn cursor-to-end _self: (addr word) {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  gap-to-end data
-}
-
-fn cursor-index _self: (addr word) -> _/eax: int {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: int <- index-of-gap data
-  return result
-}
-
-fn delete-before-cursor _self: (addr word) {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  delete-before-gap data
-}
-
-fn pop-after-cursor _self: (addr word) -> _/eax: grapheme {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: grapheme <- pop-after-gap data
-  return result
-}
-
-fn delete-next _self: (addr word) {
-  var self/esi: (addr word) <- copy _self
-  var next-ah/edi: (addr handle word) <- get self, next
-  var next/eax: (addr word) <- lookup *next-ah
-  compare next, 0
-  break-if-=
-  var next-next-ah/ecx: (addr handle word) <- get next, next
-  var self-ah/esi: (addr handle word) <- get next, prev
-  copy-object next-next-ah, next-ah
-  var new-next/eax: (addr word) <- lookup *next-next-ah
-  compare new-next, 0
-  break-if-=
-  var dest/eax: (addr handle word) <- get new-next, prev
-  copy-object self-ah, dest
-}
-
-fn render-word screen: (addr screen), _self: (addr word), x: int, y: int, render-cursor?: boolean -> _/eax: int {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: int <- render-gap-buffer screen, data, x, y, render-cursor?
-  return result
-}
-
-fn render-words screen: (addr screen), _words-ah: (addr handle word), x: int, y: int, cursor-word-addr: int -> _/eax: int {
-  var words-ah/eax: (addr handle word) <- copy _words-ah
-  var _words-a/eax: (addr word) <- lookup *words-ah
-  var words-a/ecx: (addr word) <- copy _words-a
-  compare words-a, 0
-  {
-    break-if-!=
-    return x
-  }
-  # print
-  var render-cursor?/edx: boolean <- copy 0/false
-  {
-    compare cursor-word-addr, words-a
-    break-if-!=
-    render-cursor? <- copy 1/true
-  }
-  var next-x/eax: int <- render-word screen, words-a, x, y, render-cursor?
-  var space/edx: grapheme <- copy 0x20/space
-  draw-grapheme screen, space, next-x, y, 3/fg=cyan, 0/bg
-  next-x <- increment
-  # recurse
-  var next-ah/ecx: (addr handle word) <- get words-a, next
-  next-x <- render-words screen, next-ah, next-x, y, cursor-word-addr
-  return next-x
-}
-
-fn test-render-words {
-  # words = [aaa, bbb, ccc, ddd]
-  var w-storage: (handle word)
-  var w-ah/esi: (addr handle word) <- address w-storage
-  allocate-word-with w-ah, "aaa"
-  append-word-at-end-with w-ah, "bbb"
-  append-word-at-end-with w-ah, "ccc"
-  append-word-at-end-with w-ah, "ddd"
-  # setup: screen
-  var screen-on-stack: screen
-  var screen/edi: (addr screen) <- address screen-on-stack
-  initialize-screen screen, 0x20, 4
-  #
-  var _w/eax: (addr word) <- lookup *w-ah
-  var w/ecx: (addr word) <- copy _w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "aaa  bbb  ccc  ddd  ", "F - test-render-words/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "   |                ", "F - test-render-words/0 cursor"
-  # - start moving cursor left through final word
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "aaa  bbb  ccc  ddd  ", "F - test-render-words/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "  |                 ", "F - test-render-words/1 cursor"
-  #
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "aaa  bbb  ccc  ddd  ", "F - test-render-words/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  " |                  ", "F - test-render-words/2 cursor"
-  #
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "aaa  bbb  ccc  ddd  ", "F - test-render-words/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "|                   ", "F - test-render-words/3 cursor"
-  # further moves left within the word change nothing
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "aaa  bbb  ccc  ddd  ", "F - test-render-words/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "|                   ", "F - test-render-words/4 cursor"
-  # - switch to next word
-  var w2-ah/eax: (addr handle word) <- get w, next
-  var _w/eax: (addr word) <- lookup *w2-ah
-  var w/ecx: (addr word) <- copy _w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "aaa  bbb  ccc  ddd  ", "F - test-render-words/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "        |           ", "F - test-render-words/5 cursor"
-  # now speed up a little
-  cursor-left w
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "aaa  bbb  ccc  ddd  ", "F - test-render-words/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "      |             ", "F - test-render-words/6 cursor"
-  #
-  var w2-ah/eax: (addr handle word) <- get w, next
-  var _w/eax: (addr word) <- lookup *w2-ah
-  var w/ecx: (addr word) <- copy _w
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "aaa  bbb  ccc  ddd  ", "F - test-render-words/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "            |       ", "F - test-render-words/7 cursor"
-}
-
-fn render-words-in-reverse screen: (addr screen), _words-ah: (addr handle word), x: int, y: int, cursor-word-addr: int -> _/eax: int {
-  var words-ah/eax: (addr handle word) <- copy _words-ah
-  var _words-a/eax: (addr word) <- lookup *words-ah
-  var words-a/ecx: (addr word) <- copy _words-a
-  compare words-a, 0
-  {
-    break-if-!=
-    return x
-  }
-  # recurse
-  var next-ah/eax: (addr handle word) <- get words-a, next
-  var next-x/eax: int <- render-words-in-reverse screen, next-ah, x, y, cursor-word-addr
-  # print
-  var render-cursor?/edx: boolean <- copy 0/false
-  {
-    compare cursor-word-addr, words-a
-    break-if-!=
-    render-cursor? <- copy 1/true
-  }
-  next-x <- render-word screen, words-a, next-x, y, render-cursor?
-  var space/ecx: grapheme <- copy 0x20/space
-  draw-grapheme screen, space, next-x, y, 3/fg=cyan, 0/bg
-  next-x <- increment
-  return next-x
-}
-
-fn test-render-words-in-reverse {
-  # words = [aaa, bbb, ccc, ddd]
-  var w-storage: (handle word)
-  var w-ah/esi: (addr handle word) <- address w-storage
-  allocate-word-with w-ah, "aaa"
-  append-word-at-end-with w-ah, "bbb"
-  append-word-at-end-with w-ah, "ccc"
-  append-word-at-end-with w-ah, "ddd"
-  # setup: screen
-  var screen-on-stack: screen
-  var screen/edi: (addr screen) <- address screen-on-stack
-  initialize-screen screen, 0x20, 4
-  #
-  var _w/eax: (addr word) <- lookup *w-ah
-  var w/ecx: (addr word) <- copy _w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "ddd  ccc  bbb  aaa  ", "F - test-render-words-in-reverse/0"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "                  | ", "F - test-render-words-in-reverse/0 cursor"
-  # - start moving cursor left through final word
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "ddd  ccc  bbb  aaa  ", "F - test-render-words-in-reverse/1"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "                 |  ", "F - test-render-words-in-reverse/1 cursor"
-  #
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "ddd  ccc  bbb  aaa  ", "F - test-render-words-in-reverse/2"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "                |   ", "F - test-render-words-in-reverse/2 cursor"
-  #
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "ddd  ccc  bbb  aaa  ", "F - test-render-words-in-reverse/3"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "               |    ", "F - test-render-words-in-reverse/3 cursor"
-  # further moves left within the word change nothing
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "ddd  ccc  bbb  aaa  ", "F - test-render-words-in-reverse/4"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "               |    ", "F - test-render-words-in-reverse/4 cursor"
-  # - switch to next word
-  var w2-ah/eax: (addr handle word) <- get w, next
-  var _w/eax: (addr word) <- lookup *w2-ah
-  var w/ecx: (addr word) <- copy _w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "ddd  ccc  bbb  aaa  ", "F - test-render-words-in-reverse/5"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "             |      ", "F - test-render-words-in-reverse/5 cursor"
-  # now speed up a little
-  cursor-left w
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "ddd  ccc  bbb  aaa  ", "F - test-render-words-in-reverse/6"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "           |        ", "F - test-render-words-in-reverse/6 cursor"
-  #
-  var w2-ah/eax: (addr handle word) <- get w, next
-  var _w/eax: (addr word) <- lookup *w2-ah
-  var w/ecx: (addr word) <- copy _w
-  cursor-left w
-  var cursor-word/eax: int <- copy w
-  var new-x/eax: int <- render-words-in-reverse screen, w-ah, 0/x, 0/y, cursor-word
-  check-screen-row screen, 0/y,                                   "ddd  ccc  bbb  aaa  ", "F - test-render-words-in-reverse/7"
-  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y,  "       |            ", "F - test-render-words-in-reverse/7 cursor"
-}
-
-# Gotcha with some word operations: ensure dest-ah isn't in the middle of some
-# existing chain of words. There are two pointers to patch, and you'll forget
-# to do the other one.
-fn copy-words _src-ah: (addr handle word), _dest-ah: (addr handle word) {
-  var src-ah/eax: (addr handle word) <- copy _src-ah
-  var src-a/eax: (addr word) <- lookup *src-ah
-  compare src-a, 0
-  break-if-=
-  # copy
-  var dest-ah/edi: (addr handle word) <- copy _dest-ah
-  copy-word src-a, dest-ah
-  # recurse
-  var rest: (handle word)
-  var rest-ah/ecx: (addr handle word) <- address rest
-  var next-src-ah/esi: (addr handle word) <- get src-a, next
-  copy-words next-src-ah, rest-ah
-  chain-words dest-ah, rest-ah
-}
-
-fn copy-words-in-reverse _src-ah: (addr handle word), _dest-ah: (addr handle word) {
-  var src-ah/eax: (addr handle word) <- copy _src-ah
-  var _src-a/eax: (addr word) <- lookup *src-ah
-  var src-a/esi: (addr word) <- copy _src-a
-  compare src-a, 0
-  break-if-=
-  # recurse
-  var next-src-ah/ecx: (addr handle word) <- get src-a, next
-  var dest-ah/edi: (addr handle word) <- copy _dest-ah
-  copy-words-in-reverse next-src-ah, dest-ah
-  #
-  copy-word-at-end src-a, dest-ah
-}
-
-fn copy-word-at-end src: (addr word), _dest-ah: (addr handle word) {
-  var dest-ah/edi: (addr handle word) <- copy _dest-ah
-  # if dest is null, copy and return
-  var dest-a/eax: (addr word) <- lookup *dest-ah
-  compare dest-a, 0
-  {
-    break-if-!=
-    copy-word src, dest-ah
-    return
-  }
-  # copy current word
-  var new: (handle word)
-  var new-ah/ecx: (addr handle word) <- address new
-  copy-word src, new-ah
-  # append it at the end
-  var curr-ah/edi: (addr handle word) <- copy dest-ah
-  {
-    var curr-a/eax: (addr word) <- lookup *curr-ah  # curr-a guaranteed not to be null
-    var next-ah/ecx: (addr handle word) <- get curr-a, next
-    var next-a/eax: (addr word) <- lookup *next-ah
-    compare next-a, 0
-    break-if-=
-    curr-ah <- copy next-ah
-    loop
-  }
-  chain-words curr-ah, new-ah
-}
-
-fn append-word-at-end-with _dest-ah: (addr handle word), s: (addr array byte) {
-  var dest-ah/edi: (addr handle word) <- copy _dest-ah
-  # if dest is null, copy and return
-  var dest-a/eax: (addr word) <- lookup *dest-ah
-  compare dest-a, 0
-  {
-    break-if-!=
-    allocate-word-with dest-ah, s
-    return
-  }
-  # otherwise append at end
-  var curr-ah/edi: (addr handle word) <- copy dest-ah
-  {
-    var curr-a/eax: (addr word) <- lookup *curr-ah  # curr-a guaranteed not to be null
-    var next-ah/ecx: (addr handle word) <- get curr-a, next
-    var next-a/eax: (addr word) <- lookup *next-ah
-    compare next-a, 0
-    break-if-=
-    curr-ah <- copy next-ah
-    loop
-  }
-  append-word-with *curr-ah, s
-}
-
-fn copy-word _src-a: (addr word), _dest-ah: (addr handle word) {
-  var dest-ah/eax: (addr handle word) <- copy _dest-ah
-  allocate dest-ah
-  var _dest-a/eax: (addr word) <- lookup *dest-ah
-  var dest-a/eax: (addr word) <- copy _dest-a
-  initialize-word dest-a
-  var dest/edi: (addr handle gap-buffer) <- get dest-a, scalar-data
-  var src-a/eax: (addr word) <- copy _src-a
-  var src/eax: (addr handle gap-buffer) <- get src-a, scalar-data
-  copy-gap-buffer src, dest
-}
-
-# one implication of handles: append must take a handle
-fn append-word _self-ah: (addr handle word) {
-  var saved-self-storage: (handle word)
-  var saved-self/eax: (addr handle word) <- address saved-self-storage
-  copy-object _self-ah, saved-self
-  var self-ah/esi: (addr handle word) <- copy _self-ah
-  var _self/eax: (addr word) <- lookup *self-ah
-  var self/ebx: (addr word) <- copy _self
-  # allocate new handle
-  var new: (handle word)
-  var new-ah/ecx: (addr handle word) <- address new
-  allocate new-ah
-  var new-addr/eax: (addr word) <- lookup new
-  initialize-word new-addr
-  # new->next = self->next
-  var src/esi: (addr handle word) <- get self, next
-  var dest/edi: (addr handle word) <- get new-addr, next
-  copy-object src, dest
-  # new->next->prev = new
-  {
-    var next-addr/eax: (addr word) <- lookup *src
-    compare next-addr, 0
-    break-if-=
-    dest <- get next-addr, prev
-    copy-object new-ah, dest
-  }
-  # new->prev = saved-self
-  dest <- get new-addr, prev
-  var saved-self-ah/eax: (addr handle word) <- address saved-self-storage
-  copy-object saved-self-ah, dest
-  # self->next = new
-  dest <- get self, next
-  copy-object new-ah, dest
-}
-
-fn chain-words _self-ah: (addr handle word), _next: (addr handle word) {
-  var self-ah/esi: (addr handle word) <- copy _self-ah
-  var _self/eax: (addr word) <- lookup *self-ah
-  var self/ecx: (addr word) <- copy _self
-  var dest/edx: (addr handle word) <- get self, next
-  var next-ah/edi: (addr handle word) <- copy _next
-  copy-object next-ah, dest
-  var next/eax: (addr word) <- lookup *next-ah
-  compare next, 0
-  break-if-=
-  dest <- get next, prev
-  copy-object self-ah, dest
-}
-
-fn emit-word _self: (addr word), out: (addr stream byte) {
-  var self/esi: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  emit-gap-buffer data, out
-}
-
-fn word-is-decimal-integer? _self: (addr word) -> _/eax: boolean {
-  var self/eax: (addr word) <- copy _self
-  var data-ah/eax: (addr handle gap-buffer) <- get self, scalar-data
-  var data/eax: (addr gap-buffer) <- lookup *data-ah
-  var result/eax: boolean <- gap-buffer-is-decimal-integer? data
-  return result
-}
-
-fn word-exists? haystack: (addr word), needle: (addr word) -> _/eax: boolean {
-  # base case
-  compare haystack, 0
-  {
-    break-if-!=
-    return 0/false
-  }
-  # check current word
-  var found?/eax: boolean <- words-equal? haystack, needle
-  compare found?, 0/false
-  {
-    break-if-=
-    return 1/true
-  }
-  # recurse
-  var curr/eax: (addr word) <- copy haystack
-  var next-ah/eax: (addr handle word) <- get curr, next
-  var next/eax: (addr word) <- lookup *next-ah
-  var result/eax: boolean <- word-exists? next, needle
-  return result
-}
-
-fn test-word-exists? {
-  var needle-storage: word
-  var needle/esi: (addr word) <- address needle-storage
-  initialize-word-with needle, "abc"
-  var w-storage: (handle word)
-  var w-ah/edi: (addr handle word) <- address w-storage
-  allocate w-ah
-  var _w/eax: (addr word) <- lookup *w-ah
-  var w/ecx: (addr word) <- copy _w
-  initialize-word-with w, "aaa"
-  #
-  var result/eax: boolean <- word-exists? w, w
-  check result, "F - test-word-exists? reflexive"
-  result <- word-exists? w, needle
-  check-not result, "F - test-word-exists? 1"
-  append-word-at-end-with w-ah, "bbb"
-  result <- word-exists? w, needle
-  check-not result, "F - test-word-exists? 2"
-  append-word-at-end-with w-ah, "abc"
-  result <- word-exists? w, needle
-  check result, "F - test-word-exists? 3"
-  append-word-at-end-with w-ah, "ddd"
-  result <- word-exists? w, needle
-  check result, "F - test-word-exists? 4"
-}
-
-fn word-list-length words: (addr handle word) -> _/eax: int {
-  var curr-ah/esi: (addr handle word) <- copy words
-  var result/edi: int <- copy 0
-  {
-    var curr/eax: (addr word) <- lookup *curr-ah
-    compare curr, 0
-    break-if-=
-    {
-      var word-len/eax: int <- word-length curr
-      result <- add word-len
-      result <- add 1/inter-word-margin
-    }
-    curr-ah <- get curr, next
-    loop
-  }
-  return result
-}
-
-# out-ah already has a word allocated and initialized
-fn parse-words in: (addr array byte), out-ah: (addr handle word) {
-  var in-stream: (stream byte 0x100)
-  var in-stream-a/esi: (addr stream byte) <- address in-stream
-  write in-stream-a, in
-  var cursor-word-ah/ebx: (addr handle word) <- copy out-ah
-  $parse-words:loop: {
-    var done?/eax: boolean <- stream-empty? in-stream-a
-    compare done?, 0/false
-    break-if-!=
-    var _g/eax: grapheme <- read-grapheme in-stream-a
-    var g/ecx: grapheme <- copy _g
-    # if not space, insert
-    compare g, 0x20/space
-    {
-      break-if-=
-      var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
-      add-grapheme-to-word cursor-word, g
-      loop $parse-words:loop
-    }
-    # otherwise insert word after and move cursor to it
-    append-word cursor-word-ah
-    var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
-    cursor-to-start cursor-word  # reset cursor in each function
-    cursor-word-ah <- get cursor-word, next
-    loop
-  }
-}