about summary refs log tree commit diff stats
path: root/shell/trace.mu
diff options
context:
space:
mode:
Diffstat (limited to 'shell/trace.mu')
-rw-r--r--shell/trace.mu1449
1 files changed, 1449 insertions, 0 deletions
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"
+}