about summary refs log tree commit diff stats
path: root/shell
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-03-03 22:09:50 -0800
committerKartik K. Agaram <vc@akkartik.com>2021-03-03 22:21:03 -0800
commit71e4f3812982dba2efb471283d310224e8db363e (patch)
treeea111a1acb8b8845dbda39c0e1b4bac1d198143b /shell
parentc6b928be29ac8cdb4e4d6e1eaa20420ff03e5a4c (diff)
downloadmu-71e4f3812982dba2efb471283d310224e8db363e.tar.gz
7842 - new directory organization
Baremetal is now the default build target and therefore has its sources
at the top-level. Baremetal programs build using the phase-2 Mu toolchain
that requires a Linux kernel. This phase-2 codebase which used to be at
the top-level is now under the linux/ directory. Finally, the phase-2 toolchain,
while self-hosting, has a way to bootstrap from a C implementation, which
is now stored in linux/bootstrap. The bootstrap C implementation uses some
literate programming tools that are now in linux/bootstrap/tools.

So the whole thing has gotten inverted. Each directory should build one
artifact and include the main sources (along with standard library). Tools
used for building it are relegated to sub-directories, even though those
tools are often useful in their own right, and have had lots of interesting
programs written using them.

A couple of things have gotten dropped in this process:
  - I had old ways to run on just a Linux kernel, or with a Soso kernel.
    No more.
  - I had some old tooling for running a single test at the cursor. I haven't
    used that lately. Maybe I'll bring it back one day.

The reorg isn't done yet. Still to do:
  - redo documentation everywhere. All the README files, all other markdown,
    particularly vocabulary.md.
  - clean up how-to-run comments at the start of programs everywhere
  - rethink what to do with the html/ directory. Do we even want to keep
    supporting it?

In spite of these shortcomings, all the scripts at the top-level, linux/
and linux/bootstrap are working. The names of the scripts also feel reasonable.
This is a good milestone to take stock at.
Diffstat (limited to 'shell')
-rw-r--r--shell/cell.mu89
-rw-r--r--shell/eval.mu0
-rw-r--r--shell/gap-buffer.mu781
-rw-r--r--shell/grapheme-stack.mu280
-rw-r--r--shell/main.mu22
-rw-r--r--shell/parse.mu136
-rw-r--r--shell/print.mu260
-rw-r--r--shell/read.mu15
-rw-r--r--shell/sandbox.mu263
-rw-r--r--shell/tokenize.mu422
-rw-r--r--shell/trace.mu1449
-rw-r--r--shell/vimrc.vim2
12 files changed, 3719 insertions, 0 deletions
diff --git a/shell/cell.mu b/shell/cell.mu
new file mode 100644
index 00000000..59558fb9
--- /dev/null
+++ b/shell/cell.mu
@@ -0,0 +1,89 @@
+type cell {
+  type: int
+  # type 0: pair
+  left: (handle cell)
+  right: (handle cell)
+  # type 1: number
+  number-data: float
+  # type 2: symbol
+  # type 3: string
+  text-data: (handle stream byte)
+  # TODO: array, (associative) table, stream
+}
+
+fn allocate-symbol _out: (addr handle cell) {
+  var out/eax: (addr handle cell) <- copy _out
+  allocate out
+  var out-addr/eax: (addr cell) <- lookup *out
+  var type/ecx: (addr int) <- get out-addr, type
+  copy-to *type, 2/symbol
+  var dest-ah/eax: (addr handle stream byte) <- get out-addr, text-data
+  populate-stream dest-ah, 0x40/max-symbol-size
+}
+
+fn initialize-symbol _out: (addr handle cell), val: (addr array byte) {
+  var out/eax: (addr handle cell) <- copy _out
+  var out-addr/eax: (addr cell) <- lookup *out
+  var dest-ah/eax: (addr handle stream byte) <- get out-addr, text-data
+  var dest/eax: (addr stream byte) <- lookup *dest-ah
+  write dest, val
+}
+
+fn new-symbol out: (addr handle cell), val: (addr array byte) {
+  allocate-symbol out
+  initialize-symbol out, val
+}
+
+fn allocate-number _out: (addr handle cell) {
+  var out/eax: (addr handle cell) <- copy _out
+  allocate out
+  var out-addr/eax: (addr cell) <- lookup *out
+  var type/ecx: (addr int) <- get out-addr, type
+  copy-to *type, 1/number
+}
+
+fn initialize-integer _out: (addr handle cell), n: int {
+  var out/eax: (addr handle cell) <- copy _out
+  var out-addr/eax: (addr cell) <- lookup *out
+  var dest-ah/eax: (addr float) <- get out-addr, number-data
+  var src/xmm0: float <- convert n
+  copy-to *dest-ah, src
+}
+
+fn new-integer out: (addr handle cell), n: int {
+  allocate-number out
+  initialize-integer out, n
+}
+
+fn initialize-float _out: (addr handle cell), n: float {
+  var out/eax: (addr handle cell) <- copy _out
+  var out-addr/eax: (addr cell) <- lookup *out
+  var dest-ah/eax: (addr float) <- get out-addr, number-data
+  var src/xmm0: float <- copy n
+  copy-to *dest-ah, src
+}
+
+fn new-float out: (addr handle cell), n: float {
+  allocate-number out
+  initialize-float out, n
+}
+
+fn allocate-pair _out: (addr handle cell) {
+  var out/eax: (addr handle cell) <- copy _out
+  allocate out
+  # new cells have type pair by default
+}
+
+fn initialize-pair _out: (addr handle cell), left: (handle cell), right: (handle cell) {
+  var out/eax: (addr handle cell) <- copy _out
+  var out-addr/eax: (addr cell) <- lookup *out
+  var dest-ah/ecx: (addr handle cell) <- get out-addr, left
+  copy-handle left, dest-ah
+  dest-ah <- get out-addr, right
+  copy-handle right, dest-ah
+}
+
+fn new-pair out: (addr handle cell), left: (handle cell), right: (handle cell) {
+  allocate-pair out
+  initialize-pair out, left, right
+}
diff --git a/shell/eval.mu b/shell/eval.mu
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/shell/eval.mu
diff --git a/shell/gap-buffer.mu b/shell/gap-buffer.mu
new file mode 100644
index 00000000..81537e9c
--- /dev/null
+++ b/shell/gap-buffer.mu
@@ -0,0 +1,781 @@
+# primitive for editing a single word
+
+type gap-buffer {
+  left: grapheme-stack
+  right: grapheme-stack
+  # some fields for scanning incrementally through a gap-buffer
+  left-read-index: int
+  right-read-index: int
+}
+
+fn initialize-gap-buffer _self: (addr gap-buffer), max-word-size: int {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  initialize-grapheme-stack left, max-word-size
+  var right/eax: (addr grapheme-stack) <- get self, right
+  initialize-grapheme-stack right, max-word-size
+}
+
+# just for tests
+fn initialize-gap-buffer-with self: (addr gap-buffer), s: (addr array byte) {
+  initialize-gap-buffer self, 0x10/max-word-size
+  var stream-storage: (stream byte 0x10/max-word-size)
+  var stream/ecx: (addr stream byte) <- address stream-storage
+  write stream, s
+  {
+    var done?/eax: boolean <- stream-empty? stream
+    compare done?, 0/false
+    break-if-!=
+    var g/eax: grapheme <- read-grapheme stream
+    add-grapheme-at-gap self, g
+    loop
+  }
+}
+
+fn emit-gap-buffer _self: (addr gap-buffer), out: (addr stream byte) {
+  var self/esi: (addr gap-buffer) <- copy _self
+  clear-stream out
+  var left/eax: (addr grapheme-stack) <- get self, left
+  emit-stack-from-bottom left, out
+  var right/eax: (addr grapheme-stack) <- get self, right
+  emit-stack-from-top right, out
+}
+
+# dump stack from bottom to top
+fn emit-stack-from-bottom _self: (addr grapheme-stack), out: (addr stream byte) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/eax: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index data, i
+    write-grapheme out, *g
+    i <- increment
+    loop
+  }
+}
+
+# dump stack from top to bottom
+fn emit-stack-from-top _self: (addr grapheme-stack), out: (addr stream byte) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/eax: int <- copy *top-addr
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    var g/edx: (addr grapheme) <- index data, i
+    write-grapheme out, *g
+    i <- decrement
+    loop
+  }
+}
+
+# We implicitly render everything editable in a single color, and assume the
+# cursor is a single other color.
+fn render-gap-buffer-wrapping-right-then-down screen: (addr screen), _gap: (addr gap-buffer), xmin: int, ymin: int, xmax: int, ymax: int, render-cursor?: boolean -> _/eax: int, _/ecx: int {
+  var gap/esi: (addr gap-buffer) <- copy _gap
+  var left/edx: (addr grapheme-stack) <- get gap, left
+  var x2/eax: int <- copy 0
+  var y2/ecx: int <- copy 0
+  x2, y2 <- render-stack-from-bottom-wrapping-right-then-down screen, left, xmin, ymin, xmax, ymax, xmin, ymin
+  var right/edx: (addr grapheme-stack) <- get gap, right
+  x2, y2 <- render-stack-from-top-wrapping-right-then-down screen, right, xmin, ymin, xmax, ymax, x2, y2, render-cursor?
+  # decide whether we still need to print a cursor
+  var bg/ebx: int <- copy 0
+  compare render-cursor?, 0/false
+  {
+    break-if-=
+    # if the right side is empty, grapheme stack didn't print the cursor
+    var empty?/eax: boolean <- grapheme-stack-empty? right
+    compare empty?, 0/false
+    break-if-=
+    bg <- copy 7/cursor
+  }
+  # print a grapheme either way so that cursor position doesn't affect printed width
+  var space/edx: grapheme <- copy 0x20
+  x2, y2 <- render-grapheme screen, space, xmin, ymin, xmax, ymax, x2, y2, 3/fg=cyan, bg
+  return x2, y2
+}
+
+fn render-gap-buffer screen: (addr screen), gap: (addr gap-buffer), x: int, y: int, render-cursor?: boolean -> _/eax: int {
+  var _width/eax: int <- copy 0
+  var _height/ecx: int <- copy 0
+  _width, _height <- screen-size screen
+  var width/edx: int <- copy _width
+  var height/ebx: int <- copy _height
+  var x2/eax: int <- copy 0
+  var y2/ecx: int <- copy 0
+  x2, y2 <- render-gap-buffer-wrapping-right-then-down screen, gap, x, y, width, height, render-cursor?
+  return x2  # y2? yolo
+}
+
+fn gap-buffer-length _gap: (addr gap-buffer) -> _/eax: int {
+  var gap/esi: (addr gap-buffer) <- copy _gap
+  var left/eax: (addr grapheme-stack) <- get gap, left
+  var tmp/eax: (addr int) <- get left, top
+  var left-length/ecx: int <- copy *tmp
+  var right/esi: (addr grapheme-stack) <- get gap, right
+  tmp <- get right, top
+  var result/eax: int <- copy *tmp
+  result <- add left-length
+  return result
+}
+
+fn add-grapheme-at-gap _self: (addr gap-buffer), g: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  push-grapheme-stack left, g
+}
+
+fn gap-to-start self: (addr gap-buffer) {
+  {
+    var curr/eax: grapheme <- gap-left self
+    compare curr, -1
+    loop-if-!=
+  }
+}
+
+fn gap-to-end self: (addr gap-buffer) {
+  {
+    var curr/eax: grapheme <- gap-right self
+    compare curr, -1
+    loop-if-!=
+  }
+}
+
+fn gap-at-start? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var result/eax: boolean <- grapheme-stack-empty? left
+  return result
+}
+
+fn gap-at-end? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var right/eax: (addr grapheme-stack) <- get self, right
+  var result/eax: boolean <- grapheme-stack-empty? right
+  return result
+}
+
+fn gap-right _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var g/eax: grapheme <- copy 0
+  var right/ecx: (addr grapheme-stack) <- get self, right
+  g <- pop-grapheme-stack right
+  compare g, -1
+  {
+    break-if-=
+    var left/ecx: (addr grapheme-stack) <- get self, left
+    push-grapheme-stack left, g
+  }
+  return g
+}
+
+fn gap-left _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var g/eax: grapheme <- copy 0
+  {
+    var left/ecx: (addr grapheme-stack) <- get self, left
+    g <- pop-grapheme-stack left
+  }
+  compare g, -1
+  {
+    break-if-=
+    var right/ecx: (addr grapheme-stack) <- get self, right
+    push-grapheme-stack right, g
+  }
+  return g
+}
+
+fn index-of-gap _self: (addr gap-buffer) -> _/eax: int {
+  var self/eax: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var top-addr/eax: (addr int) <- get left, top
+  var result/eax: int <- copy *top-addr
+  return result
+}
+
+fn first-grapheme-in-gap-buffer _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # try to read from left
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var top-addr/ecx: (addr int) <- get left, top
+  compare *top-addr, 0
+  {
+    break-if-<=
+    var data-ah/eax: (addr handle array grapheme) <- get left, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var result-addr/eax: (addr grapheme) <- index data, 0
+    return *result-addr
+  }
+  # try to read from right
+  var right/eax: (addr grapheme-stack) <- get self, right
+  top-addr <- get right, top
+  compare *top-addr, 0
+  {
+    break-if-<=
+    var data-ah/eax: (addr handle array grapheme) <- get right, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var top/ecx: int <- copy *top-addr
+    top <- decrement
+    var result-addr/eax: (addr grapheme) <- index data, top
+    return *result-addr
+  }
+  # give up
+  return -1
+}
+
+fn grapheme-before-cursor-in-gap-buffer _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # try to read from left
+  var left/ecx: (addr grapheme-stack) <- get self, left
+  var top-addr/edx: (addr int) <- get left, top
+  compare *top-addr, 0
+  {
+    break-if-<=
+    var result/eax: grapheme <- pop-grapheme-stack left
+    push-grapheme-stack left, result
+    return result
+  }
+  # give up
+  return -1
+}
+
+fn delete-before-gap _self: (addr gap-buffer) {
+  var self/eax: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var dummy/eax: grapheme <- pop-grapheme-stack left
+}
+
+fn pop-after-gap _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/eax: (addr gap-buffer) <- copy _self
+  var right/eax: (addr grapheme-stack) <- get self, right
+  var result/eax: grapheme <- pop-grapheme-stack right
+  return result
+}
+
+fn gap-buffer-equal? _self: (addr gap-buffer), s: (addr array byte) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # complication: graphemes may be multiple bytes
+  # so don't rely on length
+  # instead turn the expected result into a stream and arrange to read from it in order
+  var stream-storage: (stream byte 0x10/max-word-size)
+  var expected-stream/ecx: (addr stream byte) <- address stream-storage
+  write expected-stream, s
+  # compare left
+  var left/edx: (addr grapheme-stack) <- get self, left
+  var result/eax: boolean <- prefix-match? left, expected-stream
+  compare result, 0/false
+  {
+    break-if-!=
+    return result
+  }
+  # compare right
+  var right/edx: (addr grapheme-stack) <- get self, right
+  result <- suffix-match? right, expected-stream
+  compare result, 0/false
+  {
+    break-if-!=
+    return result
+  }
+  # ensure there's nothing left over
+  result <- stream-empty? expected-stream
+  return result
+}
+
+fn test-gap-buffer-equal-from-end {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g, 0x10
+  #
+  var c/eax: grapheme <- copy 0x61/a
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  # gap is at end (right is empty)
+  var result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  check result, "F - test-gap-buffer-equal-from-end"
+}
+
+fn test-gap-buffer-equal-from-middle {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g, 0x10
+  #
+  var c/eax: grapheme <- copy 0x61/a
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  var dummy/eax: grapheme <- gap-left g
+  # gap is in the middle
+  var result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  check result, "F - test-gap-buffer-equal-from-middle"
+}
+
+fn test-gap-buffer-equal-from-start {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g, 0x10
+  #
+  var c/eax: grapheme <- copy 0x61/a
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  var dummy/eax: grapheme <- gap-left g
+  dummy <- gap-left g
+  dummy <- gap-left g
+  # gap is at the start
+  var result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  check result, "F - test-gap-buffer-equal-from-start"
+}
+
+fn test-gap-buffer-equal-fails {
+  # g = "aaa"
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g, 0x10
+  var c/eax: grapheme <- copy 0x61/a
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  add-grapheme-at-gap g, c
+  #
+  var result/eax: boolean <- gap-buffer-equal? g, "aa"
+  check-not result, "F - test-gap-buffer-equal-fails"
+}
+
+fn gap-buffers-equal? self: (addr gap-buffer), g: (addr gap-buffer) -> _/eax: boolean {
+  var tmp/eax: int <- gap-buffer-length self
+  var len/ecx: int <- copy tmp
+  var leng/eax: int <- gap-buffer-length g
+  compare len, leng
+  {
+    break-if-=
+    return 0/false
+  }
+  var i/edx: int <- copy 0
+  {
+    compare i, len
+    break-if->=
+    {
+      var tmp/eax: grapheme <- gap-index self, i
+      var curr/ecx: grapheme <- copy tmp
+      var currg/eax: grapheme <- gap-index g, i
+      compare curr, currg
+      break-if-=
+      return 0/false
+    }
+    i <- increment
+    loop
+  }
+  return 1/true
+}
+
+fn gap-index _self: (addr gap-buffer), _n: int -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var n/ebx: int <- copy _n
+  # if n < left->length, index into left
+  var left/edi: (addr grapheme-stack) <- get self, left
+  var left-len-a/edx: (addr int) <- get left, top
+  compare n, *left-len-a
+  {
+    break-if->=
+    var data-ah/eax: (addr handle array grapheme) <- get left, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var result/eax: (addr grapheme) <- index data, n
+    return *result
+  }
+  # shrink n
+  n <- subtract *left-len-a
+  # if n < right->length, index into right
+  var right/edi: (addr grapheme-stack) <- get self, right
+  var right-len-a/edx: (addr int) <- get right, top
+  compare n, *right-len-a
+  {
+    break-if->=
+    var data-ah/eax: (addr handle array grapheme) <- get right, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    # idx = right->len - n - 1
+    var idx/ebx: int <- copy n
+    idx <- subtract *right-len-a
+    idx <- negate
+    idx <- subtract 1
+    var result/eax: (addr grapheme) <- index data, idx
+    return *result
+  }
+  # error
+  abort "gap-index: out of bounds"
+  return 0
+}
+
+fn test-gap-buffers-equal? {
+  var _a: gap-buffer
+  var a/esi: (addr gap-buffer) <- address _a
+  initialize-gap-buffer-with a, "abc"
+  var _b: gap-buffer
+  var b/edi: (addr gap-buffer) <- address _b
+  initialize-gap-buffer-with b, "abc"
+  var _c: gap-buffer
+  var c/ebx: (addr gap-buffer) <- address _c
+  initialize-gap-buffer-with c, "ab"
+  var _d: gap-buffer
+  var d/edx: (addr gap-buffer) <- address _d
+  initialize-gap-buffer-with d, "abd"
+  #
+  var result/eax: boolean <- gap-buffers-equal? a, a
+  check result, "F - test-gap-buffers-equal? - reflexive"
+  result <- gap-buffers-equal? a, b
+  check result, "F - test-gap-buffers-equal? - equal"
+  # length not equal
+  result <- gap-buffers-equal? a, c
+  check-not result, "F - test-gap-buffers-equal? - not equal"
+  # contents not equal
+  result <- gap-buffers-equal? a, d
+  check-not result, "F - test-gap-buffers-equal? - not equal 2"
+  result <- gap-buffers-equal? d, a
+  check-not result, "F - test-gap-buffers-equal? - not equal 3"
+}
+
+fn test-gap-buffer-index {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  # gap is at end, all contents are in left
+  var g/eax: grapheme <- gap-index gap, 0
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x61/a, "F - test-gap-index/left-1"
+  var g/eax: grapheme <- gap-index gap, 1
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x62/b, "F - test-gap-index/left-2"
+  var g/eax: grapheme <- gap-index gap, 2
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x63/c, "F - test-gap-index/left-3"
+  # now check when everything is to the right
+  gap-to-start gap
+  rewind-gap-buffer gap
+  var g/eax: grapheme <- gap-index gap, 0
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x61/a, "F - test-gap-index/right-1"
+  var g/eax: grapheme <- gap-index gap, 1
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x62/b, "F - test-gap-index/right-2"
+  var g/eax: grapheme <- gap-index gap, 2
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x63/c, "F - test-gap-index/right-3"
+}
+
+fn copy-gap-buffer _src-ah: (addr handle gap-buffer), _dest-ah: (addr handle gap-buffer) {
+  # obtain src-a, dest-a
+  var src-ah/eax: (addr handle gap-buffer) <- copy _src-ah
+  var _src-a/eax: (addr gap-buffer) <- lookup *src-ah
+  var src-a/esi: (addr gap-buffer) <- copy _src-a
+  var dest-ah/eax: (addr handle gap-buffer) <- copy _dest-ah
+  var _dest-a/eax: (addr gap-buffer) <- lookup *dest-ah
+  var dest-a/edi: (addr gap-buffer) <- copy _dest-a
+  # copy left grapheme-stack
+  var src/ecx: (addr grapheme-stack) <- get src-a, left
+  var dest/edx: (addr grapheme-stack) <- get dest-a, left
+  copy-grapheme-stack src, dest
+  # copy right grapheme-stack
+  src <- get src-a, right
+  dest <- get dest-a, right
+  copy-grapheme-stack src, dest
+}
+
+fn gap-buffer-is-decimal-integer? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var curr/ecx: (addr grapheme-stack) <- get self, left
+  var result/eax: boolean <- grapheme-stack-is-decimal-integer? curr
+  {
+    compare result, 0/false
+    break-if-=
+    curr <- get self, right
+    result <- grapheme-stack-is-decimal-integer? curr
+  }
+  return result
+}
+
+fn test-render-gap-buffer-without-cursor {
+  # setup
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 0/no-cursor
+  check-screen-row screen, 0/y, "abc ", "F - test-render-gap-buffer-without-cursor"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-without-cursor: result"
+                                                                # abc
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "    ", "F - test-render-gap-buffer-without-cursor: bg"
+}
+
+fn test-render-gap-buffer-with-cursor-at-end {
+  # setup
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  gap-to-end gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor
+  check-screen-row screen, 0/y, "abc ", "F - test-render-gap-buffer-with-cursor-at-end"
+  # we've drawn one extra grapheme for the cursor
+  check-ints-equal x, 4, "F - test-render-gap-buffer-with-cursor-at-end: result"
+                                                                # abc
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "   |", "F - test-render-gap-buffer-with-cursor-at-end: bg"
+}
+
+fn test-render-gap-buffer-with-cursor-in-middle {
+  # setup
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  gap-to-end gap
+  var dummy/eax: grapheme <- gap-left gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor
+  check-screen-row screen, 0/y, "abc ", "F - test-render-gap-buffer-with-cursor-in-middle"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-with-cursor-in-middle: result"
+                                                                # abc
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "  | ", "F - test-render-gap-buffer-with-cursor-in-middle: bg"
+}
+
+fn test-render-gap-buffer-with-cursor-at-start {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  gap-to-start gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor
+  check-screen-row screen, 0/y, "abc ", "F - test-render-gap-buffer-with-cursor-at-start"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-with-cursor-at-start: result"
+                                                                # abc
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|   ", "F - test-render-gap-buffer-with-cursor-at-start: bg"
+}
+
+## some primitives for scanning through a gap buffer
+# don't modify the gap buffer while scanning
+# this includes moving the cursor around
+
+# restart scan without affecting gap-buffer contents
+fn rewind-gap-buffer _self: (addr gap-buffer) {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var dest/eax: (addr int) <- get self, left-read-index
+  copy-to *dest, 0
+  dest <- get self, right-read-index
+  copy-to *dest, 0
+}
+
+fn gap-buffer-scan-done? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # more in left?
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var left-size/eax: int <- grapheme-stack-length left
+  var left-read-index/ecx: (addr int) <- get self, left-read-index
+  compare *left-read-index, left-size
+  {
+    break-if->=
+    return 0/false
+  }
+  # more in right?
+  var right/eax: (addr grapheme-stack) <- get self, right
+  var right-size/eax: int <- grapheme-stack-length right
+  var right-read-index/ecx: (addr int) <- get self, right-read-index
+  compare *right-read-index, right-size
+  {
+    break-if->=
+    return 0/false
+  }
+  #
+  return 1/true
+}
+
+fn peek-from-gap-buffer _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # more in left?
+  var left/ecx: (addr grapheme-stack) <- get self, left
+  var left-size/eax: int <- grapheme-stack-length left
+  var left-read-index-a/edx: (addr int) <- get self, left-read-index
+  compare *left-read-index-a, left-size
+  {
+    break-if->=
+    var left-data-ah/eax: (addr handle array grapheme) <- get left, data
+    var left-data/eax: (addr array grapheme) <- lookup *left-data-ah
+    var left-read-index/ecx: int <- copy *left-read-index-a
+    var result/eax: (addr grapheme) <- index left-data, left-read-index
+    return *result
+  }
+  # more in right?
+  var right/ecx: (addr grapheme-stack) <- get self, right
+  var _right-size/eax: int <- grapheme-stack-length right
+  var right-size/ebx: int <- copy _right-size
+  var right-read-index-a/edx: (addr int) <- get self, right-read-index
+  compare *right-read-index-a, right-size
+  {
+    break-if->=
+    # read the right from reverse
+    var right-data-ah/eax: (addr handle array grapheme) <- get right, data
+    var right-data/eax: (addr array grapheme) <- lookup *right-data-ah
+    var right-read-index/ebx: int <- copy right-size
+    right-read-index <- subtract *right-read-index-a
+    right-read-index <- subtract 1
+    var result/eax: (addr grapheme) <- index right-data, right-read-index
+    return *result
+  }
+  # if we get here there's nothing left
+  return 0/nul
+}
+
+fn read-from-gap-buffer _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # more in left?
+  var left/ecx: (addr grapheme-stack) <- get self, left
+  var left-size/eax: int <- grapheme-stack-length left
+  var left-read-index-a/edx: (addr int) <- get self, left-read-index
+  compare *left-read-index-a, left-size
+  {
+    break-if->=
+    var left-data-ah/eax: (addr handle array grapheme) <- get left, data
+    var left-data/eax: (addr array grapheme) <- lookup *left-data-ah
+    var left-read-index/ecx: int <- copy *left-read-index-a
+    var result/eax: (addr grapheme) <- index left-data, left-read-index
+    increment *left-read-index-a
+    return *result
+  }
+  # more in right?
+  var right/ecx: (addr grapheme-stack) <- get self, right
+  var _right-size/eax: int <- grapheme-stack-length right
+  var right-size/ebx: int <- copy _right-size
+  var right-read-index-a/edx: (addr int) <- get self, right-read-index
+  compare *right-read-index-a, right-size
+  {
+    break-if->=
+    # read the right from reverse
+    var right-data-ah/eax: (addr handle array grapheme) <- get right, data
+    var right-data/eax: (addr array grapheme) <- lookup *right-data-ah
+    var right-read-index/ebx: int <- copy right-size
+    right-read-index <- subtract *right-read-index-a
+    right-read-index <- subtract 1
+    var result/eax: (addr grapheme) <- index right-data, right-read-index
+    increment *right-read-index-a
+    return *result
+  }
+  # if we get here there's nothing left
+  return 0/nul
+}
+
+fn test-read-from-gap-buffer {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  # gap is at end, all contents are in left
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/left-1/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x61/a, "F - test-read-from-gap-buffer/left-1"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/left-2/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x62/b, "F - test-read-from-gap-buffer/left-2"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/left-3/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x63/c, "F - test-read-from-gap-buffer/left-3"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check done?, "F - test-read-from-gap-buffer/left-4/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0/nul, "F - test-read-from-gap-buffer/left-4"
+  # now check when everything is to the right
+  gap-to-start gap
+  rewind-gap-buffer gap
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/right-1/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x61/a, "F - test-read-from-gap-buffer/right-1"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/right-2/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x62/b, "F - test-read-from-gap-buffer/right-2"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/right-3/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x63/c, "F - test-read-from-gap-buffer/right-3"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check done?, "F - test-read-from-gap-buffer/right-4/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0/nul, "F - test-read-from-gap-buffer/right-4"
+}
+
+fn skip-whitespace-from-gap-buffer self: (addr gap-buffer) {
+  var done?/eax: boolean <- gap-buffer-scan-done? self
+  compare done?, 0/false
+  break-if-!=
+  var g/eax: grapheme <- peek-from-gap-buffer self
+  {
+    compare g, 0x20/space
+    break-if-=
+    compare g, 0xa/newline
+    break-if-=
+    return
+  }
+  g <- read-from-gap-buffer self
+  loop
+}
+
+fn edit-gap-buffer self: (addr gap-buffer), key: grapheme {
+  var g/edx: grapheme <- copy key
+  {
+    compare g, 8/backspace
+    break-if-!=
+    delete-before-gap self
+    return
+  }
+  # arrow keys
+  {
+    compare g, 4/ctrl-d
+    break-if-!=
+    # ctrl-d: cursor down
+    return
+  }
+  {
+    compare g, 0x15/ctrl-u
+    break-if-!=
+    # ctrl-u: cursor up
+    return
+  }
+  # default: insert character
+  add-grapheme-at-gap self, g
+}
+
+fn cursor-on-final-line? self: (addr gap-buffer) -> _/eax: boolean {
+  return 1/true
+}
diff --git a/shell/grapheme-stack.mu b/shell/grapheme-stack.mu
new file mode 100644
index 00000000..456df0cb
--- /dev/null
+++ b/shell/grapheme-stack.mu
@@ -0,0 +1,280 @@
+# grapheme stacks are the smallest unit of editable text
+
+type grapheme-stack {
+  data: (handle array grapheme)
+  top: int
+}
+
+fn initialize-grapheme-stack _self: (addr grapheme-stack), n: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var d/edi: (addr handle array grapheme) <- get self, data
+  populate d, n
+  var top/eax: (addr int) <- get self, top
+  copy-to *top, 0
+}
+
+fn clear-grapheme-stack _self: (addr grapheme-stack) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top/eax: (addr int) <- get self, top
+  copy-to *top, 0
+}
+
+fn grapheme-stack-empty? _self: (addr grapheme-stack) -> _/eax: boolean {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top/eax: (addr int) <- get self, top
+  compare *top, 0
+  {
+    break-if-!=
+    return 1/true
+  }
+  return 0/false
+}
+
+fn grapheme-stack-length _self: (addr grapheme-stack) -> _/eax: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top/eax: (addr int) <- get self, top
+  return *top
+}
+
+fn push-grapheme-stack _self: (addr grapheme-stack), _val: grapheme {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top-addr/ecx: (addr int) <- get self, top
+  var data-ah/edx: (addr handle array grapheme) <- get self, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var top/edx: int <- copy *top-addr
+  var dest-addr/edx: (addr grapheme) <- index data, top
+  var val/eax: grapheme <- copy _val
+  copy-to *dest-addr, val
+  add-to *top-addr, 1
+}
+
+fn pop-grapheme-stack _self: (addr grapheme-stack) -> _/eax: grapheme {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top-addr/ecx: (addr int) <- get self, top
+  {
+    compare *top-addr, 0
+    break-if->
+    return -1
+  }
+  subtract-from *top-addr, 1
+  var data-ah/edx: (addr handle array grapheme) <- get self, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var top/edx: int <- copy *top-addr
+  var result-addr/eax: (addr grapheme) <- index data, top
+  return *result-addr
+}
+
+fn copy-grapheme-stack _src: (addr grapheme-stack), dest: (addr grapheme-stack) {
+  var src/esi: (addr grapheme-stack) <- copy _src
+  var data-ah/edi: (addr handle array grapheme) <- get src, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get src, top
+  var i/eax: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index data, i
+    push-grapheme-stack dest, *g
+    i <- increment
+    loop
+  }
+}
+
+# dump stack to screen from bottom to top
+# colors hardcoded
+fn render-stack-from-bottom-wrapping-right-then-down screen: (addr screen), _self: (addr grapheme-stack), xmin: int, ymin: int, xmax: int, ymax: int, _x: int, _y: int -> _/eax: int, _/ecx: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var x/eax: int <- copy _x
+  var y/ecx: int <- copy _y
+  var top-addr/edx: (addr int) <- get self, top
+  var i/ebx: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    {
+      var g/edx: (addr grapheme) <- index data, i
+      x, y <- render-grapheme screen, *g, xmin, ymin, xmax, ymax, x, y, 3/fg=cyan, 0/bg
+    }
+    i <- increment
+    loop
+  }
+  return x, y
+}
+
+# helper for small words
+fn render-stack-from-bottom screen: (addr screen), self: (addr grapheme-stack), x: int, y: int -> _/eax: int {
+  var _width/eax: int <- copy 0
+  var _height/ecx: int <- copy 0
+  _width, _height <- screen-size screen
+  var width/edx: int <- copy _width
+  var height/ebx: int <- copy _height
+  var x2/eax: int <- copy 0
+  var y2/ecx: int <- copy 0
+  x2, y2 <- render-stack-from-bottom-wrapping-right-then-down screen, self, x, y, width, height, x, y
+  return x2  # y2? yolo
+}
+
+# dump stack to screen from top to bottom
+# optionally render a 'cursor' with the top grapheme
+fn render-stack-from-top-wrapping-right-then-down screen: (addr screen), _self: (addr grapheme-stack), xmin: int, ymin: int, xmax: int, ymax: int, _x: int, _y: int, render-cursor?: boolean -> _/eax: int, _/ecx: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var x/eax: int <- copy _x
+  var y/ecx: int <- copy _y
+  var top-addr/edx: (addr int) <- get self, top
+  var i/ebx: int <- copy *top-addr
+  i <- decrement
+  # if render-cursor?, peel off first iteration
+  {
+    compare render-cursor?, 0/false
+    break-if-=
+    compare i, 0
+    break-if-<
+    {
+      var g/edx: (addr grapheme) <- index data, i
+      x, y <- render-grapheme screen, *g, xmin, ymin, xmax, ymax, x, y, 3/fg=cyan, 7/bg=cursor
+    }
+    i <- decrement
+  }
+  # remaining iterations
+  {
+    compare i, 0
+    break-if-<
+    {
+      var g/edx: (addr grapheme) <- index data, i
+      x, y <- render-grapheme screen, *g, xmin, ymin, xmax, ymax, x, y, 3/fg=cyan, 0/bg=cursor
+    }
+    i <- decrement
+    loop
+  }
+  return x, y
+}
+
+# helper for small words
+fn render-stack-from-top screen: (addr screen), self: (addr grapheme-stack), x: int, y: int, render-cursor?: boolean -> _/eax: int {
+  var _width/eax: int <- copy 0
+  var _height/ecx: int <- copy 0
+  _width, _height <- screen-size screen
+  var width/edx: int <- copy _width
+  var height/ebx: int <- copy _height
+  var x2/eax: int <- copy 0
+  var y2/ecx: int <- copy 0
+  x2, y2 <- render-stack-from-top-wrapping-right-then-down screen, self, x, y, width, height, x, y, render-cursor?
+  return x2  # y2? yolo
+}
+
+fn test-render-grapheme-stack {
+  # setup: gs = "abc"
+  var gs-storage: grapheme-stack
+  var gs/edi: (addr grapheme-stack) <- address gs-storage
+  initialize-grapheme-stack gs, 5
+  var g/eax: grapheme <- copy 0x61/a
+  push-grapheme-stack gs, g
+  g <- copy 0x62/b
+  push-grapheme-stack gs, g
+  g <- copy 0x63/c
+  push-grapheme-stack gs, g
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4
+  #
+  var x/eax: int <- render-stack-from-bottom screen, gs, 0/x, 0/y
+  check-screen-row screen, 0/y, "abc ", "F - test-render-grapheme-stack from bottom"
+  check-ints-equal x, 3, "F - test-render-grapheme-stack from bottom: result"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "   ", "F - test-render-grapheme-stack from bottom: bg"
+  #
+  var x/eax: int <- render-stack-from-top screen, gs, 0/x, 1/y, 0/cursor=false
+  check-screen-row screen, 1/y, "cba ", "F - test-render-grapheme-stack from top without cursor"
+  check-ints-equal x, 3, "F - test-render-grapheme-stack from top without cursor: result"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "   ", "F - test-render-grapheme-stack from top without cursor: bg"
+  #
+  var x/eax: int <- render-stack-from-top screen, gs, 0/x, 2/y, 1/cursor=true
+  check-screen-row screen, 2/y, "cba ", "F - test-render-grapheme-stack from top with cursor"
+  check-ints-equal x, 3, "F - test-render-grapheme-stack from top without cursor: result"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "|   ", "F - test-render-grapheme-stack from top with cursor: bg"
+}
+
+# compare from bottom
+# beware: modifies 'stream', which must be disposed of after a false result
+fn prefix-match? _self: (addr grapheme-stack), s: (addr stream byte) -> _/eax: boolean {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/ebx: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    # if curr != expected, return false
+    {
+      var curr-a/edx: (addr grapheme) <- index data, i
+      var expected/eax: grapheme <- read-grapheme s
+      {
+        compare expected, *curr-a
+        break-if-=
+        return 0/false
+      }
+    }
+    i <- increment
+    loop
+  }
+  return 1   # true
+}
+
+# compare from bottom
+# beware: modifies 'stream', which must be disposed of after a false result
+fn suffix-match? _self: (addr grapheme-stack), s: (addr stream byte) -> _/eax: boolean {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/eax: (addr int) <- get self, top
+  var i/ebx: int <- copy *top-addr
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    {
+      var curr-a/edx: (addr grapheme) <- index data, i
+      var expected/eax: grapheme <- read-grapheme s
+      # if curr != expected, return false
+      {
+        compare expected, *curr-a
+        break-if-=
+        return 0/false
+      }
+    }
+    i <- decrement
+    loop
+  }
+  return 1   # true
+}
+
+fn grapheme-stack-is-decimal-integer? _self: (addr grapheme-stack) -> _/eax: boolean {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/eax: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edx: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/ebx: int <- copy 0
+  var result/eax: boolean <- copy 1/true
+  $grapheme-stack-is-integer?:loop: {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index data, i
+    result <- is-decimal-digit? *g
+    compare result, 0/false
+    break-if-=
+    i <- increment
+    loop
+  }
+  return result
+}
diff --git a/shell/main.mu b/shell/main.mu
new file mode 100644
index 00000000..eb437b67
--- /dev/null
+++ b/shell/main.mu
@@ -0,0 +1,22 @@
+# Experimental Mu shell
+# A Lisp with indent-sensitivity and infix, no macros. Commas are ignored.
+
+fn main {
+  var sandbox-storage: sandbox
+  var sandbox/esi: (addr sandbox) <- address sandbox-storage
+  initialize-sandbox sandbox
+  var width/eax: int <- copy 0
+  var height/ecx: int <- copy 0
+  width, height <- screen-size 0/screen
+  {
+    render-sandbox 0/screen, sandbox, 2/x, 2/y, width, height
+    {
+      var key/eax: byte <- read-key 0/keyboard
+      compare key, 0
+      loop-if-=
+      # no way to quit right now; just reboot
+      edit-sandbox sandbox, key
+    }
+    loop
+  }
+}
diff --git a/shell/parse.mu b/shell/parse.mu
new file mode 100644
index 00000000..a0045eb3
--- /dev/null
+++ b/shell/parse.mu
@@ -0,0 +1,136 @@
+fn parse-input tokens: (addr stream cell), out: (addr handle cell), trace: (addr trace) {
+  rewind-stream tokens
+  var empty?/eax: boolean <- stream-empty? tokens
+  compare empty?, 0/false
+  {
+    break-if-=
+    error trace, "nothing to parse"
+    return
+  }
+  var close-paren?/eax: boolean <- parse-sexpression tokens, out, trace
+  {
+    compare close-paren?, 0/false
+    break-if-=
+    error trace, "')' is not a valid expression"
+    return
+  }
+  {
+    var empty?/eax: boolean <- stream-empty? tokens
+    compare empty?, 0/false
+    break-if-!=
+    error trace, "unexpected tokens at end; only type in a single expression at a time"
+  }
+}
+
+# return value: true if close-paren was encountered
+fn parse-sexpression tokens: (addr stream cell), _out: (addr handle cell), trace: (addr trace) -> _/eax: boolean {
+  trace-text trace, "read", "parse"
+  trace-lower trace
+  var curr-token-storage: cell
+  var curr-token/ecx: (addr cell) <- address curr-token-storage
+  var empty?/eax: boolean <- stream-empty? tokens
+  compare empty?, 0/false
+  {
+    break-if-=
+    error trace, "end of stream; never found a balancing ')'"
+    return 1/true
+  }
+  read-from-stream tokens, curr-token
+  $parse-sexpression:type-check: {
+    # not bracket -> parse atom
+    var is-bracket-token?/eax: boolean <- is-bracket-token? curr-token
+    compare is-bracket-token?, 0/false
+    {
+      break-if-!=
+      parse-atom curr-token, _out, trace
+      break $parse-sexpression:type-check
+    }
+    # open paren -> parse list
+    var is-open-paren?/eax: boolean <- is-open-paren-token? curr-token
+    compare is-open-paren?, 0/false
+    {
+      break-if-=
+      var curr/esi: (addr handle cell) <- copy _out
+      $parse-sexpression:list-loop: {
+        allocate-pair curr
+        var curr-addr/eax: (addr cell) <- lookup *curr
+        var left/ecx: (addr handle cell) <- get curr-addr, left
+        {
+          var is-close-paren?/eax: boolean <- parse-sexpression tokens, left, trace
+          compare is-close-paren?, 0/false
+          break-if-!= $parse-sexpression:list-loop
+        }
+        #
+        curr <- get curr-addr, right
+        loop
+      }
+      break $parse-sexpression:type-check
+    }
+    # close paren -> parse list
+    var is-close-paren?/eax: boolean <- is-close-paren-token? curr-token
+    compare is-close-paren?, 0/false
+    {
+      break-if-=
+      trace-higher trace
+      return 1/true
+    }
+    # otherwise abort
+    var stream-storage: (stream byte 0x40)
+    var stream/edx: (addr stream byte) <- address stream-storage
+    write stream, "unexpected token "
+    var curr-token-data-ah/eax: (addr handle stream byte) <- get curr-token, text-data
+    var curr-token-data/eax: (addr stream byte) <- lookup *curr-token-data-ah
+    rewind-stream curr-token-data
+    write-stream stream, curr-token-data
+    trace trace, "error", stream
+  }
+  trace-higher trace
+  return 0/false
+}
+
+fn parse-atom _curr-token: (addr cell), _out: (addr handle cell), trace: (addr trace) {
+  trace-text trace, "read", "parse atom"
+  var curr-token/ecx: (addr cell) <- copy _curr-token
+  var curr-token-data-ah/eax: (addr handle stream byte) <- get curr-token, text-data
+  var _curr-token-data/eax: (addr stream byte) <- lookup *curr-token-data-ah
+  var curr-token-data/esi: (addr stream byte) <- copy _curr-token-data
+  trace trace, "read", curr-token-data
+  # number
+  var is-number-token?/eax: boolean <- is-number-token? curr-token
+  compare is-number-token?, 0/false
+  {
+    break-if-=
+    rewind-stream curr-token-data
+    var _val/eax: int <- parse-decimal-int-from-stream curr-token-data
+    var val/ecx: int <- copy _val
+    var val-float/xmm0: float <- convert val
+    allocate-number _out
+    var out/eax: (addr handle cell) <- copy _out
+    var out-addr/eax: (addr cell) <- lookup *out
+    var dest/edi: (addr float) <- get out-addr, number-data
+    copy-to *dest, val-float
+    {
+      var stream-storage: (stream byte 0x40)
+      var stream/ecx: (addr stream byte) <- address stream-storage
+      write stream, "=> number "
+      print-number out-addr, stream, 0/no-trace
+      trace trace, "read", stream
+    }
+    return
+  }
+  # default: symbol
+  # just copy token data
+  allocate-symbol _out
+  var out/eax: (addr handle cell) <- copy _out
+  var out-addr/eax: (addr cell) <- lookup *out
+  var curr-token-data-ah/ecx: (addr handle stream byte) <- get curr-token, text-data
+  var dest-ah/edx: (addr handle stream byte) <- get out-addr, text-data
+  copy-object curr-token-data-ah, dest-ah
+  {
+    var stream-storage: (stream byte 0x40)
+    var stream/ecx: (addr stream byte) <- address stream-storage
+    write stream, "=> symbol "
+    print-symbol out-addr, stream, 0/no-trace
+    trace trace, "read", stream
+  }
+}
diff --git a/shell/print.mu b/shell/print.mu
new file mode 100644
index 00000000..32f5e725
--- /dev/null
+++ b/shell/print.mu
@@ -0,0 +1,260 @@
+fn print-cell _in: (addr handle cell), out: (addr stream byte), trace: (addr trace) {
+  trace-text trace, "print", "print-cell"
+  trace-lower trace
+  var in/eax: (addr handle cell) <- copy _in
+  var in-addr/eax: (addr cell) <- lookup *in
+  {
+    var is-nil?/eax: boolean <- is-nil? in-addr
+    compare is-nil?, 0/false
+    break-if-=
+    write out, "()"
+    trace-higher trace
+    return
+  }
+  var in-type/ecx: (addr int) <- get in-addr, type
+  compare *in-type, 0/pair
+  {
+    break-if-!=
+    print-list in-addr, out, trace
+    trace-higher trace
+    return
+  }
+  compare *in-type, 1/number
+  {
+    break-if-!=
+    print-number in-addr, out, trace
+    trace-higher trace
+    return
+  }
+  compare *in-type, 2/symbol
+  {
+    break-if-!=
+    print-symbol in-addr, out, trace
+    trace-higher trace
+    return
+  }
+}
+
+fn print-symbol _in: (addr cell), out: (addr stream byte), trace: (addr trace) {
+  trace-text trace, "print", "symbol"
+  var in/esi: (addr cell) <- copy _in
+  var data-ah/eax: (addr handle stream byte) <- get in, text-data
+  var _data/eax: (addr stream byte) <- lookup *data-ah
+  var data/esi: (addr stream byte) <- copy _data
+  rewind-stream data
+  write-stream out, data
+  # trace
+  rewind-stream data
+  var stream-storage: (stream byte 0x40)
+  var stream/ecx: (addr stream byte) <- address stream-storage
+  write stream, "=> symbol "
+  write-stream stream, data
+  trace trace, "print", stream
+}
+
+fn print-number _in: (addr cell), out: (addr stream byte), trace: (addr trace) {
+  var in/esi: (addr cell) <- copy _in
+  var val/eax: (addr float) <- get in, number-data
+  write-float-decimal-approximate out, *val, 3/precision
+  # trace
+  var stream-storage: (stream byte 0x40)
+  var stream/ecx: (addr stream byte) <- address stream-storage
+  write stream, "=> number "
+  write-float-decimal-approximate stream, *val, 3/precision
+  trace trace, "print", stream
+}
+
+fn print-list _in: (addr cell), out: (addr stream byte), trace: (addr trace) {
+  var curr/esi: (addr cell) <- copy _in
+  write out, "("
+  $print-list:loop: {
+    var left/ecx: (addr handle cell) <- get curr, left
+    {
+      var left-addr/eax: (addr cell) <- lookup *left
+      var left-is-nil?/eax: boolean <- is-nil? left-addr
+      compare left-is-nil?, 0/false
+      {
+        break-if-=
+        trace-text trace, "print", "left is null"
+        break $print-list:loop
+      }
+    }
+    print-cell left, out, trace
+    var right/ecx: (addr handle cell) <- get curr, right
+    var right-addr/eax: (addr cell) <- lookup *right
+    {
+      compare right-addr, 0
+      break-if-!=
+      abort "null encountered"
+    }
+    {
+      var right-is-nil?/eax: boolean <- is-nil? right-addr
+      compare right-is-nil?, 0/false
+      {
+        break-if-=
+        trace-text trace, "print", "right is null"
+        break $print-list:loop
+      }
+    }
+    write out, " "
+    var right-type-addr/edx: (addr int) <- get right-addr, type
+    {
+      compare *right-type-addr, 0/pair
+      break-if-=
+      write out, ". "
+      print-cell right, out, trace
+      break $print-list:loop
+    }
+    curr <- copy right-addr
+    loop
+  }
+  write out, ")"
+}
+
+# Most lisps intern nil, but we don't really have globals yet, so we'll be
+# less efficient for now.
+fn is-nil? _in: (addr cell) -> _/eax: boolean {
+  var in/esi: (addr cell) <- copy _in
+  # if type != pair, return false
+  var type/eax: (addr int) <- get in, type
+  compare *type, 0/pair
+  {
+    break-if-=
+    return 0/false
+  }
+  # if left != null, return false
+  var left-ah/eax: (addr handle cell) <- get in, left
+  var left/eax: (addr cell) <- lookup *left-ah
+  compare left, 0
+  {
+    break-if-=
+    return 0/false
+  }
+  # if right != null, return false
+  var right-ah/eax: (addr handle cell) <- get in, right
+  var right/eax: (addr cell) <- lookup *right-ah
+  compare right, 0
+  {
+    break-if-=
+    return 0/false
+  }
+  return 1/true
+}
+
+fn test-print-cell-zero {
+  var num-storage: (handle cell)
+  var num/esi: (addr handle cell) <- address num-storage
+  new-integer num, 0
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell num, out, 0/no-trace
+  check-stream-equal out, "0", "F - test-print-cell-zero"
+}
+
+fn test-print-cell-integer {
+  var num-storage: (handle cell)
+  var num/esi: (addr handle cell) <- address num-storage
+  new-integer num, 1
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell num, out, 0/no-trace
+  check-stream-equal out, "1", "F - test-print-cell-integer"
+}
+
+fn test-print-cell-integer-2 {
+  var num-storage: (handle cell)
+  var num/esi: (addr handle cell) <- address num-storage
+  new-integer num, 0x30
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell num, out, 0/no-trace
+  check-stream-equal out, "48", "F - test-print-cell-integer-2"
+}
+
+fn test-print-cell-fraction {
+  var num-storage: (handle cell)
+  var num/esi: (addr handle cell) <- address num-storage
+  var val/xmm0: float <- rational 1, 2
+  new-float num, val
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell num, out, 0/no-trace
+  check-stream-equal out, "0.5", "F - test-print-cell-fraction"
+}
+
+fn test-print-cell-symbol {
+  var sym-storage: (handle cell)
+  var sym/esi: (addr handle cell) <- address sym-storage
+  new-symbol sym, "abc"
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell sym, out, 0/no-trace
+  check-stream-equal out, "abc", "F - test-print-cell-symbol"
+}
+
+fn test-print-cell-nil-list {
+  var nil-storage: (handle cell)
+  var nil/esi: (addr handle cell) <- address nil-storage
+  allocate-pair nil
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell nil, out, 0/no-trace
+  check-stream-equal out, "()", "F - test-print-cell-nil-list"
+}
+
+fn test-print-cell-singleton-list {
+  # list
+  var left-storage: (handle cell)
+  var left/ecx: (addr handle cell) <- address left-storage
+  new-symbol left, "abc"
+  var nil-storage: (handle cell)
+  var nil/edx: (addr handle cell) <- address nil-storage
+  allocate-pair nil
+  var list-storage: (handle cell)
+  var list/esi: (addr handle cell) <- address list-storage
+  new-pair list, *left, *nil
+  #
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell list, out, 0/no-trace
+  check-stream-equal out, "(abc)", "F - test-print-cell-singleton-list"
+}
+
+fn test-print-cell-list {
+  # list = cons "abc", nil
+  var left-storage: (handle cell)
+  var left/ecx: (addr handle cell) <- address left-storage
+  new-symbol left, "abc"
+  var nil-storage: (handle cell)
+  var nil/edx: (addr handle cell) <- address nil-storage
+  allocate-pair nil
+  var list-storage: (handle cell)
+  var list/esi: (addr handle cell) <- address list-storage
+  new-pair list, *left, *nil
+  # list = cons 64, list
+  new-integer left, 0x40
+  new-pair list, *left, *list
+  #
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell list, out, 0/no-trace
+  check-stream-equal out, "(64 abc)", "F - test-print-cell-list"
+}
+
+fn test-print-dotted-list {
+  # list = cons 64, "abc"
+  var left-storage: (handle cell)
+  var left/ecx: (addr handle cell) <- address left-storage
+  new-symbol left, "abc"
+  var right-storage: (handle cell)
+  var right/edx: (addr handle cell) <- address right-storage
+  new-integer right, 0x40
+  var list-storage: (handle cell)
+  var list/esi: (addr handle cell) <- address list-storage
+  new-pair list, *left, *right
+  #
+  var out-storage: (stream byte 0x40)
+  var out/edi: (addr stream byte) <- address out-storage
+  print-cell list, out, 0/no-trace
+  check-stream-equal out, "(abc . 64)", "F - test-print-dotted-list"
+}
diff --git a/shell/read.mu b/shell/read.mu
new file mode 100644
index 00000000..d3e1dc86
--- /dev/null
+++ b/shell/read.mu
@@ -0,0 +1,15 @@
+# out is not allocated
+fn read-cell in: (addr gap-buffer), out: (addr handle cell), trace: (addr trace) {
+  var tokens-storage: (stream cell 0x100)
+  var tokens/ecx: (addr stream cell) <- address tokens-storage
+  tokenize in, tokens, trace
+  var error?/eax: boolean <- has-errors? trace
+  compare error?, 0/false
+  {
+    break-if-=
+    return
+  }
+  # TODO: insert parens
+  # TODO: transform infix
+  parse-input tokens, out, trace
+}
diff --git a/shell/sandbox.mu b/shell/sandbox.mu
new file mode 100644
index 00000000..49c2a5f9
--- /dev/null
+++ b/shell/sandbox.mu
@@ -0,0 +1,263 @@
+type sandbox {
+  data: (handle gap-buffer)
+  value: (handle stream byte)
+  trace: (handle trace)
+  cursor-in-trace?: boolean
+}
+
+fn initialize-sandbox _self: (addr sandbox) {
+  var self/esi: (addr sandbox) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, data
+  allocate data-ah
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  initialize-gap-buffer data, 0x1000/4KB
+  var value-ah/eax: (addr handle stream byte) <- get self, value
+  populate-stream value-ah, 0x1000/4KB
+  var trace-ah/eax: (addr handle trace) <- get self, trace
+  allocate trace-ah
+  var trace/eax: (addr trace) <- lookup *trace-ah
+  initialize-trace trace, 0x1000/lines, 0x80/visible-lines
+}
+
+## some helpers for tests
+
+fn initialize-sandbox-with _self: (addr sandbox), s: (addr array byte) {
+  var self/esi: (addr sandbox) <- copy _self
+  var data-ah/eax: (addr handle gap-buffer) <- get self, data
+  allocate data-ah
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  initialize-gap-buffer-with data, s
+}
+
+fn allocate-sandbox-with _out: (addr handle sandbox), s: (addr array byte) {
+  var out/eax: (addr handle sandbox) <- copy _out
+  allocate out
+  var out-addr/eax: (addr sandbox) <- lookup *out
+  initialize-sandbox-with out-addr, s
+}
+
+##
+
+fn render-sandbox screen: (addr screen), _self: (addr sandbox), xmin: int, ymin: int, xmax: int, ymax: int {
+  clear-screen screen
+  var self/esi: (addr sandbox) <- copy _self
+  # data
+  var data-ah/eax: (addr handle gap-buffer) <- get self, data
+  var _data/eax: (addr gap-buffer) <- lookup *data-ah
+  var data/edx: (addr gap-buffer) <- copy _data
+  var x/eax: int <- copy xmin
+  var y/ecx: int <- copy ymin
+  var cursor-in-sandbox?/ebx: boolean <- copy 0/false
+  {
+    var cursor-in-trace?/eax: (addr boolean) <- get self, cursor-in-trace?
+    compare *cursor-in-trace?, 0/false
+    break-if-!=
+    cursor-in-sandbox? <- copy 1/true
+  }
+  x, y <- render-gap-buffer-wrapping-right-then-down screen, data, x, y, xmax, ymax, cursor-in-sandbox?
+  y <- increment
+  # trace
+  var trace-ah/eax: (addr handle trace) <- get self, trace
+  var _trace/eax: (addr trace) <- lookup *trace-ah
+  var trace/edx: (addr trace) <- copy _trace
+  var cursor-in-trace?/eax: (addr boolean) <- get self, cursor-in-trace?
+  y <- render-trace screen, trace, xmin, y, xmax, ymax, *cursor-in-trace?
+  # value
+  $render-sandbox:value: {
+    var value-ah/eax: (addr handle stream byte) <- get self, value
+    var _value/eax: (addr stream byte) <- lookup *value-ah
+    var value/esi: (addr stream byte) <- copy _value
+    rewind-stream value
+    var done?/eax: boolean <- stream-empty? value
+    compare done?, 0/false
+    break-if-!=
+    var x/eax: int <- copy 0
+    x, y <- draw-text-wrapping-right-then-down screen, "=> ", xmin, y, xmax, ymax, xmin, y, 7/fg, 0/bg
+    var x2/edx: int <- copy x
+    var dummy/eax: int <- draw-stream-rightward screen, value, x2, xmax, y, 7/fg=grey, 0/bg
+  }
+  # render menu
+  var cursor-in-trace?/eax: (addr boolean) <- get self, cursor-in-trace?
+  compare *cursor-in-trace?, 0/false
+  {
+    break-if-=
+    render-trace-menu screen
+    return
+  }
+  render-sandbox-menu screen
+}
+
+fn render-sandbox-menu screen: (addr screen) {
+  var width/eax: int <- copy 0
+  var height/ecx: int <- copy 0
+  width, height <- screen-size screen
+  var y/ecx: int <- copy height
+  y <- decrement
+  set-cursor-position screen, 0/x, y
+  draw-text-rightward-from-cursor screen, " ctrl-s ", width, 0/fg, 7/bg=grey
+  draw-text-rightward-from-cursor screen, " run sandbox  ", width, 7/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " ctrl-d ", width, 0/fg, 7/bg=grey
+  draw-text-rightward-from-cursor screen, " cursor down  ", width, 7/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " ctrl-u ", width, 0/fg, 7/bg=grey
+  draw-text-rightward-from-cursor screen, " cursor up  ", width, 7/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " tab ", width, 0/fg, 9/bg=blue
+  draw-text-rightward-from-cursor screen, " move to trace  ", width, 7/fg, 0/bg
+}
+
+fn edit-sandbox _self: (addr sandbox), key: byte {
+  var self/esi: (addr sandbox) <- copy _self
+  var g/edx: grapheme <- copy key
+  # running code
+  {
+    compare g, 0x12/ctrl-r
+    break-if-!=
+    # ctrl-r: run function outside sandbox
+    # required: fn (addr screen), (addr keyboard)
+    # Mu will pass in the real screen and keyboard.
+    return
+  }
+  {
+    compare g, 0x13/ctrl-s
+    break-if-!=
+    # ctrl-s: run sandbox(es)
+    var data-ah/eax: (addr handle gap-buffer) <- get self, data
+    var _data/eax: (addr gap-buffer) <- lookup *data-ah
+    var data/ecx: (addr gap-buffer) <- copy _data
+    var value-ah/eax: (addr handle stream byte) <- get self, value
+    var _value/eax: (addr stream byte) <- lookup *value-ah
+    var value/edx: (addr stream byte) <- copy _value
+    var trace-ah/eax: (addr handle trace) <- get self, trace
+    var trace/eax: (addr trace) <- lookup *trace-ah
+    clear-trace trace
+    run data, value, trace
+    return
+  }
+  # tab
+  var cursor-in-trace?/eax: (addr boolean) <- get self, cursor-in-trace?
+  {
+    compare g, 9/tab
+    break-if-!=
+    # if cursor in input, switch to trace
+    {
+      compare *cursor-in-trace?, 0/false
+      break-if-!=
+      copy-to *cursor-in-trace?, 1/true
+      return
+    }
+    # if cursor in trace, switch to input
+    copy-to *cursor-in-trace?, 0/false
+    return
+  }
+  # if cursor in trace, send cursor to trace
+  {
+    compare *cursor-in-trace?, 0/false
+    break-if-=
+    var trace-ah/eax: (addr handle trace) <- get self, trace
+    var trace/eax: (addr trace) <- lookup *trace-ah
+    edit-trace trace, g
+    return
+  }
+  # otherwise send cursor to input
+  var data-ah/eax: (addr handle gap-buffer) <- get self, data
+  var data/eax: (addr gap-buffer) <- lookup *data-ah
+  edit-gap-buffer data, g
+  return
+}
+
+fn run in: (addr gap-buffer), out: (addr stream byte), trace: (addr trace) {
+  var read-result-storage: (handle cell)
+  var read-result/esi: (addr handle cell) <- address read-result-storage
+  read-cell in, read-result, trace
+  var error?/eax: boolean <- has-errors? trace
+  {
+    compare error?, 0/false
+    break-if-=
+    return
+  }
+  # TODO: eval
+  clear-stream out
+  print-cell read-result, out, trace
+  mark-lines-dirty trace
+}
+
+fn test-run-integer {
+  var sandbox-storage: sandbox
+  var sandbox/esi: (addr sandbox) <- address sandbox-storage
+  initialize-sandbox sandbox
+  # type "1"
+  edit-sandbox sandbox, 0x31/1
+  # eval
+  edit-sandbox sandbox, 0x13/ctrl-s
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x80/width, 0x10/height
+  #
+  render-sandbox screen, sandbox, 0/x, 0/y, 0x80/width, 0x10/height
+  check-screen-row screen, 0/y, "1    ", "F - test-run-integer/0"
+  check-screen-row screen, 1/y, "...  ", "F - test-run-integer/1"
+  check-screen-row screen, 2/y, "=> 1 ", "F - test-run-integer/2"
+}
+
+fn test-run-error-invalid-integer {
+  var sandbox-storage: sandbox
+  var sandbox/esi: (addr sandbox) <- address sandbox-storage
+  initialize-sandbox sandbox
+  # type "1a"
+  edit-sandbox sandbox, 0x31/1
+  edit-sandbox sandbox, 0x61/a
+  # eval
+  edit-sandbox sandbox, 0x13/ctrl-s
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x80/width, 0x10/height
+  #
+  render-sandbox screen, sandbox, 0/x, 0/y, 0x80/width, 0x10/height
+  check-screen-row screen, 0/y, "1a             ", "F - test-run-error-invalid-integer/0"
+  check-screen-row screen, 1/y, "...            ", "F - test-run-error-invalid-integer/0"
+  check-screen-row screen, 2/y, "invalid number ", "F - test-run-error-invalid-integer/2"
+}
+
+fn test-run-move-cursor-into-trace {
+  var sandbox-storage: sandbox
+  var sandbox/esi: (addr sandbox) <- address sandbox-storage
+  initialize-sandbox sandbox
+  # type "12"
+  edit-sandbox sandbox, 0x31/1
+  edit-sandbox sandbox, 0x32/2
+  # eval
+  edit-sandbox sandbox, 0x13/ctrl-s
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x80/width, 0x10/height
+  #
+  render-sandbox screen, sandbox, 0/x, 0/y, 0x80/width, 0x10/height
+  check-screen-row screen,                                  0/y, "12    ", "F - test-run-move-cursor-into-trace/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "  |   ", "F - test-run-move-cursor-into-trace/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "...   ", "F - test-run-move-cursor-into-trace/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "      ", "F - test-run-move-cursor-into-trace/pre-1/cursor"
+  check-screen-row screen,                                  2/y, "=> 12 ", "F - test-run-move-cursor-into-trace/pre-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "      ", "F - test-run-move-cursor-into-trace/pre-2/cursor"
+  # move cursor into trace
+  edit-sandbox sandbox, 9/tab
+  #
+  render-sandbox screen, sandbox, 0/x, 0/y, 0x80/width, 0x10/height
+  check-screen-row screen,                                  0/y, "12    ", "F - test-run-move-cursor-into-trace/trace-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "      ", "F - test-run-move-cursor-into-trace/trace-0/cursor"
+  check-screen-row screen,                                  1/y, "...   ", "F - test-run-move-cursor-into-trace/trace-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "|||   ", "F - test-run-move-cursor-into-trace/trace-1/cursor"
+  check-screen-row screen,                                  2/y, "=> 12 ", "F - test-run-move-cursor-into-trace/trace-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "      ", "F - test-run-move-cursor-into-trace/trace-2/cursor"
+  # move cursor into input
+  edit-sandbox sandbox, 9/tab
+  #
+  render-sandbox screen, sandbox, 0/x, 0/y, 0x80/width, 0x10/height
+  check-screen-row screen,                                  0/y, "12    ", "F - test-run-move-cursor-into-trace/input-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "  |   ", "F - test-run-move-cursor-into-trace/input-0/cursor"
+  check-screen-row screen,                                  1/y, "...   ", "F - test-run-move-cursor-into-trace/input-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "      ", "F - test-run-move-cursor-into-trace/input-1/cursor"
+  check-screen-row screen,                                  2/y, "=> 12 ", "F - test-run-move-cursor-into-trace/input-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "      ", "F - test-run-move-cursor-into-trace/input-2/cursor"
+}
diff --git a/shell/tokenize.mu b/shell/tokenize.mu
new file mode 100644
index 00000000..7beedf23
--- /dev/null
+++ b/shell/tokenize.mu
@@ -0,0 +1,422 @@
+# We reuse the cell data structure for tokenization
+# Token cells are special, though. They have no type, they're always atoms,
+# they always have text-data.
+
+fn tokenize in: (addr gap-buffer), out: (addr stream cell), trace: (addr trace) {
+  trace-text trace, "read", "tokenize"
+  trace-lower trace
+  rewind-gap-buffer in
+  var token-storage: cell
+  var token/edx: (addr cell) <- address token-storage
+  {
+    var done?/eax: boolean <- gap-buffer-scan-done? in
+    compare done?, 0/false
+    break-if-!=
+    # initialize token data each iteration to avoid aliasing
+    var dest-ah/eax: (addr handle stream byte) <- get token, text-data
+    populate-stream dest-ah, 0x40/max-token-size
+    #
+    next-token in, token, trace
+    var error?/eax: boolean <- has-errors? trace
+    compare error?, 0/false
+    {
+      break-if-=
+      return
+    }
+    write-to-stream out, token  # shallow-copy text-data
+    loop
+  }
+  trace-higher trace
+}
+
+fn next-token in: (addr gap-buffer), _out-cell: (addr cell), trace: (addr trace) {
+  trace-text trace, "read", "next-token"
+  trace-lower trace
+  var out-cell/eax: (addr cell) <- copy _out-cell
+  var out-ah/eax: (addr handle stream byte) <- get out-cell, text-data
+  var _out/eax: (addr stream byte) <- lookup *out-ah
+  var out/edi: (addr stream byte) <- copy _out
+  $next-token:body: {
+    clear-stream out
+    skip-whitespace-from-gap-buffer in
+    var g/eax: grapheme <- peek-from-gap-buffer in
+    {
+      var stream-storage: (stream byte 0x40)
+      var stream/esi: (addr stream byte) <- address stream-storage
+      write stream, "next: "
+      var gval/eax: int <- copy g
+      write-int32-hex stream, gval
+      trace trace, "read", stream
+    }
+    # digit
+    {
+      var digit?/eax: boolean <- is-decimal-digit? g
+      compare digit?, 0/false
+      break-if-=
+      next-number-token in, out, trace
+      break $next-token:body
+    }
+    # other symbol char
+    {
+      var symbol?/eax: boolean <- is-symbol-grapheme? g
+      compare symbol?, 0/false
+      break-if-=
+      next-symbol-token in, out, trace
+      break $next-token:body
+    }
+    # brackets are always single-char tokens
+    {
+      var bracket?/eax: boolean <- is-bracket-grapheme? g
+      compare bracket?, 0/false
+      break-if-=
+      var g/eax: grapheme <- read-from-gap-buffer in
+      next-bracket-token g, out, trace
+      break $next-token:body
+    }
+  }
+  trace-higher trace
+  var stream-storage: (stream byte 0x40)
+  var stream/eax: (addr stream byte) <- address stream-storage
+  write stream, "=> "
+  rewind-stream out
+  write-stream stream, out
+  trace trace, "read", stream
+}
+
+fn next-symbol-token in: (addr gap-buffer), out: (addr stream byte), trace: (addr trace) {
+  trace-text trace, "read", "looking for a symbol"
+  trace-lower trace
+  $next-symbol-token:loop: {
+    var done?/eax: boolean <- gap-buffer-scan-done? in
+    compare done?, 0/false
+    break-if-!=
+    var g/eax: grapheme <- peek-from-gap-buffer in
+    {
+      var stream-storage: (stream byte 0x40)
+      var stream/esi: (addr stream byte) <- address stream-storage
+      write stream, "next: "
+      var gval/eax: int <- copy g
+      write-int32-hex stream, gval
+      trace trace, "read", stream
+    }
+    # if non-symbol, return
+    {
+      var symbol-grapheme?/eax: boolean <- is-symbol-grapheme? g
+      compare symbol-grapheme?, 0/false
+      break-if-!=
+      trace-text trace, "read", "stop"
+      break $next-symbol-token:loop
+    }
+    var g/eax: grapheme <- read-from-gap-buffer in
+    write-grapheme out, g
+    loop
+  }
+  trace-higher trace
+  var stream-storage: (stream byte 0x40)
+  var stream/esi: (addr stream byte) <- address stream-storage
+  write stream, "=> "
+  rewind-stream out
+  write-stream stream, out
+  trace trace, "read", stream
+}
+
+fn next-number-token in: (addr gap-buffer), out: (addr stream byte), trace: (addr trace) {
+  trace-text trace, "read", "looking for a number"
+  trace-lower trace
+  $next-number-token:loop: {
+    var done?/eax: boolean <- gap-buffer-scan-done? in
+    compare done?, 0/false
+    break-if-!=
+    var g/eax: grapheme <- peek-from-gap-buffer in
+    {
+      var stream-storage: (stream byte 0x40)
+      var stream/esi: (addr stream byte) <- address stream-storage
+      write stream, "next: "
+      var gval/eax: int <- copy g
+      write-int32-hex stream, gval
+      trace trace, "read", stream
+    }
+    # if not symbol grapheme, return
+    {
+      var symbol-grapheme?/eax: boolean <- is-symbol-grapheme? g
+      compare symbol-grapheme?, 0/false
+      break-if-!=
+      trace-text trace, "read", "stop"
+      break $next-number-token:loop
+    }
+    # if not digit grapheme, abort
+    {
+      var digit?/eax: boolean <- is-decimal-digit? g
+      compare digit?, 0/false
+      break-if-!=
+      error trace, "invalid number"
+      return
+    }
+    trace-text trace, "read", "append"
+    var g/eax: grapheme <- read-from-gap-buffer in
+    write-grapheme out, g
+    loop
+  }
+  trace-higher trace
+}
+
+fn next-bracket-token g: grapheme, out: (addr stream byte), trace: (addr trace) {
+  trace-text trace, "read", "bracket"
+  write-grapheme out, g
+  var stream-storage: (stream byte 0x40)
+  var stream/esi: (addr stream byte) <- address stream-storage
+  write stream, "=> "
+  rewind-stream out
+  write-stream stream, out
+  trace trace, "read", stream
+}
+
+fn is-symbol-grapheme? g: grapheme -> _/eax: boolean {
+  ## whitespace
+  compare g, 9/tab
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0xa/newline
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x20/space
+  {
+    break-if-!=
+    return 0/false
+  }
+  ## quotes
+  compare g, 0x22/double-quote
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x27/single-quote
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x60/backquote
+  {
+    break-if-!=
+    return 0/false
+  }
+  ## brackets
+  compare g, 0x28/open-paren
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x29/close-paren
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x5b/open-square-bracket
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x5d/close-square-bracket
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x7b/open-curly-bracket
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x7d/close-curly-bracket
+  {
+    break-if-!=
+    return 0/false
+  }
+  # - other punctuation
+  # '!' is a symbol char
+  compare g, 0x23/hash
+  {
+    break-if-!=
+    return 0/false
+  }
+  # '$' is a symbol char
+  compare g, 0x25/percent
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x26/ampersand
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x2a/asterisk
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x2b/plus
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x2c/comma
+  {
+    break-if-!=
+    return 0/false
+  }
+  # '-' is a symbol char
+  compare g, 0x2e/period
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x2f/slash
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x2f/slash
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x3a/colon
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x3b/semi-colon
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x3c/less-than
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x3d/equal
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x3e/greater-than
+  {
+    break-if-!=
+    return 0/false
+  }
+  # '?' is a symbol char
+  compare g, 0x40/at-sign
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x5c/backslash
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x5e/caret
+  {
+    break-if-!=
+    return 0/false
+  }
+  # '_' is a symbol char
+  compare g, 0x7c/vertical-line
+  {
+    break-if-!=
+    return 0/false
+  }
+  compare g, 0x7e/tilde
+  {
+    break-if-!=
+    return 0/false
+  }
+  return 1/true
+}
+
+fn is-bracket-grapheme? g: grapheme -> _/eax: boolean {
+  compare g, 0x28/open-paren
+  {
+    break-if-!=
+    return 1/true
+  }
+  compare g, 0x29/close-paren
+  {
+    break-if-!=
+    return 1/true
+  }
+  compare g, 0x5b/open-square-bracket
+  {
+    break-if-!=
+    return 1/true
+  }
+  compare g, 0x5d/close-square-bracket
+  {
+    break-if-!=
+    return 1/true
+  }
+  compare g, 0x7b/open-curly-bracket
+  {
+    break-if-!=
+    return 1/true
+  }
+  compare g, 0x7d/close-curly-bracket
+  {
+    break-if-!=
+    return 1/true
+  }
+  return 0/false
+}
+
+fn is-number-token? _in: (addr cell) -> _/eax: boolean {
+  var in/eax: (addr cell) <- copy _in
+  var in-data-ah/eax: (addr handle stream byte) <- get in, text-data
+  var in-data/eax: (addr stream byte) <- lookup *in-data-ah
+  rewind-stream in-data
+  var g/eax: grapheme <- read-grapheme in-data
+  var result/eax: boolean <- is-decimal-digit? g
+  return result
+}
+
+fn is-bracket-token? _in: (addr cell) -> _/eax: boolean {
+  var in/eax: (addr cell) <- copy _in
+  var in-data-ah/eax: (addr handle stream byte) <- get in, text-data
+  var in-data/eax: (addr stream byte) <- lookup *in-data-ah
+  rewind-stream in-data
+  var g/eax: grapheme <- read-grapheme in-data
+  var result/eax: boolean <- is-bracket-grapheme? g
+  return result
+}
+
+fn is-open-paren-token? _in: (addr cell) -> _/eax: boolean {
+  var in/eax: (addr cell) <- copy _in
+  var in-data-ah/eax: (addr handle stream byte) <- get in, text-data
+  var in-data/eax: (addr stream byte) <- lookup *in-data-ah
+  rewind-stream in-data
+  var g/eax: grapheme <- read-grapheme in-data
+  compare g, 0x28/open-paren
+  {
+    break-if-!=
+    return 1/true
+  }
+  return 0/false
+}
+
+fn is-close-paren-token? _in: (addr cell) -> _/eax: boolean {
+  var in/eax: (addr cell) <- copy _in
+  var in-data-ah/eax: (addr handle stream byte) <- get in, text-data
+  var in-data/eax: (addr stream byte) <- lookup *in-data-ah
+  rewind-stream in-data
+  var g/eax: grapheme <- read-grapheme in-data
+  compare g, 0x29/open-paren
+  {
+    break-if-!=
+    return 1/true
+  }
+  return 0/false
+}
diff --git a/shell/trace.mu b/shell/trace.mu
new file mode 100644
index 00000000..7fd52ceb
--- /dev/null
+++ b/shell/trace.mu
@@ -0,0 +1,1449 @@
+# A trace records the evolution of a computation.
+# An integral part of the Mu Shell is facilities for browsing traces.
+
+type trace {
+  # steady-state life cycle of a trace:
+  #   reload loop:
+  #     there are already some visible lines
+  #     append a bunch of new trace lines to the trace
+  #     render loop:
+  #       rendering displays trace lines that match visible lines
+  #       rendering computes cursor-line based on the cursor-y coordinate
+  #       edit-trace updates cursor-y coordinate
+  #       edit-trace might add/remove lines to visible
+  curr-depth: int  # depth that will be assigned to next line appended
+  data: (handle array trace-line)
+  first-free: int
+  visible: (handle array trace-line)
+  recompute-visible?: boolean
+  top-line-index: int  # index into data
+  cursor-y: int  # row index on screen
+  cursor-line-index: int  # index into data
+}
+
+type trace-line {
+  depth: int
+  label: (handle array byte)
+  data: (handle array byte)
+  visible?: boolean
+}
+
+fn initialize-trace _self: (addr trace), capacity: int, visible-capacity: int {
+  var self/esi: (addr trace) <- copy _self
+  compare self, 0
+  break-if-=
+  var trace-ah/eax: (addr handle array trace-line) <- get self, data
+  populate trace-ah, capacity
+  var visible-ah/eax: (addr handle array trace-line) <- get self, visible
+  populate visible-ah, visible-capacity
+}
+
+fn clear-trace _self: (addr trace) {
+  var self/eax: (addr trace) <- copy _self
+  compare self, 0
+  break-if-=
+  var len/edx: (addr int) <- get self, first-free
+  copy-to *len, 0
+  # might leak memory; existing elements won't be used anymore
+}
+
+fn mark-lines-dirty _self: (addr trace) {
+  var self/eax: (addr trace) <- copy _self
+  var dest/edx: (addr boolean) <- get self, recompute-visible?
+  copy-to *dest, 1/true
+}
+
+fn mark-lines-clean _self: (addr trace) {
+  var self/eax: (addr trace) <- copy _self
+  var dest/edx: (addr boolean) <- get self, recompute-visible?
+  copy-to *dest, 0/false
+}
+
+fn has-errors? _self: (addr trace) -> _/eax: boolean {
+  var self/eax: (addr trace) <- copy _self
+  var max/edx: (addr int) <- get self, first-free
+  var trace-ah/eax: (addr handle array trace-line) <- get self, data
+  var _trace/eax: (addr array trace-line) <- lookup *trace-ah
+  var trace/esi: (addr array trace-line) <- copy _trace
+  var i/ecx: int <- copy 0
+  {
+    compare i, *max
+    break-if->=
+    var offset/eax: (offset trace-line) <- compute-offset trace, i
+    var curr/eax: (addr trace-line) <- index trace, offset
+    var curr-label-ah/eax: (addr handle array byte) <- get curr, label
+    var curr-label/eax: (addr array byte) <- lookup *curr-label-ah
+    var is-error?/eax: boolean <- string-equal? curr-label, "error"
+    compare is-error?, 0/false
+    {
+      break-if-=
+      return 1/true
+    }
+    i <- increment
+    loop
+  }
+  return 0/false
+}
+
+fn trace _self: (addr trace), label: (addr array byte), message: (addr stream byte) {
+  var self/esi: (addr trace) <- copy _self
+  compare self, 0
+  break-if-=
+  var data-ah/eax: (addr handle array trace-line) <- get self, data
+  var data/eax: (addr array trace-line) <- lookup *data-ah
+  var index-addr/edi: (addr int) <- get self, first-free
+  var index/ecx: int <- copy *index-addr
+  var offset/ecx: (offset trace-line) <- compute-offset data, index
+  var dest/eax: (addr trace-line) <- index data, offset
+  var depth/ecx: (addr int) <- get self, curr-depth
+  rewind-stream message
+  initialize-trace-line *depth, label, message, dest
+  increment *index-addr
+}
+
+fn trace-text self: (addr trace), label: (addr array byte), s: (addr array byte) {
+  compare self, 0
+  break-if-=
+  var data-storage: (stream byte 0x100)
+  var data/eax: (addr stream byte) <- address data-storage
+  write data, s
+  trace self, label, data
+}
+
+fn error self: (addr trace), message: (addr array byte) {
+  trace-text self, "error", message
+}
+
+fn initialize-trace-line depth: int, label: (addr array byte), data: (addr stream byte), _out: (addr trace-line) {
+  var out/edi: (addr trace-line) <- copy _out
+  # depth
+  var src/eax: int <- copy depth
+  var dest/ecx: (addr int) <- get out, depth
+  copy-to *dest, src
+  # label
+  var dest/eax: (addr handle array byte) <- get out, label
+  copy-array-object label, dest
+  # data
+  var dest/eax: (addr handle array byte) <- get out, data
+  stream-to-array data, dest
+}
+
+fn trace-lower _self: (addr trace) {
+  var self/esi: (addr trace) <- copy _self
+  compare self, 0
+  break-if-=
+  var depth/eax: (addr int) <- get self, curr-depth
+  increment *depth
+}
+
+fn trace-higher _self: (addr trace) {
+  var self/esi: (addr trace) <- copy _self
+  compare self, 0
+  break-if-=
+  var depth/eax: (addr int) <- get self, curr-depth
+  decrement *depth
+}
+
+fn render-trace screen: (addr screen), _self: (addr trace), xmin: int, ymin: int, xmax: int, ymax: int, show-cursor?: boolean -> _/ecx: int {
+  var already-hiding-lines?: boolean
+  var y/ecx: int <- copy ymin
+  var self/esi: (addr trace) <- copy _self
+  compare self, 0
+  {
+    break-if-!=
+    return ymin
+  }
+  clamp-cursor-to-top self, y
+  var trace-ah/eax: (addr handle array trace-line) <- get self, data
+  var _trace/eax: (addr array trace-line) <- lookup *trace-ah
+  var trace/edi: (addr array trace-line) <- copy _trace
+  var i/edx: int <- copy 0
+  var max-addr/ebx: (addr int) <- get self, first-free
+  var max/ebx: int <- copy *max-addr
+  $render-trace:loop: {
+    compare i, max
+    break-if->=
+    $render-trace:iter: {
+      var offset/ebx: (offset trace-line) <- compute-offset trace, i
+      var curr/ebx: (addr trace-line) <- index trace, offset
+      var curr-label-ah/eax: (addr handle array byte) <- get curr, label
+      var curr-label/eax: (addr array byte) <- lookup *curr-label-ah
+      var bg/edi: int <- copy 0/black
+      compare show-cursor?, 0/false
+      {
+        break-if-=
+        var cursor-y/eax: (addr int) <- get self, cursor-y
+        compare *cursor-y, y
+        break-if-!=
+        bg <- copy 7/cursor-line-bg
+        var cursor-line-index/eax: (addr int) <- get self, cursor-line-index
+        copy-to *cursor-line-index, i
+      }
+      # always display errors
+      var is-error?/eax: boolean <- string-equal? curr-label, "error"
+      {
+        compare is-error?, 0/false
+        break-if-=
+        y <- render-trace-line screen, curr, xmin, y, xmax, ymax, 0xc/fg=trace-error, bg
+        copy-to already-hiding-lines?, 0/false
+        break $render-trace:iter
+      }
+      # display expanded lines
+      var display?/eax: boolean <- should-render? self, curr
+      {
+        compare display?, 0/false
+        break-if-=
+        y <- render-trace-line screen, curr, xmin, y, xmax, ymax, 9/fg=blue, bg
+        copy-to already-hiding-lines?, 0/false
+        break $render-trace:iter
+      }
+      # ignore the rest
+      compare already-hiding-lines?, 0/false
+      {
+        break-if-!=
+        var x/eax: int <- copy xmin
+        x, y <- draw-text-wrapping-right-then-down screen, "...", xmin, ymin, xmax, ymax, x, y, 9/fg=trace, bg
+        y <- increment
+        copy-to already-hiding-lines?, 1/true
+      }
+    }
+    i <- increment
+    loop
+  }
+  # prevent cursor from going too far down
+  clamp-cursor-to-bottom self, y, screen, xmin, ymin, xmax, ymax
+  mark-lines-clean self
+  return y
+}
+
+fn render-trace-line screen: (addr screen), _self: (addr trace-line), xmin: int, ymin: int, xmax: int, ymax: int, fg: int, bg: int -> _/ecx: int {
+  var self/esi: (addr trace-line) <- copy _self
+  var xsave/edx: int <- copy xmin
+  var y/ecx: int <- copy ymin
+  var label-ah/eax: (addr handle array byte) <- get self, label
+  var _label/eax: (addr array byte) <- lookup *label-ah
+  var label/ebx: (addr array byte) <- copy _label
+  var is-error?/eax: boolean <- string-equal? label, "error"
+  compare is-error?, 0/false
+  {
+    break-if-!=
+    var x/eax: int <- copy xsave
+    {
+      var depth/edx: (addr int) <- get self, depth
+      x, y <- draw-int32-decimal-wrapping-right-then-down screen, *depth, xmin, ymin, xmax, ymax, x, y, fg, bg
+      x, y <- draw-text-wrapping-right-then-down screen, " ", xmin, ymin, xmax, ymax, x, y, fg, bg
+      # don't show label in UI; it's just for tests
+    }
+    xsave <- copy x
+  }
+  var data-ah/eax: (addr handle array byte) <- get self, data
+  var _data/eax: (addr array byte) <- lookup *data-ah
+  var data/ebx: (addr array byte) <- copy _data
+  var x/eax: int <- copy xsave
+  x, y <- draw-text-wrapping-right-then-down screen, data, xmin, ymin, xmax, ymax, x, y, fg, bg
+  y <- increment
+  return y
+}
+
+fn should-render? _self: (addr trace), _line: (addr trace-line) -> _/eax: boolean {
+  var self/esi: (addr trace) <- copy _self
+  # if visible? is already cached, just return it
+  var dest/edx: (addr boolean) <- get self, recompute-visible?
+  compare *dest, 0/false
+  {
+    break-if-!=
+    var line/eax: (addr trace-line) <- copy _line
+    var result/eax: (addr boolean) <- get line, visible?
+    return *result
+  }
+  # recompute
+  var candidates-ah/eax: (addr handle array trace-line) <- get self, visible
+  var candidates/eax: (addr array trace-line) <- lookup *candidates-ah
+  var i/ecx: int <- copy 0
+  var len/edx: int <- length candidates
+  {
+    compare i, len
+    break-if->=
+    {
+      var curr-offset/ecx: (offset trace-line) <- compute-offset candidates, i
+      var curr/ecx: (addr trace-line) <- index candidates, curr-offset
+      var match?/eax: boolean <- trace-lines-equal? curr, _line
+      compare match?, 0/false
+      break-if-=
+      var line/eax: (addr trace-line) <- copy _line
+      var dest/eax: (addr boolean) <- get line, visible?
+      copy-to *dest, 1/true
+      return 1/true
+    }
+    i <- increment
+    loop
+  }
+  var line/eax: (addr trace-line) <- copy _line
+  var dest/eax: (addr boolean) <- get line, visible?
+  copy-to *dest, 0/false
+  return 0/false
+}
+
+# this is probably super-inefficient, string comparing every trace line
+# against every visible line on every render
+fn trace-lines-equal? _a: (addr trace-line), _b: (addr trace-line) -> _/eax: boolean {
+  var a/esi: (addr trace-line) <- copy _a
+  var b/edi: (addr trace-line) <- copy _b
+  var a-depth/ecx: (addr int) <- get a, depth
+  var b-depth/edx: (addr int) <- get b, depth
+  var benchmark/eax: int <- copy *b-depth
+  compare *a-depth, benchmark
+  {
+    break-if-=
+    return 0/false
+  }
+  var a-label-ah/eax: (addr handle array byte) <- get a, label
+  var _a-label/eax: (addr array byte) <- lookup *a-label-ah
+  var a-label/ecx: (addr array byte) <- copy _a-label
+  var b-label-ah/ebx: (addr handle array byte) <- get b, label
+  var b-label/eax: (addr array byte) <- lookup *b-label-ah
+  var label-match?/eax: boolean <- string-equal? a-label, b-label
+  {
+    compare label-match?, 0/false
+    break-if-!=
+    return 0/false
+  }
+  var a-data-ah/eax: (addr handle array byte) <- get a, data
+  var _a-data/eax: (addr array byte) <- lookup *a-data-ah
+  var a-data/ecx: (addr array byte) <- copy _a-data
+  var b-data-ah/ebx: (addr handle array byte) <- get b, data
+  var b-data/eax: (addr array byte) <- lookup *b-data-ah
+  var data-match?/eax: boolean <- string-equal? a-data, b-data
+  return data-match?
+}
+
+fn clamp-cursor-to-top _self: (addr trace), _y: int {
+  var y/ecx: int <- copy _y
+  var self/esi: (addr trace) <- copy _self
+  var cursor-y/eax: (addr int) <- get self, cursor-y
+  compare *cursor-y, y
+  break-if->=
+  copy-to *cursor-y, y
+}
+
+# extremely hacky; consider deleting test-render-trace-empty-3 when you clean this up
+fn clamp-cursor-to-bottom _self: (addr trace), _y: int, screen: (addr screen), xmin: int, ymin: int, xmax: int, ymax: int {
+  var y/ebx: int <- copy _y
+  compare y, ymin
+  {
+    break-if->
+    return
+  }
+  y <- decrement
+  var self/esi: (addr trace) <- copy _self
+  var cursor-y/eax: (addr int) <- get self, cursor-y
+  compare *cursor-y, y
+  break-if-<=
+  copy-to *cursor-y, y
+  # redraw cursor-line
+  # TODO: ugly duplication
+  var trace-ah/eax: (addr handle array trace-line) <- get self, data
+  var trace/eax: (addr array trace-line) <- lookup *trace-ah
+  var cursor-line-index-addr/ecx: (addr int) <- get self, cursor-line-index
+  var cursor-line-index/ecx: int <- copy *cursor-line-index-addr
+  var first-free/edx: (addr int) <- get self, first-free
+  compare cursor-line-index, *first-free
+  {
+    break-if-<
+    return
+  }
+  var cursor-offset/ecx: (offset trace-line) <- compute-offset trace, cursor-line-index
+  var cursor-line/ecx: (addr trace-line) <- index trace, cursor-offset
+  var display?/eax: boolean <- should-render? self, cursor-line
+  {
+    compare display?, 0/false
+    break-if-=
+    var dummy/ecx: int <- render-trace-line screen, cursor-line, xmin, y, xmax, ymax, 9/fg=blue, 7/cursor-line-bg
+    return
+  }
+  var dummy1/eax: int <- copy 0
+  var dummy2/ecx: int <- copy 0
+  dummy1, dummy2 <- draw-text-wrapping-right-then-down screen, "...", xmin, ymin, xmax, ymax, xmin, y, 9/fg=trace, 7/cursor-line-bg
+}
+
+fn test-render-trace-empty {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 5/xmax, 4/ymax, 0/no-cursor
+  #
+  check-ints-equal y, 0, "F - test-render-trace-empty/cursor"
+  check-screen-row screen,                                  0/y, "    ", "F - test-render-trace-empty"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "    ", "F - test-render-trace-empty/bg"
+}
+
+fn test-render-trace-empty-2 {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 2/ymin, 5/xmax, 4/ymax, 0/no-cursor  # cursor below top row
+  #
+  check-ints-equal y, 2, "F - test-render-trace-empty-2/cursor"
+  check-screen-row screen,                                  2/y, "    ", "F - test-render-trace-empty-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "    ", "F - test-render-trace-empty-2/bg"
+}
+
+fn test-render-trace-empty-3 {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 2/ymin, 5/xmax, 4/ymax, 1/show-cursor  # try show cursor
+  # still no cursor to show
+  check-ints-equal y, 2, "F - test-render-trace-empty-3/cursor"
+  check-screen-row screen,                                  1/y, "    ", "F - test-render-trace-empty-3/line-above-cursor"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "    ", "F - test-render-trace-empty-3/bg-for-line-above-cursor"
+  check-screen-row screen,                                  2/y, "    ", "F - test-render-trace-empty-3"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "    ", "F - test-render-trace-empty-3/bg"
+}
+
+fn test-render-trace-collapsed-by-default {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  trace-text t, "l", "data"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 5/xmax, 4/ymax, 0/no-cursor
+  #
+  check-ints-equal y, 1, "F - test-render-trace-collapsed-by-default/cursor"
+  check-screen-row screen, 0/y, "... ", "F - test-render-trace-collapsed-by-default"
+}
+
+fn test-render-trace-error {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  error t, "error"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0xa/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 0/no-cursor
+  #
+  check-ints-equal y, 1, "F - test-render-trace-error/cursor"
+  check-screen-row screen, 0/y, "error", "F - test-render-trace-error"
+}
+
+fn test-render-trace-error-at-start {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  error t, "error"
+  trace-text t, "l", "data"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0xa/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 0/no-cursor
+  #
+  check-ints-equal y, 2, "F - test-render-trace-error-at-start/cursor"
+  check-screen-row screen, 0/y, "error", "F - test-render-trace-error-at-start/0"
+  check-screen-row screen, 1/y, "...  ", "F - test-render-trace-error-at-start/1"
+}
+
+fn test-render-trace-error-at-end {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "data"
+  error t, "error"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0xa/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 0/no-cursor
+  #
+  check-ints-equal y, 2, "F - test-render-trace-error-at-end/cursor"
+  check-screen-row screen, 0/y, "...  ", "F - test-render-trace-error-at-end/0"
+  check-screen-row screen, 1/y, "error", "F - test-render-trace-error-at-end/1"
+}
+
+fn test-render-trace-error-in-the-middle {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  error t, "error"
+  trace-text t, "l", "line 3"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0xa/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 0/no-cursor
+  #
+  check-ints-equal y, 3, "F - test-render-trace-error-in-the-middle/cursor"
+  check-screen-row screen, 0/y, "...  ", "F - test-render-trace-error-in-the-middle/0"
+  check-screen-row screen, 1/y, "error", "F - test-render-trace-error-in-the-middle/1"
+  check-screen-row screen, 2/y, "...  ", "F - test-render-trace-error-in-the-middle/2"
+}
+
+fn test-render-trace-cursor-in-single-line {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  error t, "error"
+  trace-text t, "l", "line 3"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0xa/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...   ", "F - test-render-trace-cursor-in-single-line/0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||   ", "F - test-render-trace-cursor-in-single-line/0/cursor"
+  check-screen-row screen,                                  1/y, "error ", "F - test-render-trace-cursor-in-single-line/1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "      ", "F - test-render-trace-cursor-in-single-line/1/cursor"
+  check-screen-row screen,                                  2/y, "...   ", "F - test-render-trace-cursor-in-single-line/2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "      ", "F - test-render-trace-cursor-in-single-line/2/cursor"
+}
+
+fn render-trace-menu screen: (addr screen) {
+  var width/eax: int <- copy 0
+  var height/ecx: int <- copy 0
+  width, height <- screen-size screen
+  var y/ecx: int <- copy height
+  y <- decrement
+  set-cursor-position screen, 0/x, y
+  draw-text-rightward-from-cursor screen, " ctrl-s ", width, 0/fg, 7/bg=grey
+  draw-text-rightward-from-cursor screen, " run sandbox  ", width, 7/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " ctrl-d ", width, 0/fg, 7/bg=grey
+  draw-text-rightward-from-cursor screen, " cursor down  ", width, 7/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " ctrl-u ", width, 0/fg, 7/bg=grey
+  draw-text-rightward-from-cursor screen, " cursor up  ", width, 7/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " tab ", width, 0/fg, 3/bg=cyan
+  draw-text-rightward-from-cursor screen, " move to sandbox  ", width, 7/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " enter ", width, 0/fg, 7/bg=grey
+  draw-text-rightward-from-cursor screen, " expand  ", width, 7/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " backspace ", width, 0/fg, 7/bg=grey
+  draw-text-rightward-from-cursor screen, " collapse  ", width, 7/fg, 0/bg
+}
+
+fn edit-trace _self: (addr trace), key: grapheme {
+  var self/esi: (addr trace) <- copy _self
+  # cursor down
+  {
+    compare key, 4/ctrl-d
+    break-if-!=
+    var cursor-y/eax: (addr int) <- get self, cursor-y
+    increment *cursor-y
+    return
+  }
+  # cursor up
+  {
+    compare key, 0x15/ctrl-u
+    break-if-!=
+    var cursor-y/eax: (addr int) <- get self, cursor-y
+    decrement *cursor-y
+    return
+  }
+  # enter = expand
+  {
+    compare key, 0xa/newline
+    break-if-!=
+    expand self
+    return
+  }
+  # backspace = collapse
+  {
+    compare key, 8/backspace
+    break-if-!=
+    collapse self
+    return
+  }
+}
+
+fn expand _self: (addr trace) {
+  var self/esi: (addr trace) <- copy _self
+  var trace-ah/eax: (addr handle array trace-line) <- get self, data
+  var _trace/eax: (addr array trace-line) <- lookup *trace-ah
+  var trace/edi: (addr array trace-line) <- copy _trace
+  var cursor-line-index-addr/ecx: (addr int) <- get self, cursor-line-index
+  var cursor-line-index/ecx: int <- copy *cursor-line-index-addr
+  var cursor-line-offset/eax: (offset trace-line) <- compute-offset trace, cursor-line-index
+  var cursor-line/edx: (addr trace-line) <- index trace, cursor-line-offset
+  var cursor-line-visible?/eax: (addr boolean) <- get cursor-line, visible?
+  var cursor-line-depth/ebx: (addr int) <- get cursor-line, depth
+  var target-depth/ebx: int <- copy *cursor-line-depth
+  # if cursor-line is already visible, increment target-depth
+  compare *cursor-line-visible?, 0/false
+  {
+    break-if-=
+    target-depth <- increment
+  }
+  # reveal the run of lines starting at cursor-line-index with depth target-depth
+  var i/ecx: int <- copy cursor-line-index
+  var max/edx: (addr int) <- get self, first-free
+  {
+    compare i, *max
+    break-if->=
+    var curr-line-offset/eax: (offset trace-line) <- compute-offset trace, i
+    var curr-line/edx: (addr trace-line) <- index trace, curr-line-offset
+    var curr-line-depth/eax: (addr int) <- get curr-line, depth
+    compare *curr-line-depth, target-depth
+    break-if-<
+    {
+      break-if-!=
+      var curr-line-visible?/eax: (addr boolean) <- get curr-line, visible?
+      copy-to *curr-line-visible?, 1/true
+      reveal-trace-line self, curr-line
+    }
+    i <- increment
+    loop
+  }
+}
+
+fn collapse _self: (addr trace) {
+  var self/esi: (addr trace) <- copy _self
+  var trace-ah/eax: (addr handle array trace-line) <- get self, data
+  var _trace/eax: (addr array trace-line) <- lookup *trace-ah
+  var trace/edi: (addr array trace-line) <- copy _trace
+  var cursor-line-index-addr/ecx: (addr int) <- get self, cursor-line-index
+  var cursor-line-index/ecx: int <- copy *cursor-line-index-addr
+  var cursor-line-offset/eax: (offset trace-line) <- compute-offset trace, cursor-line-index
+  var cursor-line/edx: (addr trace-line) <- index trace, cursor-line-offset
+  var cursor-line-visible?/eax: (addr boolean) <- get cursor-line, visible?
+  # if cursor-line is not visible, do nothing
+  compare *cursor-line-visible?, 0/false
+  {
+    break-if-!=
+    return
+  }
+  # hide all lines between previous and next line with a lower depth
+  var cursor-line-depth/ebx: (addr int) <- get cursor-line, depth
+  var cursor-y/edx: (addr int) <- get self, cursor-y
+  var target-depth/ebx: int <- copy *cursor-line-depth
+  var i/ecx: int <- copy cursor-line-index
+  $collapse:loop1: {
+    compare i, 0
+    break-if-<
+    var curr-line-offset/eax: (offset trace-line) <- compute-offset trace, i
+    var curr-line/eax: (addr trace-line) <- index trace, curr-line-offset
+    {
+      var curr-line-depth/eax: (addr int) <- get curr-line, depth
+      compare *curr-line-depth, target-depth
+      break-if-< $collapse:loop1
+    }
+    # if cursor-line is visible, decrement cursor-y
+    {
+      var curr-line-visible?/eax: (addr boolean) <- get curr-line, visible?
+      compare *curr-line-visible?, 0/false
+      break-if-=
+      decrement *cursor-y
+    }
+    i <- decrement
+    loop
+  }
+  i <- increment
+  var max/edx: (addr int) <- get self, first-free
+  $collapse:loop2: {
+    compare i, *max
+    break-if->=
+    var curr-line-offset/eax: (offset trace-line) <- compute-offset trace, i
+    var curr-line/edx: (addr trace-line) <- index trace, curr-line-offset
+    var curr-line-depth/eax: (addr int) <- get curr-line, depth
+    compare *curr-line-depth, target-depth
+    break-if-<
+    {
+      hide-trace-line self, curr-line
+      var curr-line-visible?/eax: (addr boolean) <- get curr-line, visible?
+      copy-to *curr-line-visible?, 0/false
+    }
+    i <- increment
+    loop
+  }
+}
+
+# the 'visible' array is not required to be in order
+# elements can also be deleted out of order
+# so it can have holes
+# however, lines in it always have visible? set
+# we'll use visible? being unset as a sign of emptiness
+fn reveal-trace-line _self: (addr trace), line: (addr trace-line) {
+  var self/esi: (addr trace) <- copy _self
+  var visible-ah/eax: (addr handle array trace-line) <- get self, visible
+  var visible/eax: (addr array trace-line) <- lookup *visible-ah
+  var i/ecx: int <- copy 0
+  var len/edx: int <- length visible
+  {
+    compare i, len
+    break-if->=
+    var curr-offset/edx: (offset trace-line) <- compute-offset visible, i
+    var curr/edx: (addr trace-line) <- index visible, curr-offset
+    var curr-visible?/eax: (addr boolean) <- get curr, visible?
+    compare *curr-visible?, 0/false
+    {
+      break-if-!=
+      # empty slot found
+      copy-object line, curr
+      return
+    }
+    i <- increment
+    loop
+  }
+  abort "too many visible lines; increase size of array trace.visible"
+}
+
+fn hide-trace-line _self: (addr trace), line: (addr trace-line) {
+  var self/esi: (addr trace) <- copy _self
+  var visible-ah/eax: (addr handle array trace-line) <- get self, visible
+  var visible/eax: (addr array trace-line) <- lookup *visible-ah
+  var i/ecx: int <- copy 0
+  var len/edx: int <- length visible
+  {
+    compare i, len
+    break-if->=
+    var curr-offset/edx: (offset trace-line) <- compute-offset visible, i
+    var curr/edx: (addr trace-line) <- index visible, curr-offset
+    var found?/eax: boolean <- trace-lines-equal? curr, line
+    compare found?, 0/false
+    {
+      break-if-=
+      clear-object curr
+    }
+    i <- increment
+    loop
+  }
+}
+
+fn test-cursor-down-and-up-within-trace {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  error t, "error"
+  trace-text t, "l", "line 3"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0xa/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...   ", "F - test-cursor-down-and-up-within-trace/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||   ", "F - test-cursor-down-and-up-within-trace/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "error ", "F - test-cursor-down-and-up-within-trace/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "      ", "F - test-cursor-down-and-up-within-trace/pre-1/cursor"
+  check-screen-row screen,                                  2/y, "...   ", "F - test-cursor-down-and-up-within-trace/pre-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "      ", "F - test-cursor-down-and-up-within-trace/pre-2/cursor"
+  # cursor down
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...   ", "F - test-cursor-down-and-up-within-trace/down-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "      ", "F - test-cursor-down-and-up-within-trace/down-0/cursor"
+  check-screen-row screen,                                  1/y, "error ", "F - test-cursor-down-and-up-within-trace/down-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "||||| ", "F - test-cursor-down-and-up-within-trace/down-1/cursor"
+  check-screen-row screen,                                  2/y, "...   ", "F - test-cursor-down-and-up-within-trace/down-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "      ", "F - test-cursor-down-and-up-within-trace/down-2/cursor"
+  # cursor up
+  edit-trace t, 0x15/ctrl-u
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...   ", "F - test-cursor-down-and-up-within-trace/up-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||   ", "F - test-cursor-down-and-up-within-trace/up-0/cursor"
+  check-screen-row screen,                                  1/y, "error ", "F - test-cursor-down-and-up-within-trace/up-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "      ", "F - test-cursor-down-and-up-within-trace/up-1/cursor"
+  check-screen-row screen,                                  2/y, "...   ", "F - test-cursor-down-and-up-within-trace/up-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "      ", "F - test-cursor-down-and-up-within-trace/up-2/cursor"
+}
+
+fn test-cursor-down-past-bottom-of-trace {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  error t, "error"
+  trace-text t, "l", "line 3"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0xa/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...   ", "F - test-cursor-down-past-bottom-of-trace/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||   ", "F - test-cursor-down-past-bottom-of-trace/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "error ", "F - test-cursor-down-past-bottom-of-trace/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "      ", "F - test-cursor-down-past-bottom-of-trace/pre-1/cursor"
+  check-screen-row screen,                                  2/y, "...   ", "F - test-cursor-down-past-bottom-of-trace/pre-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "      ", "F - test-cursor-down-past-bottom-of-trace/pre-2/cursor"
+  # cursor down several times
+  edit-trace t, 4/ctrl-d
+  edit-trace t, 4/ctrl-d
+  edit-trace t, 4/ctrl-d
+  edit-trace t, 4/ctrl-d
+  edit-trace t, 4/ctrl-d
+  # hack: we do need to render to make this test pass; we're mixing state management with rendering
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0xa/xmax, 4/ymax, 1/show-cursor
+  # cursor clamps at bottom
+  check-screen-row screen,                                  0/y, "...   ", "F - test-cursor-down-past-bottom-of-trace/down-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "      ", "F - test-cursor-down-past-bottom-of-trace/down-0/cursor"
+  check-screen-row screen,                                  1/y, "error ", "F - test-cursor-down-past-bottom-of-trace/down-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "      ", "F - test-cursor-down-past-bottom-of-trace/down-1/cursor"
+  check-screen-row screen,                                  2/y, "...   ", "F - test-cursor-down-past-bottom-of-trace/down-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "|||   ", "F - test-cursor-down-past-bottom-of-trace/down-2/cursor"
+}
+
+fn test-expand-within-trace {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-text t, "l", "line 2"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...      ", "F - test-expand-within-trace/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||      ", "F - test-expand-within-trace/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "         ", "F - test-expand-within-trace/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "         ", "F - test-expand-within-trace/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1 ", "F - test-expand-within-trace/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||||||| ", "F - test-expand-within-trace/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2 ", "F - test-expand-within-trace/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "         ", "F - test-expand-within-trace/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "         ", "F - test-expand-within-trace/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "         ", "F - test-expand-within-trace/expand-2/cursor"
+}
+
+fn test-trace-expand-skips-lower-depth {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-lower t
+  trace-text t, "l", "line 2"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...      ", "F - test-trace-expand-skips-lower-depth/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||      ", "F - test-trace-expand-skips-lower-depth/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "         ", "F - test-trace-expand-skips-lower-depth/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "         ", "F - test-trace-expand-skips-lower-depth/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1 ", "F - test-trace-expand-skips-lower-depth/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||||||| ", "F - test-trace-expand-skips-lower-depth/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "...      ", "F - test-trace-expand-skips-lower-depth/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "         ", "F - test-trace-expand-skips-lower-depth/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "         ", "F - test-trace-expand-skips-lower-depth/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "         ", "F - test-trace-expand-skips-lower-depth/expand-2/cursor"
+}
+
+fn test-trace-expand-continues-past-lower-depth {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-lower t
+  trace-text t, "l", "line 1.1"
+  trace-higher t
+  trace-text t, "l", "line 2"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...      ", "F - test-trace-expand-continues-past-lower-depth/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||      ", "F - test-trace-expand-continues-past-lower-depth/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "         ", "F - test-trace-expand-continues-past-lower-depth/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "         ", "F - test-trace-expand-continues-past-lower-depth/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1 ", "F - test-trace-expand-continues-past-lower-depth/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||||||| ", "F - test-trace-expand-continues-past-lower-depth/expand-0/cursor"
+  # TODO: might be too wasteful to show every place where lines are hidden
+  check-screen-row screen,                                  1/y, "...      ", "F - test-trace-expand-continues-past-lower-depth/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "         ", "F - test-trace-expand-continues-past-lower-depth/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2 ", "F - test-trace-expand-continues-past-lower-depth/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "         ", "F - test-trace-expand-continues-past-lower-depth/expand-2/cursor"
+}
+
+fn test-trace-expand-stops-at-higher-depth {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1.1"
+  trace-lower t
+  trace-text t, "l", "line 1.1.1"
+  trace-higher t
+  trace-text t, "l", "line 1.2"
+  trace-higher t
+  trace-text t, "l", "line 2"
+  trace-lower t
+  trace-text t, "l", "line 2.1"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 8/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-expand-stops-at-higher-depth/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-expand-stops-at-higher-depth/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-expand-stops-at-higher-depth/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-expand-stops-at-higher-depth/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1.1 ", "F - test-trace-expand-stops-at-higher-depth/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||||||||| ", "F - test-trace-expand-stops-at-higher-depth/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-expand-stops-at-higher-depth/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-expand-stops-at-higher-depth/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 1.2 ", "F - test-trace-expand-stops-at-higher-depth/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-expand-stops-at-higher-depth/expand-2/cursor"
+  check-screen-row screen,                                  3/y, "...        ", "F - test-trace-expand-stops-at-higher-depth/expand-3"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 3/y, "           ", "F - test-trace-expand-stops-at-higher-depth/expand-3/cursor"
+  check-screen-row screen,                                  4/y, "           ", "F - test-trace-expand-stops-at-higher-depth/expand-4"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 4/y, "           ", "F - test-trace-expand-stops-at-higher-depth/expand-4/cursor"
+}
+
+fn test-trace-expand-twice {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-lower t
+  trace-text t, "l", "line 1.1"
+  trace-higher t
+  trace-text t, "l", "line 2"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-expand-twice/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-expand-twice/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-expand-twice/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-expand-twice/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-expand-twice/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-expand-twice/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-expand-twice/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-expand-twice/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-expand-twice/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-expand-twice/expand-2/cursor"
+  # cursor down
+  edit-trace t, 4/ctrl-d
+  # hack: we need to render here to make this test pass; we're mixing state management with rendering
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-expand-twice/down-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-expand-twice/down-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-expand-twice/down-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "|||        ", "F - test-trace-expand-twice/down-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-expand-twice/down-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-expand-twice/down-2/cursor"
+  # expand again
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-expand-twice/expand2-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-expand-twice/expand2-0/cursor"
+  check-screen-row screen,                                  1/y, "1 line 1.1 ", "F - test-trace-expand-twice/expand2-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "|||||||||| ", "F - test-trace-expand-twice/expand2-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-expand-twice/expand2-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-expand-twice/expand2-2/cursor"
+}
+
+fn test-trace-refresh-cursor {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-text t, "l", "line 2"
+  trace-text t, "l", "line 3"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-refresh-cursor/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-refresh-cursor/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-refresh-cursor/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-refresh-cursor/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-refresh-cursor/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-refresh-cursor/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2   ", "F - test-trace-refresh-cursor/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-refresh-cursor/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 3   ", "F - test-trace-refresh-cursor/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-refresh-cursor/expand-2/cursor"
+  # cursor down
+  edit-trace t, 4/ctrl-d
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-refresh-cursor/down-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-refresh-cursor/down-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2   ", "F - test-trace-refresh-cursor/down-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-refresh-cursor/down-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 3   ", "F - test-trace-refresh-cursor/down-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "||||||||   ", "F - test-trace-refresh-cursor/down-2/cursor"
+  # recreate trace
+  clear-trace t
+  trace-text t, "l", "line 1"
+  trace-text t, "l", "line 2"
+  trace-text t, "l", "line 3"
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # cursor remains unchanged
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-refresh-cursor/refresh-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-refresh-cursor/refresh-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2   ", "F - test-trace-refresh-cursor/refresh-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-refresh-cursor/refresh-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 3   ", "F - test-trace-refresh-cursor/refresh-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "||||||||   ", "F - test-trace-refresh-cursor/refresh-2/cursor"
+}
+
+fn test-trace-preserve-cursor-on-refresh {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-text t, "l", "line 2"
+  trace-text t, "l", "line 3"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-preserve-cursor-on-refresh/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-preserve-cursor-on-refresh/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-preserve-cursor-on-refresh/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-preserve-cursor-on-refresh/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-preserve-cursor-on-refresh/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-preserve-cursor-on-refresh/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2   ", "F - test-trace-preserve-cursor-on-refresh/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-preserve-cursor-on-refresh/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 3   ", "F - test-trace-preserve-cursor-on-refresh/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "              ", "F - test-trace-preserve-cursor-on-refresh/expand-2/cursor"
+  # cursor down
+  edit-trace t, 4/ctrl-d
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-preserve-cursor-on-refresh/down-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-preserve-cursor-on-refresh/down-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2   ", "F - test-trace-preserve-cursor-on-refresh/down-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-preserve-cursor-on-refresh/down-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 3   ", "F - test-trace-preserve-cursor-on-refresh/down-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "||||||||   ", "F - test-trace-preserve-cursor-on-refresh/down-2/cursor"
+  # recreate trace with slightly different lines
+  clear-trace t
+  trace-text t, "l", "line 4"
+  trace-text t, "l", "line 5"
+  trace-text t, "l", "line 3"  # cursor line is unchanged
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # cursor remains unchanged
+  check-screen-row screen,                                  0/y, "0 line 4   ", "F - test-trace-preserve-cursor-on-refresh/refresh-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-preserve-cursor-on-refresh/refresh-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 5   ", "F - test-trace-preserve-cursor-on-refresh/refresh-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-preserve-cursor-on-refresh/refresh-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 3   ", "F - test-trace-preserve-cursor-on-refresh/refresh-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "||||||||   ", "F - test-trace-preserve-cursor-on-refresh/refresh-2/cursor"
+}
+
+fn test-trace-keep-cursor-visible-on-refresh {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-text t, "l", "line 2"
+  trace-text t, "l", "line 3"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-keep-cursor-visible-on-refresh/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-keep-cursor-visible-on-refresh/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-keep-cursor-visible-on-refresh/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-keep-cursor-visible-on-refresh/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2   ", "F - test-trace-keep-cursor-visible-on-refresh/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 3   ", "F - test-trace-keep-cursor-visible-on-refresh/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "              ", "F - test-trace-keep-cursor-visible-on-refresh/expand-2/cursor"
+  # cursor down
+  edit-trace t, 4/ctrl-d
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-keep-cursor-visible-on-refresh/down-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/down-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2   ", "F - test-trace-keep-cursor-visible-on-refresh/down-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/down-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 3   ", "F - test-trace-keep-cursor-visible-on-refresh/down-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "||||||||   ", "F - test-trace-keep-cursor-visible-on-refresh/down-2/cursor"
+  # recreate trace with entirely different lines
+  clear-trace t
+  trace-text t, "l", "line 4"
+  trace-text t, "l", "line 5"
+  trace-text t, "l", "line 6"
+  mark-lines-dirty t
+  clear-screen screen
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # trace collapses, and cursor bumps up
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-keep-cursor-visible-on-refresh/refresh-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-keep-cursor-visible-on-refresh/refresh-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/refresh-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/refresh-1/cursor"
+  check-screen-row screen,                                  2/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/refresh-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-keep-cursor-visible-on-refresh/refresh-2/cursor"
+}
+
+fn test-trace-collapse-at-top {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-lower t
+  trace-text t, "l", "line 1.1"
+  trace-higher t
+  trace-text t, "l", "line 2"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse-at-top/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse-at-top/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse-at-top/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-at-top/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-collapse-at-top/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-collapse-at-top/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-collapse-at-top/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-at-top/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-collapse-at-top/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-collapse-at-top/expand-2/cursor"
+  # collapse
+  edit-trace t, 8/backspace
+  # hack: we need to render here to make this test pass; we're mixing state management with rendering
+  clear-screen screen
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-ints-equal y, 1, "F - test-trace-collapse-at-top/post-0/y"
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse-at-top/post-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse-at-top/post-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse-at-top/post-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-at-top/post-1/cursor"
+}
+
+fn test-trace-collapse {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-text t, "l", "line 2"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-collapse/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-collapse/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "0 line 2   ", "F - test-trace-collapse/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse/expand-1/cursor"
+  # cursor down
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # collapse
+  edit-trace t, 8/backspace
+  clear-screen screen
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-ints-equal y, 1, "F - test-trace-collapse/post-0/y"
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse/post-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse/post-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse/post-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse/post-1/cursor"
+}
+
+fn test-trace-collapse-skips-invisible-lines {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-lower t
+  trace-text t, "l", "line 1.1"
+  trace-higher t
+  trace-text t, "l", "line 2"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse-skips-invisible-lines/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse-skips-invisible-lines/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse-skips-invisible-lines/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-skips-invisible-lines/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # two visible lines with an invisible line in between
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-collapse-skips-invisible-lines/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-collapse-skips-invisible-lines/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-collapse-skips-invisible-lines/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-skips-invisible-lines/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-collapse-skips-invisible-lines/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-collapse-skips-invisible-lines/expand-2/cursor"
+  # cursor down to second visible line
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # collapse
+  edit-trace t, 8/backspace
+  clear-screen screen
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-ints-equal y, 1, "F - test-trace-collapse-skips-invisible-lines/post-0/y"
+  var cursor-y/eax: (addr int) <- get t, cursor-y
+  check-ints-equal *cursor-y, 0, "F - test-trace-collapse-skips-invisible-lines/post-0/cursor-y"
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse-skips-invisible-lines/post-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse-skips-invisible-lines/post-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse-skips-invisible-lines/post-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-skips-invisible-lines/post-1/cursor"
+}
+
+fn test-trace-collapse-two-levels {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-lower t
+  trace-text t, "l", "line 1.1"
+  trace-higher t
+  trace-text t, "l", "line 2"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 4/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse-two-levels/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse-two-levels/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse-two-levels/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-two-levels/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # two visible lines with an invisible line in between
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-collapse-two-levels/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-collapse-two-levels/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-collapse-two-levels/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-two-levels/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-collapse-two-levels/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-collapse-two-levels/expand-2/cursor"
+  # cursor down to ellipses
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # two visible lines with an invisible line in between
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-collapse-two-levels/expand2-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-collapse-two-levels/expand2-0/cursor"
+  check-screen-row screen,                                  1/y, "1 line 1.1 ", "F - test-trace-collapse-two-levels/expand2-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "|||||||||| ", "F - test-trace-collapse-two-levels/expand2-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-collapse-two-levels/expand2-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-collapse-two-levels/expand2-2/cursor"
+  # cursor down to second visible line
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  # collapse
+  edit-trace t, 8/backspace
+  clear-screen screen
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 4/ymax, 1/show-cursor
+  #
+  check-ints-equal y, 1, "F - test-trace-collapse-two-levels/post-0/y"
+  var cursor-y/eax: (addr int) <- get t, cursor-y
+  check-ints-equal *cursor-y, 0, "F - test-trace-collapse-two-levels/post-0/cursor-y"
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse-two-levels/post-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse-two-levels/post-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse-two-levels/post-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-two-levels/post-1/cursor"
+}
+
+fn test-trace-collapse-nested-level {
+  var t-storage: trace
+  var t/esi: (addr trace) <- address t-storage
+  initialize-trace t, 0x10, 0x10
+  #
+  trace-text t, "l", "line 1"
+  trace-lower t
+  trace-text t, "l", "line 1.1"
+  trace-higher t
+  trace-text t, "l", "line 2"
+  trace-lower t
+  trace-text t, "l", "line 2.1"
+  trace-text t, "l", "line 2.2"
+  trace-higher t
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 0x10/width, 8/height
+  #
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  #
+  check-screen-row screen,                                  0/y, "...        ", "F - test-trace-collapse-nested-level/pre-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "|||        ", "F - test-trace-collapse-nested-level/pre-0/cursor"
+  check-screen-row screen,                                  1/y, "           ", "F - test-trace-collapse-nested-level/pre-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-nested-level/pre-1/cursor"
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  # two visible lines with an invisible line in between
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-collapse-nested-level/expand-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "||||||||   ", "F - test-trace-collapse-nested-level/expand-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-collapse-nested-level/expand-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-nested-level/expand-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-collapse-nested-level/expand-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-collapse-nested-level/expand-2/cursor"
+  check-screen-row screen,                                  3/y, "...        ", "F - test-trace-collapse-nested-level/expand-3"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 3/y, "           ", "F - test-trace-collapse-nested-level/expand-3/cursor"
+  # cursor down to bottom
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  edit-trace t, 4/ctrl-d
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  # expand
+  edit-trace t, 0xa/enter
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  # two visible lines with an invisible line in between
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-collapse-nested-level/expand2-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-collapse-nested-level/expand2-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-collapse-nested-level/expand2-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-nested-level/expand2-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-collapse-nested-level/expand2-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "           ", "F - test-trace-collapse-nested-level/expand2-2/cursor"
+  check-screen-row screen,                                  3/y, "1 line 2.1 ", "F - test-trace-collapse-nested-level/expand2-3"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 3/y, "|||||||||| ", "F - test-trace-collapse-nested-level/expand2-3/cursor"
+  check-screen-row screen,                                  4/y, "1 line 2.2 ", "F - test-trace-collapse-nested-level/expand2-4"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 4/y, "           ", "F - test-trace-collapse-nested-level/expand2-4/cursor"
+  # collapse
+  edit-trace t, 8/backspace
+  clear-screen screen
+  var y/ecx: int <- render-trace screen, t, 0/xmin, 0/ymin, 0x10/xmax, 8/ymax, 1/show-cursor
+  #
+  check-ints-equal y, 4, "F - test-trace-collapse-nested-level/post-0/y"
+  var cursor-y/eax: (addr int) <- get t, cursor-y
+  check-ints-equal *cursor-y, 2, "F - test-trace-collapse-nested-level/post-0/cursor-y"
+  check-screen-row screen,                                  0/y, "0 line 1   ", "F - test-trace-collapse-nested-level/post-0"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 0/y, "           ", "F - test-trace-collapse-nested-level/post-0/cursor"
+  check-screen-row screen,                                  1/y, "...        ", "F - test-trace-collapse-nested-level/post-1"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 1/y, "           ", "F - test-trace-collapse-nested-level/post-1/cursor"
+  check-screen-row screen,                                  2/y, "0 line 2   ", "F - test-trace-collapse-nested-level/post-2"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 2/y, "||||||||   ", "F - test-trace-collapse-nested-level/post-2/cursor"
+  check-screen-row screen,                                  3/y, "...        ", "F - test-trace-collapse-nested-level/post-3"
+  check-background-color-in-screen-row screen, 7/bg=cursor, 3/y, "           ", "F - test-trace-collapse-nested-level/post-3/cursor"
+}
diff --git a/shell/vimrc.vim b/shell/vimrc.vim
new file mode 100644
index 00000000..348fe364
--- /dev/null
+++ b/shell/vimrc.vim
@@ -0,0 +1,2 @@
+" when opening files in this directory, load vimrc from cwd (top-level)
+source vimrc.vim